OpenSL ES: 利用OpenSL ES实现录音功能
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})
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; } }
有几点需要注意:
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):
点击导入,点最上方的三角形按钮可以选择试听:
参考链接: