OpenSL ES: 利用OpenSL ES实现录音功能

时间:2024-04-16 10:20:22

SLAudioRecorder.h

//
// Created by yongdaimi on 2020/3/2.
//

#ifndef DONGLEAPPDEMO_SLAUDIORECORDER_H
#define DONGLEAPPDEMO_SLAUDIORECORDER_H

#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>

#include <stdio.h>


class SLAudioRecorder
{

private:
    int      mIndex;
    short    *mRecordBuffs[2];
    unsigned mRecordBufferSize;

    bool mIsRecording;

    FILE *mFile;

    SLObjectItf                   mEngineObj;
    SLEngineItf                   mEngineInterface;
    SLObjectItf                   mRecorderObj;
    SLRecordItf                   mRecorderInterface;
    SLAndroidSimpleBufferQueueItf mBufferQueue;

public:
    SLAudioRecorder(const char *fileSavePath);

    /** Call this method to start audio recording */
    bool start();

    /** Call this method to stop audio recording */
    void stop();

    ~SLAudioRecorder();

private:

    bool initEngine();

    /** Call this method to initialize an audio recorder */
    bool initRecorder();

    /** Call this method to release the resources related to recording */
    void release();

    /** This method is called every time an audio frame is recorded*/
    static void recorderCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext);

};


#endif //DONGLEAPPDEMO_SLAUDIORECORDER_H

SLAudioRecorder.cpp

//
// Created by yongdaimi on 2020/3/2.
//


#include "SLAudioRecorder.h"
#include "SLLog.h"


#define RECORD_BUFFER_QUEUE_NUM 2
#define RECORDER_FRAMES 2048


SLAudioRecorder::SLAudioRecorder(const char *fileSavePath) :
        mEngineObj(nullptr),
        mEngineInterface(nullptr),
        mRecorderObj(nullptr),
        mRecorderInterface(nullptr),
        mRecordBufferSize(RECORDER_FRAMES),
        mBufferQueue(nullptr),
        mIndex(0), mIsRecording(false)
{

    this->mFile = fopen(fileSavePath, "w");

    this->mRecordBuffs[0] = new short[mRecordBufferSize]();
    this->mRecordBuffs[1] = new short[mRecordBufferSize]();
}


bool SLAudioRecorder::initEngine()
{

    SLresult ret;

    ret = slCreateEngine(&mEngineObj, 0, nullptr, 0, nullptr, nullptr);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("slCreateEngine obj failed");
        return false;
    }

    ret = (*mEngineObj)->Realize(mEngineObj, SL_BOOLEAN_FALSE);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("sl engineObj realize failed");
        return false;
    }

    ret = (*mEngineObj)->GetInterface(mEngineObj, SL_IID_ENGINE, &mEngineInterface);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("sl get engine interface failed");
        return false;
    }

    XLOGI("init engine success");
    return true;
}


bool SLAudioRecorder::initRecorder()
{

    if (!mEngineInterface) {
        if (!initEngine()) {
            XLOGE("init engine failed");
            return false;
        }
    }

    SLresult ret;

    // Configuration the recorder\'s audio data source
    SLDataLocator_IODevice device = {
            SL_DATALOCATOR_IODEVICE,
            SL_IODEVICE_AUDIOINPUT,
            SL_DEFAULTDEVICEID_AUDIOINPUT,
            nullptr // Must be Null if deviceID parameter is to be used.
    };

    SLDataSource dataSource = {&device,
                               nullptr}; // This parameter is ignored if pLocator is SLDataLocator_IODevice.

    // Configuration the recorder\'s audio data save way.
    SLDataLocator_AndroidSimpleBufferQueue queue = {
            SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
            RECORD_BUFFER_QUEUE_NUM
    };

    // Audio Format: PCM
    // Audio Channels: 2
    // SampleRate: 44100
    // SampleFormat: 16bit
    // Endian: Little Endian
    SLDataFormat_PCM pcmFormat = {
            SL_DATAFORMAT_PCM,
            1,
            SL_SAMPLINGRATE_16,
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_SPEAKER_FRONT_CENTER,
            SL_BYTEORDER_LITTLEENDIAN
    };

    SLDataSink dataSink = {&queue, &pcmFormat};

    // Configure the interface that the recorder needs to support.
    SLInterfaceID ids[]          = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
    SLboolean     ids_required[] = {SL_BOOLEAN_TRUE};
    SLuint32      numInterfaces  = 1;

    // Create the audio recorder.
    ret = (*mEngineInterface)->CreateAudioRecorder(mEngineInterface, &mRecorderObj,
                                                   &dataSource,
                                                   &dataSink,
                                                   numInterfaces,
                                                   ids,
                                                   ids_required);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("CreateAudioRecorder() failed");
        return false;
    }

    ret = (*mRecorderObj)->Realize(mRecorderObj, SL_BOOLEAN_FALSE);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("mRecorderObj realize failed");
        return false;
    }

    ret = (*mRecorderObj)->GetInterface(mRecorderObj, SL_IID_RECORD, &mRecorderInterface);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("mRecorderObj get SL_IID_RECORD interface failed");
        return false;
    }

    ret = (*mRecorderObj)->GetInterface(mRecorderObj, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
                                        &mBufferQueue);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("mRecorderObj get simpleBufferQueue interface failed");
        return false;
    }

    ret = (*mBufferQueue)->RegisterCallback(mBufferQueue, recorderCallback, this);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("register read pcm data callback failed");
        return false;
    }

    XLOGI("init recorder success");
    return true;

}

bool SLAudioRecorder::start()
{
    if (mIsRecording) return true;

    if (!mRecorderInterface) {
        if (!initRecorder()) {
            XLOGE("init recorder failed");
            return false;
        }
    }

    if (!mFile) {
        XLOGE("record file open failed");
        return false;
    }

    SLresult ret;

    ret = (*mRecorderInterface)->SetRecordState(mRecorderInterface, SL_RECORDSTATE_STOPPED);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("mRecorderInterface stop record state failed");
        return false;
    }

    ret = (*mBufferQueue)->Clear(mBufferQueue);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("mBufferQueue clear bufferQueue failed");
        return false;
    }

    // Enqueue an empty buffer to fill data when recording audio to the recorder.
    ret = (*mBufferQueue)->Enqueue(mBufferQueue, mRecordBuffs[mIndex],
                                   mRecordBufferSize * sizeof(short));
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("mBufferQueue enqueue buffer failed");
        return false;
    }

    ret = (*mRecorderInterface)->SetRecordState(mRecorderInterface, SL_RECORDSTATE_RECORDING);
    if (ret != SL_RESULT_SUCCESS) {
        XLOGE("mRecorderInterface start record state failed");
        return false;
    }

    mIsRecording = true;
    XLOGI("audioRecorder start recording...");
    return true;
}

void SLAudioRecorder::stop()
{
    mIsRecording = false;
}

/**
 * Called after each frame of audio is recorded
 * @param bufferQueue
 * @param pContext
 */
void SLAudioRecorder::recorderCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext)
{
    if (pContext == nullptr) {
        XLOGE("SLAudioRecorder recorderCallback argus is null");
        return;
    }

    SLAudioRecorder *recorder = (SLAudioRecorder *) pContext;

    /*int ret = write(recorder->mRecordedFilePathFd,
                    recorder->mRecordBuffs[recorder->mIndex],
                    recorder->mRecordBufferSize);*/

    int ret = fwrite(recorder->mRecordBuffs[recorder->mIndex], sizeof(short),
                     recorder->mRecordBufferSize, recorder->mFile);

    if (ret < 0) {
        XLOGE("SLAudioRecorder recorderCallback write failed");
        return;
    }

    // If it is currently recording, modify the index and switch another buffer for the next recording
    if (recorder->mIsRecording) {
        recorder->mIndex = 1 - recorder->mIndex;
        (*recorder->mBufferQueue)->Enqueue(recorder->mBufferQueue,
                                           recorder->mRecordBuffs[recorder->mIndex],
                                           recorder->mRecordBufferSize * sizeof(short));
    } else {
        (*recorder->mRecorderInterface)->SetRecordState(recorder->mRecorderInterface,
                                                        SL_RECORDSTATE_STOPPED);
        fclose(recorder->mFile);
    }
}

void SLAudioRecorder::release()
{
    if (mRecorderObj) {
        (*mRecorderInterface)->SetRecordState(mRecorderInterface, SL_RECORDSTATE_STOPPED);
        (*mRecorderObj)->Destroy(mRecorderObj);
        mRecorderObj       = nullptr;
        mRecorderInterface = nullptr;
        mBufferQueue       = nullptr;
    }

    if (mEngineObj) {
        (*mEngineObj)->Destroy(mEngineObj);
        mEngineObj       = nullptr;
        mEngineInterface = nullptr;
    }

    if (mRecordBuffs[0]) {
        delete mRecordBuffs[0];
        mRecordBuffs[0] = nullptr;
    }

    if (mRecordBuffs[1]) {
        delete mRecordBuffs[1];
        mRecordBuffs[1] = nullptr;
    }

    if (mFile) {
        fclose(mFile);
    }

    mIsRecording = false;
    mIndex       = 0;

    XLOGI("audioRecorder stopped");
}


SLAudioRecorder::~SLAudioRecorder()
{
    release();
}

CMakeLists.txt

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)


# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

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).
        src/main/cpp/native-lib.cpp
        src/main/cpp/SLLog.cpp
        src/main/cpp/SLAudioRecorder.cpp
        )

# 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
        android

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})
CMakeLists.txt

JNI调用函数

//
// Created by nisha_chen on 2020/1/20.
//


#include <jni.h>
#include "SLLog.h"
#include "SLAudioRecorder.h"

SLAudioRecorder *audioRecorder = nullptr;

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_yongdaimi_android_dongle_helper_UsbAudioHelper_native_1start_1record(JNIEnv *env,
                                                                            jclass clazz,
                                                                            jstring record_file_save_path)
{
    const char *recordFileSavePath = env->GetStringUTFChars(record_file_save_path, nullptr);
    if (!audioRecorder) {
        audioRecorder = new SLAudioRecorder(recordFileSavePath);
    }

    env->ReleaseStringUTFChars(record_file_save_path, recordFileSavePath);
    return (jboolean) audioRecorder->start();
}


extern "C"
JNIEXPORT void JNICALL
Java_com_yongdaimi_android_dongle_helper_UsbAudioHelper_native_1stop_1record(JNIEnv *env,
                                                                           jclass clazz)
{
    if (audioRecorder) {
        audioRecorder->stop();
        delete audioRecorder;
        audioRecorder = nullptr;
    }
}
native-lib.cpp

 

有几点需要注意:

1. 需要添加: <uses-permission android:name="android.permission.RECORD_AUDIO" />  权限,如果权限未添加,仍然没有办法正常录音;

2. Android 10.0设备的适配问题,Android 10.0 之后,Google要求在sdcard/data/<packageName>目录下创建文件,所以生成的pcm文件建议放在这个目录或公共的Media目录下。

3. CMakeLists.txt文件中需要添加相应的类库:OpenSL ES的类库名字为:OpenSLES

4. OpenSL 使用回调机制来访问异步IO,但它的回调里并不会把音频数据作为参数传递,回调方法仅仅是告诉我们,BufferQueue已准备就绪,可以接收/写入数据了。

 

录音完毕后可以使用 Audacity 这个软件检测生成的PCM文件是否有问题。方法如下:
打开Audacity, 选择:

 

 

选择生成的PCM文件:

 

 

选择合适的采样率和采样位数(比如我在code中定义的录音器配置为:16000hz采样率,单声道,采样位数为16bit):

 

 

点击导入,点最上方的三角形按钮可以选择试听:

 

 

参考链接:

1. OpenSL ES 实现音频的录制与播放

2. Google native-audio

3. 浅聊OpenSL ES音频开发