C++调用Python和numpy第三方库计算MFCC音频特征实现封装发布

本文主要是介绍C++调用Python和numpy第三方库计算MFCC音频特征实现封装发布,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 项目简介
  • 环境准备
  • 执行步骤
    • 1.新建python虚拟环境
    • 2.虚拟环境运行下python代码
    • 3.迁移虚拟环境
    • 4.编写Cmakelists.txt
    • 5.编写C++代码
    • 6.编译项目
    • 7.测试

项目简介

深度学习程序的边缘部署以性能绝佳的C++为主(⊙﹏⊙),但遇到项目开发周期短,则以功能优先,一些复杂的算法和处理用C++写怕不是得写到天荒地老,于是C++调用python以及第三方库的C端接口这样的方案就应运而生,牺牲一小部分性能,换来功能的完成,连准确性也顺便验证了(注意如果开发人员水平不够(ㄒoㄒ),用C++造轮子的性能还不如python)

本项目首先开发了一个python的类用于预处理wav音频文件来提取MFCC特征,得益于python_speech_features库其实几行代码就能解决,但为了后续的学习借鉴,本次开发较完善点,开发的多个接口对多种数据传递的情况做演示,然后用C++调用这些python接口并取回数据,经测试,每次调用接口会比纯python执行慢不到1毫秒,最终打包后的项目放到无任何开发环境的虚拟机做测试,这其中的波折和踩坑真的只有做过的才懂┭┮﹏┭┮

梅尔频率倒谱系数(MFCC)通过对音频信号的处理和分析,提取出反映语音特征的信息,广泛应用于语音识别、语音合成、说话人识别等领域。可以简单的理解为将一个音频文件转为了矩阵,该矩阵保存了音频特征。

在这里插入图片描述# 程序/数据集下载

点击进入下载地址

本文章只发布于博客园爆米算法,被抄袭后可能排版错乱或下载失效,作者:爆米LiuChen

环境准备

python3.8(虚拟环境或主环境均可)、VS2019(已支持cmake)、什么都没装的win虚拟机(用于测试)
整个项目的文件结构如下

在这里插入图片描述

执行步骤

1.新建python虚拟环境

anaconda的命令是【conda create -n 环境名 python=3.8】,然后pip安装下numpy、scipy,python_speech这几个包

在这里插入图片描述

2.虚拟环境运行下python代码

AudioPreprocess.py代码如下,主要实现了AudioPreprocess这个类,作用是将wav文件先采样成numpy矩阵,然后提取MFCC特征
from python_speech_features import mfcc
import scipy.io.wavfile
from numpy.typing import NDArray
from typing import Tuple
import numpy as npdef yell():print('''Congratulations,you import 【AudioPreprocess】 successfully!!!''')class AudioPreprocess():def __init__(self,numcep:int=13,keepSecs:int=8):'''预处理类:param numcep: MFCC特征数(通道数):param keepSecs: 一个wav文件读取后保留的秒数,不够则补0'''self.numcep = numcepself.keepSecs = keepSecsdef readWave(self,wavePath:str)->Tuple[int,NDArray[np.int16]]:'''读取一个wave文件:param wavePath: wav文件路径:return: 采样率,采样'''samplerate, samples = scipy.io.wavfile.read(wavePath)return samplerate, samplesdef samples2MFCC(self,samplerate:int, samples:NDArray[np.int16])->NDArray[np.float32]:'''一个wav的采样转MFCC特征:param samplerate: 采样率:param samples: 采样:return: MFCC特征 size=(channel,feature)'''samples = samples if len(samples.shape) <= 1 else samples[:, 0]samples = samples[:int(self.keepSecs*samplerate)]samples = np.pad(samples, pad_width=(0, int(samplerate * self.keepSecs) - samples.shape[0]), mode='constant',constant_values=(0, 0))mfccFeature = mfcc(samples, samplerate=samplerate,numcep=self.numcep)mfccFeature = np.transpose(mfccFeature,axes=(1,0))return mfccFeaturedef wave2MFCC(self,wavePath:str)->NDArray[np.float64]:'''wav路径转MFCC:param wavePath: wav文件路径:return: MFCC特征 size=(channel,feature)'''samplerate, samples = self.readWave(wavePath)mfccFeature = self.samples2MFCC(samplerate, samples)return mfccFeatureif __name__ == "__main__":import timepath = "test.wav"audioPreprocess = AudioPreprocess()samplerate, samples = audioPreprocess.readWave(path)t1 = time.time()for i in range(100):mfccFeature = audioPreprocess.wave2MFCC(path)t2 = time.time()print((t2-t1)*1000)

3.迁移虚拟环境

可以将整个虚拟环境都转移到项目中,这样最稳,但文件也最多,我是主要复制了下面几个文件和文件夹,并删除了Lib/site-packages里一些用不到的库,结果还是得要250多M,numpy和scipy这俩库太大了...其实可以尝试一个个的删除,只要留下的文件能支撑你的项目就行,但我这边就懒得这么做了

在这里插入图片描述

4.编写Cmakelists.txt

因为需要调用python解释器,并且也用到了numpy的C接口,所以要额外编写下这俩的配置,需要的文件都在我们的虚拟环境中
cmake_minimum_required (VERSION 3.8)
project ("AudioPrepocess")
SET(CMAKE_BUILD_TYPE "Release")#Debug或Release模式
set(CMAKE_CXX_STANDARD 11)
add_compile_options("$<$<C_COMPILER_ID:MSVC>:/utf-8>")
add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")#项目文件路径配置
set(CMAKE_BINARY_DIR "${CMAKE_SOURCE_DIR}/build")#项目源码构建路径
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/bin")#存放可执行软件的目录;
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/lib")#默认存放项目生成的静态库的文件夹位置;
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/lib")#默认存放项目生成的动态库的文件夹位置;
include_directories(include)#头文件目录
aux_source_directory(source SRC_FILES)#源文件目录的所有文件#调用python的设置
set(PYTHON_DIR "${CMAKE_SOURCE_DIR}/python38/env")
include_directories("${PYTHON_DIR}/include")#头文件目录
link_libraries("${PYTHON_DIR}/libs/python38.lib")
#调用numpy的设置
include_directories("${CMAKE_SOURCE_DIR}/python38/env/Lib/site-packages/numpy/core/include/numpy")#头文件目录
link_libraries("${CMAKE_SOURCE_DIR}/python38/env/Lib/site-packages/numpy/core/lib/npymath.lib")
#移动一些python的依赖
file(COPY "${CMAKE_SOURCE_DIR}/python38" DESTINATION "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
file(RENAME "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/python38/env/python38.dll" "${CMAKE_SOURCE_DIR}/bin/python38.dll")add_executable(${PROJECT_NAME} main.cpp ${SRC_FILES} "source/AudioPreprocess.cpp")#构建可执行文件

5.编写C++代码

include/AudioPreprocess.h如下,声明一个对应python的AudioPreprocess类,成员函数也一致(可以不用这么对应,单纯写个函数去调执行py脚本里的AudioPreprocess类接口就行)反正最后是调用python代码,要不要对应不重要,但这个博客主要是演示的全面一点,注释也写得全一点
#include <chrono>
#include <vector>
#include "Python.h"
#include "arrayobject.h"long long getCurrentTimeMS();//获得当前时间戳 单位毫秒int* initNumpy();//初始化numpy会有返回值 不能直接放在类的构造函数中,所以拿个形式函数包裹下//包裹readWave的返回值
struct ResReadWave {int sampleRate;PyArrayObject* samples;
};//调用python进行音频预处理类 可选择是否标准化数据 但需要传入标准化文件路径 
class AudioPreprocess
{
public:/// @brief 初始化python 初始化模块和导入的python类/// @param scalerPath 标准化文件路径/// @param numcep MFCC特征数(通道数)/// @param keepSecs 一个wav文件读取后保留的秒数,不够则补0AudioPreprocess(int numcep=13, int keepSecs=8);/// @brief 读取wav文件,返回采样率和采样/// @param wavePath ResReadWave readWave(char* wavePath);/// @brief 采样转MFCC特征 返回MFCC特征/// @param samplerates /// @param samples /// @return MFCC特征 二维数组PyArrayObject *samples2MFCC(int samplerates, PyArrayObject* samples);/// @brief 读取wav文件,返回MFCC特征/// @param wavePath /// @return MFCC特征 二维数组PyArrayObject* wave2MFCC(char* wavePath);~AudioPreprocess();private:PyObject* pyModule;PyObject* pyFunc;PyObject* pyArgs;PyObject* pyClass;PyObject* pyClassObj;//python预处理类中对应的函数、参数、返回值PyObject* pyFuncReadWave;PyObject* pyArgsReadWave;PyObject* pyResReadWave;PyObject* pyFuncSamples2MFCC;PyObject* pyArgsSamples2MFCC;PyObject* pyResSamples2MFCC;PyObject* pyFuncWave2MFCC;PyObject* pyArgsWave2MFCC;PyObject* pyResWave2MFCC;int numcep;int keepSecs;
};
source/AudioPreprocess.cpp如下,实现C++和python互传一些基本类型以及numpy这种稍微复杂点的矩阵,注意python初始化的执行顺序,还有最好手动释放那些python对象,还有注意numpy的数据精度类型,不对齐是不会报错的

可以看出C++其实实例化了一个python解释器,然后在解释器里执行python代码,等于在python外套了一层,因此不管怎样都不可能比python还快,这种方式适合需要实现复杂算法且开发时间短的场景,毕竟谁愿意去看MFCC的公式呢...
#include "AudioPreprocess.h"int* initNumpy() {import_array();
}long long getCurrentTimeMS() {auto now = std::chrono::system_clock::now(); // 获取当前时间点auto now_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now); // 转换为毫秒auto epoch = now_ms.time_since_epoch(); // 计算自纪元以来的毫秒数return epoch.count(); // 返回毫秒数
}AudioPreprocess::AudioPreprocess(int numcep, int keepSecs):numcep(numcep), keepSecs(keepSecs){//初始化python解释器Py_SetPythonHome(L"python38/env");Py_Initialize();initNumpy();//初始化numpy,必须紧跟在python解释器初始化后面PyRun_SimpleString("import sys;sys.path.append('./python38')");this->pyModule = PyImport_ImportModule("AudioPreprocess");this->pyFunc = PyObject_GetAttrString(this->pyModule, "yell");//yell这个函数的作用只是确认导入成功 顺便示范下怎么调用python函数PyEval_CallObject(this->pyFunc, nullptr);//实例化python的音频处理类this->pyClass = PyObject_GetAttrString(this->pyModule, "AudioPreprocess");//获取AudioPreprocess这个类this->pyArgs = Py_BuildValue("(i,i)", numcep, keepSecs);this->pyClassObj = PyEval_CallObject(this->pyClass,this->pyArgs);//初始化指针对应python的音频处理类成员函数、参数、返回值this->pyFuncReadWave = PyObject_GetAttrString(this->pyClassObj, "readWave");this->pyArgsReadWave = PyTuple_New(1);this->pyResReadWave = PyTuple_New(2);this->pyFuncSamples2MFCC = PyObject_GetAttrString(this->pyClassObj, "samples2MFCC");this->pyArgsSamples2MFCC = PyTuple_New(2);this->pyResSamples2MFCC = PyTuple_New(1);this->pyFuncWave2MFCC = PyObject_GetAttrString(this->pyClassObj, "wave2MFCC");this->pyArgsWave2MFCC = PyTuple_New(1);this->pyResWave2MFCC = PyTuple_New(1);
}ResReadWave AudioPreprocess::readWave(char* wavePath) {//传入路径PyTuple_SetItem(this->pyArgsReadWave,0,Py_BuildValue("s",wavePath));this->pyResReadWave = PyEval_CallObject(this->pyFuncReadWave, this->pyArgsReadWave);//返回值1 采样率int sampleRate;PyArg_Parse(PyTuple_GetItem(this->pyResReadWave, 0),"i", &sampleRate);//返回值2 采样 numpy int16一维数组PyArrayObject*  samples = (PyArrayObject*)PyArray_FROM_OTF(PyTuple_GetItem(this->pyResReadWave, 1), NPY_INT16, NPY_IN_ARRAY);ResReadWave result = {sampleRate,samples};//打印下值,验证准确性 python输出的值为58npy_intp indices[1] = {0};  // [0]的位置int16_t value = *(int16_t*)PyArray_GetPtr(result.samples, indices);printf("python输出数组[0,0]    :58\nC++&python输出数组[0,0]:%d\n\n",value);return result;
}PyArrayObject* AudioPreprocess::samples2MFCC(int sampleRate, PyArrayObject* samples) {//传入 采样率 采样二维数组PyTuple_SetItem(this->pyArgsSamples2MFCC, 0, Py_BuildValue("i", sampleRate));PyTuple_SetItem(this->pyArgsSamples2MFCC, 1, (PyObject*)samples);this->pyResSamples2MFCC = PyEval_CallObject(this->pyFuncSamples2MFCC, this->pyArgsSamples2MFCC);//返回值 采样二维数组PyArrayObject* mfccFeature = (PyArrayObject*)PyArray_FROM_OTF(this->pyResSamples2MFCC, NPY_FLOAT64, NPY_IN_ARRAY);//打印下值,验证准确性 python输出的值为11.31785676885986npy_intp indices[2] = {0,0};  // [0,0]的位置double_t value = *(double_t*)PyArray_GetPtr(mfccFeature, indices);printf("python输出数组[0,0]    :11.31785676885986\nC++&python输出数组[0,0]:%.14f\n",value);return mfccFeature;
}PyArrayObject* AudioPreprocess::wave2MFCC(char* wavePath) {//传入路径PyTuple_SetItem(this->pyArgsWave2MFCC, 0, Py_BuildValue("s", wavePath));this->pyResWave2MFCC = PyEval_CallObject(this->pyFuncWave2MFCC, this->pyArgsWave2MFCC);//返回值 采样二维数组PyArrayObject* mfccFeature = (PyArrayObject*)PyArray_FROM_OTF(this->pyResWave2MFCC, NPY_FLOAT64, NPY_IN_ARRAY);return mfccFeature;
}AudioPreprocess::~AudioPreprocess() {Py_CLEAR(pyModule);Py_CLEAR(pyFunc);Py_CLEAR(pyArgs);Py_CLEAR(pyClass);Py_CLEAR(pyClassObj);Py_CLEAR(pyFuncReadWave);Py_CLEAR(pyArgsReadWave);Py_CLEAR(pyResReadWave);Py_CLEAR(pyFuncSamples2MFCC);Py_CLEAR(pyArgsSamples2MFCC);Py_CLEAR(pyResSamples2MFCC);Py_CLEAR(pyFuncWave2MFCC);Py_CLEAR(pyArgsWave2MFCC);Py_CLEAR(pyResWave2MFCC);Py_Finalize();
}
main.cpp如下,验证下上文实现的方法,并于python做下对比验证,精度不一致问题是深度学习大忌,还有看看性能损失有多少,顺便做一下多线程实验,python内部的GIL锁会导致C++多线程崩溃,必须手动给python加锁
#include <iostream>
#include "AudioPreprocess.h"
#include <thread>
#include <mutex>AudioPreprocess AP(13, 8);//初始化音频处理类 理论上只需要简单实现wave2MFCC函数,但我对应python的类都实现了,就当练习void wave2MFCC_thread(char* wavePath) {PyGILState_STATE state = PyGILState_Ensure();AP.wave2MFCC("./python38/test.wav");PyGILState_Release(state);}void main() {ResReadWave resReadWave;//存储采样率和采用PyArrayObject* mfccFeature;//存储MFCC特征//resReadWave.samples只能在类内访问 不明原因 可能是因为python解释器在那个类中初始化的,可以想办法在类内转成C++ vector数组再访问resReadWave = AP.readWave("./python38/test.wav");mfccFeature = AP.samples2MFCC(resReadWave.sampleRate, resReadWave.samples);mfccFeature = AP.wave2MFCC("./python38/test.wav");//运行100次,计算时间 ,对比纯python的时间long long t1 = getCurrentTimeMS();for (int i = 1; i <= 100; ++i) {mfccFeature = AP.wave2MFCC("./python38/test.wav");}long long t2 = getCurrentTimeMS();printf("\npython运行100次函数时间    :930 ms\nC++&python运行100次函数时间:%d ms\n",t2-t1);//多线程实验 如果没处理好 C++多线程会使python解释器崩溃printf("\n多线程实验");printf("\n多线程初始化:%d", PyEval_ThreadsInitialized());printf("\n全局解释器锁GIL:%d\n", PyGILState_Check());//PyEval_InitThreads();//开启多线程支持 3.8这个版本已经不需要手动调用这行代码来开启多线程支持Py_BEGIN_ALLOW_THREADS;//暂时释放全局解释器锁GILchar* wavePath = "./python38/test.wav";std::thread t1(wave2MFCC_thread, wavePath);std::thread t2(wave2MFCC_thread, wavePath);t1.join();t2.join();Py_END_ALLOW_THREADS;//重新获取全局解释器锁//Python的对象最好都自己手动销毁Py_CLEAR(resReadWave.samples);Py_CLEAR(mfccFeature);system("pause");
}

6.编译项目

如果有安装上文的文件结构放置,那cmake会将可执行文件和虚拟环境以及测试文件放入bin目录下,并将虚拟环境的python38.dll移动到exe文件同目录,但附件中不会有bin目录,bin是编译后生成的很占空间,如下图

在这里插入图片描述

7.测试

将bin目录扔到虚拟机,模拟一个没有开发环境的客户端,运行exe文件,可以看到运行结果验证和对比,执行100次函数延迟了200ms,算得出做1次调用会比python慢2ms,不过这个可以接受

在这里插入图片描述

这篇关于C++调用Python和numpy第三方库计算MFCC音频特征实现封装发布的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1104160

相关文章

python: 多模块(.py)中全局变量的导入

文章目录 global关键字可变类型和不可变类型数据的内存地址单模块(单个py文件)的全局变量示例总结 多模块(多个py文件)的全局变量from x import x导入全局变量示例 import x导入全局变量示例 总结 global关键字 global 的作用范围是模块(.py)级别: 当你在一个模块(文件)中使用 global 声明变量时,这个变量只在该模块的全局命名空

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

高效+灵活,万博智云全球发布AWS无代理跨云容灾方案!

摘要 近日,万博智云推出了基于AWS的无代理跨云容灾解决方案,并与拉丁美洲,中东,亚洲的合作伙伴面向全球开展了联合发布。这一方案以AWS应用环境为基础,将HyperBDR平台的高效、灵活和成本效益优势与无代理功能相结合,为全球企业带来实现了更便捷、经济的数据保护。 一、全球联合发布 9月2日,万博智云CEO Michael Wong在线上平台发布AWS无代理跨云容灾解决方案的阐述视频,介绍了

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time