使用libfvad进行实时录音人声检测(安卓和iOS)

发布时间 2023-05-24 11:15:52作者: rome753

要实现的功能是实时检测人声,检测到之后保存音频数据并上传处理。需要录音比较实时而且能在回调中获取音频数据。

录音方案:

在安卓平台上,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还是要麻烦一些。

  1. 把libfvad代码复制到app/src/main/cpp目录里面
  2. 添加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;
}
  1. 添加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"
    }
}
  1. 写一个帮助类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