使用libfvad进行实时录音人声检测(安卓和iOS)
要实现的功能是实时检测人声,检测到之后保存音频数据并上传处理。需要录音比较实时而且能在回调中获取音频数据。
录音方案:
在安卓平台上,AudioRecord是一种用于录制音频数据的API。它可以以流的形式将音频数据读取到应用程序中,并支持实时监测音频输入。它可以用于录制高质量的音频,同时也可以进行实时音频处理。
在iOS平台上,AudioQueue也是一种用于录制音频数据的API。它提供了一种低延迟的音频录制方式,并支持实时监测音频输入。它可以用于录制高质量的音频,同时也可以进行实时音频处理。
人声检测方案:
https://github.com/dpirch/libfvad
libfvad是一款开源的语音活动检测库,它使用谷歌开发的FVAD(Frame-based Voice Activity Detector)算法来检测语音信号中的活动和非活动部分。该库可用于嵌入式设备和服务器等多种场景下,支持多种采样率(例如8kHz、16kHz、32kHz和48kHz),并且在保持高准确性的同时,具有较低的计算复杂度和内存占用。
通过使用libfvad,开发人员可以方便地将语音活动检测功能集成到自己的应用程序中,以实现更精确的语音识别、语音合成、语音过滤等功能。在实际应用中,libfvad可以与其他开源库和工具(如PortAudio、PulseAudio、FFmpeg等)结合使用,以实现更丰富的音频处理功能。
libfvad的使用相对简单,它提供了易于使用的C API,并且附带了丰富的示例代码和文档。同时,由于它是基于开源的FVAD算法开发的,因此也具有较好的可定制性和可扩展性,开发人员可以根据自己的需要对其进行定制和扩展。
总的来说,libfvad是一款功能强大、易于使用、高度可定制的语音活动检测库,适用于各种语音处理场景。如果您需要在自己的应用程序中集成语音活动检测功能,那么libfvad是一个不错的选择。
iOS
1. 1 AudioQueue录音
https://github.com/msching/MCAudioInputQueue
找了一下,这个库可以直接使用,它简单封装了AudioQueue,便于使用。它是OC的,Swift没有找到能用的代码。OC虽然写法繁琐一点,但是操作底层数据和调用c/cpp代码更有优势。
录音代码如下
#pragma mark - record
- (void)_startRecord
{
if (_started)
{
return;
}
// [_player stop];
_started = YES;
// 不设置这个可能无法开始录音
// https://developer.apple.com/documentation/avfaudio/avaudiosession/errorcode/cannotstartrecording
NSError *error = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
NSLog(@"Error setting audio session category: %@", error);
return;
}
// _data = [NSMutableData data];
_recorder = [MCAudioInputQueue inputQueueWithFormat:_format bufferDuration:bufferDuration delegate:self];
_recorder.meteringEnabled = YES;
[_recorder start];
self.logCallback(@"开始录音");
}
- (void)_stopRecord
{
if (!_started)
{
return;
}
_started = NO;
[_recorder stop];
_recorder = nil;
vadData = nil;
tvad0 = 0;
tvad1 = 0;
self.logCallback(@"停止录音");
}
1.2 libfvad编译
libfvad是一个c代码库,编译成iOS可用的有两种方式:在电脑上交叉编译生成iOS可用的链接库,或者把代码放到Xcode工程里面直接编译,我这里选择了后者。因为Xcode对c的编译非常友好,友好到什么程度呢?就是libfvad的代码基本上不用做什么修改,就能直接编译调用了,并且运行时能直接对c代码断点调试。
Xcode对c的编译友好应该是来自于OC对c的兼容性比较好,这可能是大部分公司都不愿意切换Swift的原因。
调用fvad检测人声代码如下,直接用fvad_process函数运行就行。返回值1代表检测到人声,0代表没检测到,-1代表检测有问题。一开始一直返回-1,调试了一下,发现它对输入音频包大小是有要求的,调整bufferDuration也就是录制缓冲间隔为0.02秒后,音频包大小为320,这样就能满足要求,检测到人声。
#pragma mark - inputqueue delegate
- (void)inputQueue:(MCAudioInputQueue *)inputQueue inputData:(NSData *)data numberOfPackets:(UInt32)numberOfPackets
{
if (data)
{
// 假设你已经定义了一个名为audioData的NSData对象,其中包含了录制的音频数据。
// 将NSData对象转换为指向音频数据的指针。
const int16_t *audioBuffer = (const int16_t *)[data bytes];
// 计算音频数据的长度(以采样点为单位)。
size_t audioLength = [data length] / sizeof(int16_t);
int b = fvad_process(fvadInst, audioBuffer, audioLength);
}
}
2 安卓
2.1 AudioRecord录音
录音代码如下
class VoiceRecorder {
private val SAMPLE_RATE = 16000
private val FRAME_SIZE = 320
private var FRAME_DURATION = SAMPLE_RATE / FRAME_SIZE // 50ms
private var audioRecorder: AudioRecord? = null
private var fvad: FvadWrapper? = null
private var isRecording = false
init {
fvad = FvadWrapper()
fvad?.setMode(0)
fvad?.setSampleRate(SAMPLE_RATE)
}
fun startRecording() {
val minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
if (ActivityCompat.checkSelfPermission(MainApplication.getInstance(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
// grantResults: IntArray)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return
}
audioRecorder = AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize)
audioRecorder?.startRecording()
isRecording = true
Thread(Runnable {
val all = ShortArray(SAMPLE_RATE * 1000) // 1000s
var allLen = 0
val audioBuffer = ShortArray(FRAME_SIZE)
while (isRecording) {
val bytesRead = audioRecorder?.read(audioBuffer, 0, FRAME_SIZE) ?: 0
if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION || bytesRead == AudioRecord.ERROR_BAD_VALUE) {
return@Runnable
}
val result = fvad?.process(audioBuffer) ?: -1
}).start()
}
fun stopRecording() {
isRecording = false
audioRecorder?.stop()
audioRecorder?.release()
fvad?.destroy()
}
}
2.2 libfvad编译
编译同样有两种方式:在电脑上交叉编译,或者在Android Studio中用ndk进行编译。我选择了后者,它相对iOS还是要麻烦一些。
- 把libfvad代码复制到app/src/main/cpp目录里面
- 添加jni接口文件,它是java和c之间的桥梁,它调用c,Java调它。
jni里面的函数有特定的格式,写起来很繁琐。我把fvad.c扔给GPT,让他直接给我生成了jni函数
#include <jni.h>
#include "../include/fvad.h"
JNIEXPORT jlong JNICALL Java_com_example_FvadWrapper_createFvad(JNIEnv *env, jobject obj) {
Fvad *inst = fvad_new();
return (jlong) inst;
}
JNIEXPORT void JNICALL Java_com_example_FvadWrapper_destroyFvad(JNIEnv *env, jobject obj, jlong handle) {
Fvad *inst = (Fvad*) handle;
fvad_free(inst);
}
JNIEXPORT void JNICALL Java_com_example_FvadWrapper_reset(JNIEnv *env, jobject obj, jlong handle) {
Fvad *inst = (Fvad*)handle;
fvad_reset(inst);
}
JNIEXPORT jint JNICALL Java_com_example_FvadWrapper_setMode(JNIEnv *env, jobject obj, jlong handle, jint mode) {
Fvad *inst = (Fvad*) handle;
return fvad_set_mode(inst, mode);
}
JNIEXPORT jint JNICALL Java_com_example_FvadWrapper_setSampleRate(JNIEnv *env, jobject obj, jlong handle, jint sampleRate) {
Fvad *inst = (Fvad*) handle;
return fvad_set_sample_rate(inst, sampleRate);
}
JNIEXPORT jint JNICALL Java_com_example_FvadWrapper_process(JNIEnv *env, jobject obj, jlong handle, jshortArray frame, jint length) {
Fvad *inst = (Fvad*) handle;
const jshort *frameData = (*env)->GetShortArrayElements(env, frame, NULL);
int result = fvad_process(inst, frameData, length);
(*env)->ReleaseShortArrayElements(env, frame, frameData, JNI_ABORT);
return result;
}
- 添加CMakeLists.txt文件。ndk编译有Android.mk和cmake两种方式。它们差别不大,都是告诉ndk有哪些源码文件,编译生成什么abi文件,cmake更新更简洁一点。
CMakeLists.txt文件如下,就是把全部源码和fvad_jni.c文件加上,生成libfvad.so文件
cmake_minimum_required(VERSION 3.5)
set(CMAKE_ANDROID_ARCH_ABI "armeabi-v7a arm64-v8a")
set(SOURCES common.h
fvad_jni.c
fvad.c
signal_processing/division_operations.c
signal_processing/energy.c
signal_processing/get_scaling_square.c
signal_processing/resample_48khz.c
signal_processing/resample_by_2_internal.h
signal_processing/resample_by_2_internal.c
signal_processing/resample_fractional.c
signal_processing/signal_processing_library.h
signal_processing/spl_inl.h
signal_processing/spl_inl.c
vad/vad_core.h
vad/vad_core.c
vad/vad_filterbank.h
vad/vad_filterbank.c
vad/vad_gmm.h
vad/vad_gmm.c
vad/vad_sp.h
vad/vad_sp.c)
#add_library(fvad STATIC ${SOURCES})
add_library(fvad SHARED ${SOURCES})
set_property(TARGET fvad PROPERTY POSITION_INDEPENDENT_CODE 1)
install(TARGETS fvad DESTINATION lib)
在build.gradle里还要加上
ndk {
abiFilters "arm64-v8a", "armeabi-v7a"
}
externalNativeBuild {
cmake {
path "src/main/cpp/libfvad/src/CMakeLists.txt"
}
}
- 写一个帮助类FvadWrapper用来加载so库和调用jni方法
public class FvadWrapper {
static {
System.loadLibrary("fvad");
}
private long handle;
public FvadWrapper() {
handle = createFvad();
}
public void destroy() {
destroyFvad(handle);
handle = 0;
}
public void reset() {
reset(handle);
}
public int setMode(int mode) {
return setMode(handle, mode);
}
public int setSampleRate(int sampleRate) {
return setSampleRate(handle, sampleRate);
}
public int process(short[] frame) {
return process(handle, frame, frame.length);
}
private static native long createFvad();
private static native void destroyFvad(long handle);
private static native void reset(long handle);
private static native int setMode(long handle, int mode);
private static native int setSampleRate(long handle, int sampleRate);
private static native int process(long handle, short[] frame, int length);
}
初始化和调用就很简单了
init {
fvad = FvadWrapper()
fvad?.setMode(0)
fvad?.setSampleRate(SAMPLE_RATE)
}
val result = fvad?.process(audioBuffer) ?: -1