前言

硕士毕业一年左右,在AI芯片公司智能座舱部门,工作内容以多模语音SDK开发为主,写这篇博客的目的最主要是对自己过往一年的复盘总结,其次,后面就业方向可能转变为自动驾驶相关,但是不可避免在未来的某些项目中也需要用到语音相关的知识。总结起来,技术栈核心还是C++和cmake工程开发,语音工程开发是对语音识别过程的数据流进行调度,需要对语音基本知识以及基本处理流程需要有充分的了解,其次也需要了解图像处理、linux系统、操作系统、计算机网络等基础知识

AI赋能的两大领域,一个是语音,另一个则是图像,均具有不同方向众多落地场景。相比于利润率高的互联网企业,从营业能力层面来看,国内主营为语音的公司:科大讯飞、思必驰、云知声、出门问问等基本很难有利润,第一梯队的讯飞也要靠一些教育类的产品盈利。反观图像公司,国内最知名的AI四小龙(商汤、旷世、云从、依图)研发投入也巨大,盈利更是遥遥无期。在自动驾驶领域,也是以图像AI为驱动的行业,技术为主,单从研发投入上也是巨大的。怎么说呢,科技兴国,虽然目前这些高科技公司亏损严重,但个人看来,这是不可改变的历史潮流,人类无论如何也是需要这些创新科技的公司来推动整个社会的技术的革命。未来一定是更加智能的,自动驾驶、智能机器人、流畅的人机交互在未来的某一天将会到临。


1. 语音识别流程

语音识别是将mic采集的音频数字信号转换成文本,并进行理解和分析语义的一个过程。整体流程大致可以分为以下几个部分:预处理、VAD端点激活音检测、唤醒识别、ASR语音识别、NLU自然语音理解、NLG自然语音生成、TTS(TextToSpeech)文本到语音。

1.1 概念介绍

  • 采样率,每1秒采样的音频个数或者频率,通常为16k(每秒采样16000次),也有48k,44.1k,采样率越高,获取的信息越多
  • 采样位数(位深),每个采样点的大小,通常为16bit(2字节),也有8bit,32bit
  • 通道数,每次采样的通道数量,通常和mic的数量相同,一般为双声道,即录制的音频是两个通道的,也有单通道,4通道,8通道的音频
  • 音频大小,采样率16k,位深16bit,通道数量为4,则录制1s的音频buffer大小大概是128kb(实际稍微小一点1000 / 1024)
buffer_size = 16000 * 16 / 8 * 4  (字节)
  • 大小端:每个采样点存储的方式,一般为小端模式(低位地址在高位)
  • 音频交织方式,一次中断录制音频的存储方式
// 1 交织方式:  音频不同通道交互着存储
1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4
// 2 非交织方式:  首先读取第一通道的全部数据,然后第2,3,4通道
1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4
  • tinyalsa录音,参考tinyalsa录音实现;除此之外,linux底层是提供了录音的原生驱动alsa(tinyalsa相当于是对内核ALSA的接口一层封装,使用上更加便捷)。
  • wav和pcm区别:相比于pcm保存的全部是原始音频的信息,wav多了一个头部,存储了该段音频的一些属性信息,通常多出44字节,根据录制方式,可能头部大小也有可能存储更多的信息,大于44字节。
    WAV头部信息介绍
  • 音频分析工具:audition
  • 音频二进度读取工具:Binary Viewer 下载

1.2 语音基础

  • 唤醒:预先设定的特定唤醒词,如嗨siri,小爱同学,唤醒之后即可对通过mic对声音进行采集,而后才进行后续的语音识别相关算法检测;所以对唤醒率的要求还是比较高,因为需要在任何场景下,对于指定的唤醒词能够一触即发,同时,随着唤醒率的提高,唤醒的误报(在无声或者没有唤醒词的情况下被唤醒)也可能变高,这是相互矛盾的,但是总归有方法来平衡唤醒率与误报次数之间的关系
  • 全时免唤醒:无需唤醒词,即可对mic采集的音频进行识别,相当于,语音系统后台一直在对音频进行识别
  • 语音激活音(VAD检测):底层算法应该是根据音频的强度信息来检测指定音频的开始时间和结束时间,相当于在这段时间检测出的音频是有声音,其他时间段是无声的;其作用也很明显,经过vad检测截取的音频是更加有效的(音频长度更短,但是更加准确)
  • 语音识别(ASR检测):根据vad检测截取的音频,将二进制的数字化的音频转换成文本的一个过程。asr一般由声学模型,语言模型和解码器三者组成,用更加通俗的话来说,声学模型是将一段音频转换成拼音,语言模型将拼音转换成对应的文字,而且解码器即是结合声学模型和语言模型的结果通过一定的算法来匹配出概率最大的一句完成的话。对于asr来说,其指标就是句准率,即系统检测的这句话和标签的重合度有多少。另外,asr识别分为云端(在线)和端侧(离线),云端的话依赖网络,检测效果肯定更好;端侧的话不依赖网络,直接在本地即完成检测
  • 自然语言处理(NLP):包括自然语言理解(NLU)和自然语音生成(NLG)
  • NLU:将asr检测出的文本转换成机器可以理解的结果,比如说“空调开到23度”,nlu则负责理解其domain(领域)、intent(意图)和一些slots(属性);车机才知道车控相关领域,需要打开空调,打开的大小为23度
  • NLG:对机器说"今天天气怎么样",机器在理解你说的话之后,需要做相应的响应,来生成一句话回复你,如“今天天气多云,温度为23度”
  • TTS:语音合成,机器生成一段语音,并播报出来

1.3 工程经验

做工程开发是比较吃经验的一份工作,所以尤其需要扎实基础;对于应届生来说,在学校的项目中,很难有系统性地学习工程方面的知识,顶多谢谢应用层面的一些代码,不清楚所有模块具体是如果构建出来;所以,做一个项目快速积累经验的话,一定要理解底层原理,对于后期开发大有利处。举个简单的例子:经常使用vector和map,得知道其底层是如何实现的;代码经过编译只有才能运行,得知道机器是如何编译的;cmake是如果管理不同源文件的;使用过内存池、线程池,知道它的优点,那么他们是如何实现的;性能分析有哪些方法,又是如何能优化的;perf工具可以抓取程序运行时的一些堆栈信息,那么它是如何实现的呢;C++11掌握程度如何,那C++14、17、20+呢;操作系统掌握程度如何,是否了解系统底层的一些原理;是否充分了解linux内核,汇编,能够基于底层对C++工程代码进行优化。我觉得,在项目中成长自己,也需要我们额外地学习更多知识反馈于项目之中,这才是对项目的充分理解。同时,在面试过程中,知晓其中的原理,总比调调接口,写写应用层的代码更加重要。而且,面试官更加看重的也是你对项目的理解,对底层原理的理解,以及你的思维能力,沟通能力,适应新团队的能力

此外,普通应届生从事互联网或者车企相关工作,前期以提升技术,扎实基础为根本,投入足够多的时间总是能够做好一个程序员或者一个产品经理的。在一个内卷的时代,技术是我们的终点吗?或许热爱技术的话,可以想想如何成为一个系统架构师;喜欢业务的话,可以多摸索项目中的一些业务经验,如何主导一个产品的实现;善于交际,又懂技术,也可以走上管理之路。普通人的成长之路,应该首先要成为一个大头兵,然后再看机遇一步一步摸索,找到适合自己的方向


2、必备基本技能

2.1 C++11

代码如下(示例):

// 智能指针 #include <memory>
shared_ptr<>
weak_ptr<>
unique_ptr<>
// 

2.2 cmake编译

比较好的cmake学习,CMakeLists.txt学习的一个网站:cmake学习

CMakeLists.txt 常用语法笔记示例:

#指定版本号
cmake_minimum_required(VERSION 2.8) 

#指定项目名  引入两个变量CSDK_BINARY_DIR和CSDK_SOURCE_DIR
PROJECT(CSDK)  

设置变量的值
set()

find_package(OpenCV REQUIRED)寻找OpenCV库的头文件和库文件的指令,找到则可提供OpenCV_INCLUDE_DIRS和OpenCV_LIBRARIES目录

include() 引入cmake代码
include_directories() #用来提供找头文件路径的(全路径)

include_directories(${PROJECT_SOURCE_DIR}/) #库文件搜索以PROJECT_SOURCE_DIR路径为基础
Linux设置包含目录 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_CURRENT_SOURCE_DIR}")  

link_directories(directory1 directory2 ...) #添加需要链接的库文件目录LINK_DIRECTORIES,全路径
Linux设置链接目录 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_CURRENT_SOURCE_DIR}/libs")

add_exectualbe() 生成一个可执行文件
add_library() 生成一个库文件
add_library(common STATIC util.cpp) # 生成静态库
add_library(common SHARED util.cpp) # 生成动态库或共享库
add_subdirectory() 添加一个子目录并构建该子目录
cmake .. 在CMakeList.txt所在的路径下执行命令
message() 打印信息

PROJECT_SOURCE_DIR:工程的根目录PROJECT_BINARY_DIR:运行 cmake 命令的目录,通常是 ${PROJECT_SOURCE_DIR}/build
PROJECT_NAME:返回通过 project 命令定义的项目名称
CMAKE_CURRENT_SOURCE_DIR:当前处理的 CMakeLists.txt 所在的路径
CMAKE_CURRENT_BINARY_DIR:target 编译目录
CMAKE_CURRENT_LIST_DIR:CMakeLists.txt 的完整路径
CMAKE_CURRENT_LIST_LINE:当前所在的行
CMAKE_MODULE_PATH:定义自己的 cmake 模块所在的路径,SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake),然后可以用INCLUDE命令来调用自己的模块
EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置
LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置
//https://blog.csdn.net/afei__/article/details/81201039


# 设置SOURCE_INC变量包含的文件
file(GLOB SOURCE_INC
        "include/hobotalsasdk/*.hpp"
        "include/hobotalsasdk/*.h"
        )

install(TARGETS test DESTINATION bin)  #将test安装到/usr/local/bin目录下
install(FILES filedemo DESTINATION include)  #将filedemo安装到/usr/local/include目录下

在cmake语法中,link_libraries和target_link_libraries是很重要的两个链接库的方式,虽然写法上很相似,但是功能上有很大区别:

1,link_libraries用在add_executable之前,指定查找的路径,该路径下可能包含lib,没有指定target;而target_link_libraries用在add_executable之后,用来链接库,类似GCC中的-L概念,指定了target并且调用链接库(比如将库文件链接到可执行程序中)。
2,link_libraries用来链接静态库,target_link_libraries用来链接导入库,即按照header file + .lib + .dll方式隐式调用动态库的.lib库

set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time") 在指定域中设置一个命名属性;第一个参数决定该属性设置所在的域;必选项PROPERTY后面紧跟着要获取的属性的名字
get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY LINK_DIRECTORIES) 获取一个属性值


在include_directories()后,获取已经添加的目录
get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
foreach(dir ${dirs})
  message(STATUS "dir='${dir}'")
endforeach()
//https://blog.csdn.net/LaineGates/article/details/80324329


message :为用户显示一条消息
message( [STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)
() = 重要消息;
 STATUS = 非重要消息;
 WARNING = CMake 警告, 会继续执行;
 AUTHOR_WARNING = CMake 警告 (dev), 会继续执行;
 SEND_ERROR = CMake 错误, 继续执行,但是会跳过生成的步骤;
 FATAL_ERROR = CMake 错误, 终止所有处理过程;
 
 
 set(CMAKE_BUILD_TYPE Debug) 生成debug库  file libcaboremm.so(not stripped)
 set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-s") 去掉debug库的符号表,变成release库(not stripped)
 
 file libhrsc.so 可以查看是否是debug库,not stripped为debug,stripped为release库

#编译debug库
set(CMAKE_BUILD_TYPE Debug)
cmake .. DCMAKE_BUILD_TYPE=Debug

#在CMakeLists.txt中宏定义OFFLINETEST_JSON变量,需要加-D
add_definitions(-DOFFLINETEST_JSON) 
#OFFLINETEST_JSON可以用作C++代码的宏定义判断,控制代码的开启和关闭
#ifdef(OFFLINETEST_JSON) 或者 #if defined(OFFLINETEST_JSON)

# 查看编译及链接耗时
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time")
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CMAKE_COMMAND} -E time")

2.3 开发工具

2.4 脚本语言

2.4.1 python

2.4.2 shell

三剑客:grep、awk、sed

2.5 代码规范

C++:官方代码规范如下,很有必要学习,虽然看着枯燥,其实也是对自己基础的一种提升,更是大部分公司规范编码格式的强制要求

google代码规范


3、SDK设计原则

SDK(Software Development Kit),软件开发工具包,一般都是一些软件工程师为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件时的开发工具的集合。一些通用的工具或者算法可以通过封装成SDK的形式提供给需求方(包括公司内部或者外部),通常会提供工具的设计文档,动态库或者静态库以及对应的头文件,示例代码给需求方进行集成

3.1. 设计流程

需求分析、方案评估、方案设计、文档编写、接口设计、代码编写。

3.2 设计原则


4、性能优化分析

4.1 cpu占用

4.1.1 线程池

  1. 线程池原理
  • 管理一个任务队列queue,一个线程池vector,然后每次取一个任务分配给一个线程去做,循环往复;
  •   应用:初始化创建PRE_SIZE 20个线程,塞入free_list闲余线程链表;每个线程对应同一个任务的队列queue(大小为60),任务数量大于60时,清空该任务队列;每增加一个任务,使用一个闲余的free_list线程,并增加一个used_list线程,并将线程塞入线程链表的首部;闲余线程free_list为0,即线程数量大于20时(最大数量不能超过MAX_SIZE 50),额外再创建一个线程,线程的生存时间为600s,并对线程链表的尾部进行判断(尾部线程创建的时间超过600s,该线程加入free_list,同时used_list使用的线程减1) 
    
  • 任务队列是典型的生产者-消费者模型,通常实现为一个 mutex + 一个条件变量,或是一个 mutex + 一个信号量。mutex 实际上就是锁,保证任务的添加和移除(获取)的互斥性,一个条件变量是保证获取 task 的同步性(条件阻塞),一个 empty 的队列,线程应该等待(阻塞);
  • 设置线程名,线程优先级,绑定线程到指定CPU核数
  1. 线程池实现
    C++11实现线程池

4.1.2 cpu绑核

  1. cpu亲和性
  2. 将指定的线程绑定的特定的cpu核数上,则可以保证该核cpu优先处理这个线程的任务,避免该线程由系统决定在不同cpu核上跳动处理;如果对于实时率要求较高的线程,我们怕该线程在处理的过程中在特定时间节点上抢占不到cpu进行处理,从而影响实时率,则可以将该线程绑定到特定的cpu核心上

4.1.3 cpu优化

  1. cpu占用测试方法,linux系统下使用top实时查看cpu占用情况,ps -ef,ps -aux可以查看静态的cpu占用情况
  2. 如果需要更加系统的测试cpu占用情况,可以使用一些辅助工具,如perf;当然如果简单的使用该工具,不知其所以然,还是不够;所以,可以分析perf检测cpu占用的底层原理是什么
  3. 降低cpu占用的常用方法

4.2 内存占用

4.2.1 内存池

  1. 内存池原理
  2. 内存池实现

4.2.2 内存优化

  1. 内存占用测试
  2. 内存占用降低方法

4.3 延迟统计

  1. 延迟测试方法

4.3.1 实时率

一段1s的音频,工程及算法合计处理时间为0.3s,即可得到结果,则语音识别的总体实时率为0.3

4.3.2 延迟优化

4.3.3 帧率优化

4.4 音频诊断

4.4.1 强度诊断

读取录制的音频实质上一组二进制的数字,幅度范围在00~FF之间,判断录制的一端音频是否有效,可以判断该断音频中幅度为00或者FF的个数占总音频总长度的百分比是否超过一定阈值,如果基本全为00,该音频基本是静音的,如果基本全为FF,这段音频也是无效的杂音

4.4.2 丢帧率检测

  1. 通过记录音频录制开始时间和结束时间,就知道理论上应该录制音频的内存大小,然后和实际读取或者保存的音频内存大小比较,即可知道丢帧的大小,当然该方法要求记录的开始和结束时间足够准确
  2. 我们假设每一帧音频是绑定时间戳的,每一帧为16ms,这样的话就可以通过判断每两帧音频之间的时间戳是否相差16ms来判断是否丢帧

5. 总结

All in AI

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐