funasr语音识别docker部署——并基于qt开发_funasr websocket
开发要求
a. 在ubuntu安装布署语音识别框架Funasr;
b. 编写接口测试代码,熟悉开发流程及函数
c. 开发基于QT的最终测试案例
开发效果:实现出现一个ui界面,当点下按钮后,开始录制语音,松开后开始识别并在界面上显示语音识别结果。
1. 在ubuntu安装布署语音识别框架Funasr
Funasr介绍:
Funasr是阿里达摩院的开源大型端到端(语音信号直接映射到文本技术)语音识别工具包,提供了在大规模工业语料库上训练的模型,并能够将其部署到应用程序中。
工具包的核心模型是Paraformer,结合了语音端点检测、语音识别、标点等模型,为构建高精度的长音频语音识别服务提供了坚实的基础。
与在公开数据集上训练的其它模型相比,Paraformer展现出了更卓越的性能, FunASR 的中文语音转写效果比openai的开源框架 Whisper 更优秀,因此本项目考虑使用FunASR。
1.1在Ubuntu2204中安装docker
Docker介绍:Docker 是一个开源的应用容器,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上。容器是完全使用沙箱机制,相互之间不会有任何接口,更重要的是容器性能开销极低。
安装原因: 阿里开发团队在Docker上上传了Funasr镜像,Funasr框架在Docker容器上下载运行更为方便高效。
1.1.1更新Ubuntu的镜像源
sudo apt updatesudo apt upgrade
1.1.2安装必要的证书并允许apt包管理器使用以下命令通过https使用存储库。
sudo apt install apt-transport-https ca-certificates curl software-properties-common gnupg lsb-release
1.1.3添加Docker的官方GPG密钥、添加Docker官方库、更新Ubuntu源列表
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpgsudo echo \"deb[arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullsudo apt update
1.1.4
安装Docker
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
1.1.5启动后查看状态、设为开机自启动
sudo systemctl start dockersystemctl status dockersudo systemctl enable docker
1.2 funasr镜像和容器下载
1.2.1
在
Docker
获取镜像
---------------------------------------------------------------------------------------------------------------------------------
- Docker 镜像(Images):是用于创建 Docker 容器的模板,比如 Ubuntu 系统。
- Docker 容器(Container):是独立运行的一个或一组应用,是镜像运行时的实体。
镜像和容器的关系就像从c++中类和对象的关系,要想使用镜像就要先创建一个对应的容器
Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。
Docker Hub(https://hub.docker.com) 提供了庞大的镜像集合供使用。
一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 : 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
---------------------------------------------------------------------------------------------------------------------------------
下载funasr : funasr-runtime-sdk-online-cpu-0.1.12(: )的镜像:
sudo docker pull \\registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12mkdir -p ./funasr-runtime-resources/models
列出镜像列表
docker images
:列出本地主机上的镜像
TAG
:镜像的标签
IMAGE ID
:镜像ID
CREATED
:镜像创建时间
SIZE
:镜像大小
1.2.2根据镜像启动一个容器
一定要加-p 10096:10095 ,否则容器启动后容器外无法访问容器内服务,容器内外是相当于两个不同系统,两边端口号没有任何关联,要通过启动时-p 10096:10095映射
-i:
交互式操作。-t: 终端。
sudo docker run -p 10096:10095 -it --privileged=true \\ -v $PWD/funasr-runtime-resources/models:/workspace/models \\ registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
执行以上命令后该终端会进入容器内部,因为启动时加入参数-it,进行终端交互
打开另一个终端
查看所有的容器命令如下:
docker ps -a
(在容器外的终端输入)
1.2.3
容器内启动和关闭funasr服务端(以下部分在容器内终端操作)
进入该目录
cd FunASR/runtime
执行以下命令:
bash run_server.sh \\ --certfile 0 \\ --download-model-dir /workspace/models \\ --vad-dir damo/speech_fsmn_vad_zh-cn-8k-common-onnx \\ --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-8k-common-vocab8404-onnx \\ --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-8k-common-vocab8404-online-onnx \\ --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx \\ --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst \\ --itn-dir thuduj12/fst_itn_zh
命令参数说明:
- # 如果您想关闭ssl,增加参数:--certfile 0
- # 如果您想使用SenseVoiceSmall模型、时间戳、nn热词模型进行部署,请设置--model-dir为对应模型:iic/SenseVoiceSmall-onnx、 damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx(时间戳)、 damo/speech_paraformer-large-contextual_asr_nat-zh-cn-16k-common-vocab8404-onnx(nn热词)
- # 如果您想在服务端加载热词,请在宿主机文件./funasr-runtime-resources/models/hotwords.txt配置热词(docker映射地址为/workspace/models/hotwords.txt):
- # 每行一个热词,格式(热词 权重):阿里巴巴 20(注:热词理论上无限制,但为了兼顾性能和效果,建议热词长度不超过10,个数不超过1k,权重1~100)
- # SenseVoiceSmall-onnx识别结果中“ ”分别为对应的语种、情感、事件信息
如果该容器的服务已经运行过上面的命令一次了,也就是曾经启动过该服务,已经完成模型下载,可以直接运行下面命令启动:
bash run_server.sh update --ssl 0
当出现监听端口时即成功部署funasr服务端
1.2.4
关闭服务端
查看 funasr-wss-server 对应的PID
ps -x | grep funasr-wss-server-2passkill -9 PID
关闭容器命令:exit
(容器只是退出关闭,还能再启动)
1.2.5 docker
容器的其他基本操作
停止容器
停止容器的命令如下(在容器外停止):
$ docker stop
停止的容器可以通过 docker restart 重启:
$ docker restart
进入容器
容器再启动后会进入后台。此时想要进入容器,可以通过以下指令进入:
$ docker attach $ docker exec
推荐使用 docker exec 命令,因为此命令会退出容器终端,但不会导致容器的停止。
导出容器
如果要导出本地某个容器,可以使用 docker export 命令。
$ docker export 1e560fca3906 > ubuntu.tar
导出容器 1e560fca3906 快照到本地文件 ubuntu.tar。
这样将导出容器快照到本地文件。
导入容器快照
可以使用 docker import 从容器快照文件中再导入为镜像,以下实例将快照文件 ubuntu.tar 导入到镜像 test/ubuntu : v1(: ):
$ cat docker/ubuntu.tar | docker import - test/ubuntu:v1
删除容器
删除容器使用 docker rm 命令:
$ docker rm -f 1e560fca3906
下面的命令可以清理掉所有处于终止状态的容器。
$ docker container prune
删除镜像
删除镜像使用 docker rmi 命令:
$ docker rmi
2. 编写接口测试代码,熟悉开发流程及函数
2.1接口测试
执行一下命令下载解压官方客户端案例
wget https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/sample/funasr_samples.tar.gztar -xvf funasr_samples.tar.gz
在服务器已经运行的情况下,进入samples/cpp目录,执行以下语句
./funasr-wss-client --server-ip 127.0.0.1 --port 10096 --wav-path ../audio/asr_example.wav
可以看到以下结果则已经完成接口测试
ubuntu2204
本身不带libssl库,会报错,可以用一下命令下载(该网址库有时效性,过期了会下载失败):
wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1-1ubuntu2.1~18.04.23_amd64.debsudo dpkg -i libssl1.1_1.1.1-1ubuntu2.1~18.04.23_amd64.deb
2.2
解析官方案例代码
接下来对funasr-wss-client.cpp代码进行分析:
由主函数看代码可以发现:
TCLAP: 用于命令行参数解析。
websocketpp: 用于WebSocket通信。
-------------------------------------------------------------------------------
TCLAP通过ValueArg定义多个参数:
server_ip: 服务器IP地址(必需,默认值为127.0.0.1)。
port: 服务器端口(必需,默认值为10095)。
wav_path: 输入音频文件路径,可以是WAV文件、PCM文件或Kaldi风格的WAV列表。
audio_fs: 音频采样率(可选,默认值为16000)。
thread_num: 使用的线程数(可选,默认值为1)。
is_ssl: 是否使用SSL(可选,默认值为1,表示使用WSS)。
use_itn: 是否使用ITN(可选,默认值为1)。
hotword: 热词文件路径(可选)。
所有参数都通过cmd.add()方法添加到命令行解析器中,并通过cmd.parse(argc, argv)解析。
由于在qt开发客户端,可以自己设定参数,所以不采用TCLAP。
接下来重点分析websocket通信。
-------------------------------------------------------------------------------
2.2.1 WebSocket 协议
WebSocket 协议是一种基于TCP的网络层协议,用于在客户端和服务器之间建立持久连接,并且可以在这个连接上实时地交换数据。WebSocket协议有自己的握手协议,用于建立连接,也有自己的数据传输格式。与http关系如下:
区别:WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,HTTP是单向的。WebSocket在建立握手时,数据是通过HTTP传输的,但是建立之后,在真正传输时候是不需要HTTP协议的。
2.2.2 WebsocketClient
类
在funasr-wss-client.cpp中,定义了一个专门的类class WebsocketClient
在主函数中,主要的数据传输代码为以下
if (is_ssl == 1){ WebsocketClient c(is_ssl); c.m_client.set_tls_init_handler(bind(&OnTlsInit, ::_1)); c.run(uri, wav_list, wav_ids, audio_fs, hws_map, use_itn);} else { WebsocketClient c(is_ssl); c.run(uri, wav_list, wav_ids, audio_fs, hws_map, use_itn);}
可以看到主要是实例化对象
c
,然后进行连接和通信,而开启
ssl
模式则在连接前进行
set_tls_init_handler(bind(&OnTlsInit, ::_1))
的初始化,由于
qt
自带
ssl
通信证书类,所以不进行分析。
分析
WebsocketClient
的
run
函数:
由m_client.get_connection(uri, ec);进行连接,send_wav_data(wav_list[i], wav_ids[i], audio_fs, hws_map, send_hotword, use_itn);进行数据的发送。
2.2.3 WebsocketClient
类的send_wav_data函数
经分析,客户端对于
json
数据的发送包括音频的解析传输都在
send_wav_data
函数中。
函数
void send_wav_data(string wav_path, string wav_id, int audio_fs,
const std::unordered_map& hws_map,
bool send_hotword, bool use_itn)
参数如下:
- string wav_path:音频文件的路径。
- string wav_id:音频文件的唯一标识符。
- int audio_fs:音频文件的采样率。
- const std::unordered_map& hws_map:热词映射表,包含热词及其对应的权重。
- bool send_hotword:是否发送热词信息。
- bool use_itn:是否使用说话人识别技术(ITN)。
函数内容分析:
一、初始化:
使用 funasr::Audio 类创建一个音频对象,并设置采样率为 audio_fs。
根据 wav_path 的文件扩展名,确定音频文件的格式(PCM或其他)。
如果是PCM文件,使用 LoadPcmwav 方法加载PCM格式的WAV文件;
如果不是PCM文件,使用 LoadOthers2Char 方法加载其他格式的音频文件。
PCM
:
PCM(脉冲编码调制,Pulse Code Modulation)是一种用于数字音频信号的编码方式,通常用于将模拟音频信号转换为数字信号。
音频编码:在PCM中,模拟音频信号被定期采样,每个样本的幅度被量化并编码为数字值。这些数字值表示特定时间点上的音频幅度。
采样率:PCM的一个重要参数是采样率,表示每秒钟采样多少次音频信号。常用的采样率包括44.1 kHz(CD音质)和48 kHz(视频音频标准)。更高的采样率可以捕捉更多的声音细节,但也会生成更大的文件。
二、发送音频数据前的准备:
等待WebSocket连接打开,如果连接已关闭,则停止发送数据。
构建JSON对象 jsonbegin,包含音频分块大小、间隔、文件名、格式、采样率和ITN设置。
如果 use_itn 为 false,则在JSON对象中设置ITN为 false。
如果 send_hotword 为 true 且 hws_map 不为空,则将热词信息添加到JSON对象中。
三、数据发送:(案例使用 nlohmann::json 库来构建和发送JSON数据。)
- 发送JSON开始信息:使用 m_client.send 方法发送构建好的JSON对象 jsonbegin 到服务器。
- 发送音频数据:如果音频格式是PCM,循环获取音频数据,并将浮点数转换为短整型,然后分块发送;如果音频格式不是PCM,直接发送整个音频数据。在发送每个数据块后,打印已发送数据的长度。
3.发送JSON结束信息:构建一个JSON对象 jsonresult,表示音频数据发送完成,并发送到服务器。
3.开发基于QT的最终测试案例
3.1 qt的websocket连接
3.1.1 库准备
在qt中,有qwebsocket库专门实现websocket连接的方法,在终端下载qwebsocket
sudo apt-get install libqt5websockets5-dev
然后在qt工程的.pro文件添加如下:
QT += websocketsQT += network
3.1.2 实现qt客户端的简单连接
在qt构建实现websocket的类
class WebsocketClient : public QObject{ Q_OBJECTpublic: explicit WebsocketClient(int is_ssl, QObject *parent = nullptr); ~WebsocketClient(); void connectToServer(const QUrl& url); void sendWavFile(const QString &filePath); void send_JsonData (const QString &wavId, int audioFs, bool useItn, const QJsonObject &hotwords); void send_jsonresult(); QWebSocket m_client; //主要的QWebSocket对象 QString text;signals: void messageReceived(const QString &message); void connected(); void disconnected();private slots: void onConnected(); void onDisconnected(); void onTextMessageReceived(const QString &message); void onSslErrors(const QList& errors);private: bool m_is_ssl; bool connect_stutas;};
其中构造函数主要为进行信号与槽的绑定:
connect(&m_client, &QWebSocket::disconnected, this, &WebsocketClient::onDisconnected); connect(&m_client, &QWebSocket::textMessageReceived, this, &WebsocketClient::onTextMessageReceived);connect(&m_client, &QWebSocket::sslErrors, this, &WebsocketClient::onSslErrors); // 处理 SSL 错误
利用QWebSocket对象连接服务器:
m_client.open(url);
然后发现本地上的服务器并不能接受ssl关闭的连接,即使服务器已经关闭了ssl服务还是会有以下报错:
报错内容提示ssl错误。
搜集资料,多次尝试,发现qt上websocket有自带的ssl证书
结果成功:
此时客户端已经实现初步连接,等待发送数据。
3.2 qt的json构建
在事例代码中,运用了nlohmann::json 库来构建json头和尾,而在qt中,有相应的json数据处理函数,要使用如下头文件:
#include #include #include
对于json头的构建,参照事例:
以下为json头构建发送函数,传入四个参数,分别为wav文件名、音频的采样频率、是否使用ITN、包含热词映射的字符串。
另外由于我的工程默认为pcm 格式,所以默认。\"is_speaking\"为告诉服务器是否传数据的参数,\"chunk_size\"可能代表了音频数据在发送给服务器时被分成的块的大小,这里照抄事例格式。
void WebsocketClient::send_JsonData(const QString &wavId, int audioFs, bool useItn, const QJsonObject &hotwords){ QJsonObject jsonBegin; QJsonArray chunkSize; chunkSize.append(5); chunkSize.append(10); chunkSize.append(5); jsonBegin[\"chunk_size\"] = chunkSize; jsonBegin[\"chunk_interval\"] = 10; jsonBegin[\"wav_name\"] = wavId; jsonBegin[\"wav_format\"] = \"pcm\"; jsonBegin[\"audio_fs\"] = audioFs; jsonBegin[\"itn\"] = useItn; jsonBegin[\"is_speaking\"] = true; if (!hotwords.isEmpty()) { jsonBegin[\"hotwords\"] = hotwords; }//sendTextMessage 函数需要一个字符串作为参数doc QJsonDocument doc(jsonBegin) ;//通过调用 doc.toJson(QJsonDocument::Compact),得到一个紧凑格式的 JSON 字 符串,便于传输。 QString jsonString = doc.toJson(QJsonDocument::Compact); if (connect_stutas) { m_client.sendTextMessage(jsonString); //m_client是类的QWebSocket成员 } else { qDebug() << \"WebSocket is not connect!\"; } return;}
同理,json尾根据事例构建发送如下:
void WebsocketClient::send_jsonresult(){ QJsonObject jsonresult; jsonresult[\"is_speaking\"] = false; QJsonDocument doc(jsonresult); QString jsonString = doc.toJson(QJsonDocument::Compact); if (connect_stutas) { m_client.sendTextMessage(jsonString); } else { qDebug() << \"WebSocket is not connect!\"; } return;}
3.3 音频的处理和发送
3.3.1 音频下载
在示例代码中,对于音频的处理方法主要集成在了audio.cpp和resample.cpp中。
在qt工程当中我打算直接调用两个文件的api接口对音频进行处理。
此处还要对官方的示例代码进行详细分析:
对于案例中音频,在确定了音频文件的路径和文件名后,是先对文件进行加载,然后再根据设定的采样率,位深等参数进行采样分割,最后才会发送给服务端。
具体的加载文件操作如下:
在确认了音频文件是PCM编码后,主要用Audio::LoadPcmwav函数进行文件的加载,
该函数的传参分别为:音频文件名、采样率、是否进行重采样
在Audio::LoadPcmwav函数中,有
void WebsocketClient::send_jsonresult(){ QJsonObject jsonresult; jsonresult[\"is_speaking\"] = false; QJsonDocument doc(jsonresult); QString jsonString = doc.toJson(QJsonDocument::Compact); if (connect_stutas) { m_client.sendTextMessage(jsonString); } else { qDebug() << \"WebSocket is not connect!\"; } return;}
这里就是从文件中读取音频数据,并将其转换为浮点数格式存入Audio对象的speech_data成员中,随后有:
if (resample && *sampling_rate != 16000) { WavResample(*sampling_rate, speech_data, speech_len);}
这里如果传参为重采样同时采样率不是16000,会进行重采样,以满足服务器的需要。后续为了提高效率以及尽量不失真,我的录音参数采样率和客户端处理采样率都是16000
最后,利用frame_queue.push(frame),将处理后的数据存入音频帧队列,以便后续处理。
以上为音频的下载,案例中还用到了audio.Fetch函数
Fetch 函数检查 frame_queue 是否有 AudioFrame 对象。如果有,它取出队列前面的 AudioFrame 对象,更新 dout 指向该帧的开始,len 设置为帧的长度,并删除该 AudioFrame 对象,然后返回 1 并设置 flag 为 S_END。
如果 frame_queue 为空,它直接返回 0。
掌握了以上两个函数,我模仿官方案例完成了基于qt的音频处理和发送,以下是我的代码:
这里buff拿下来的数据并不能直接发送给服务器,要将 float 数组转换为 short 数组,通过乘以 32768 来进行缩放,最后将short 数组分块发送到服务器,每块大小为 102400 字节。
查阅资料后明白将 float 数据乘以一个缩放因子,以将其映射到 short 数据的范围,是对于16位PCM的处理,这个缩放因子是 32768(即 2^15),而对应的 short 数据的范围是 -32768 到 32767。
连接上服务器后,发送json头再发送音频,最后再加上json尾。此时已经能实现现有音频的识别。
3.4回复数据的处理
以下为服务器对于长音频识别后返回的结果:
分析服务器回复的json可以看出最后识别出的结果都放在了text下,所以在qt中直接直接取json中的text字段作为识别结果就可以了
3.5实现录音并且生成wav文件
由上面的开发我实现了对现有音频的识别,但是qt开发的客户端仅实现了对默认格式的处理,并不能如同官方案例一样对各种文件都能实现识别,我的客户端对音频作出了以下限定:pcm编码的wav文件,单音道,采样率为16000,位深为16。
录音功能我采取了利用ubuntu内的alsa库编程实现录音并存为音频文件,qt内实现调用并控制外部录音程序的方案。
3.5.1 在ubuntu终端中实现录音并保存为wav文件
定义wav头(固定格式):
文件类型、采样率、通道数、数据大小等信息都包含在里面
设置音频硬件参数
音频参数(如格式、采样率、通道数、帧大小等)通过 snd_pcm_hw_params 进行配置。
打开WAV文件并写入头部
录音循环
当键盘输入q时会自动退出录音循环。
3.5.2 实现qt对外部录音程序的调用与控制
3.5.3 录音程序完整代码:
#include #include #include #include #define PCM_DEVICE \"plughw:0,0\"#define FORMAT SND_PCM_FORMAT_S16_LE#define CHANNELS 1#define SAMPLE_RATE 16000#define BITS_PER_SAMPLE 16#define WAV_HEADER_SIZE 44// WAV 文件头typedef struct { char riff[4]; // \"RIFF\" unsigned int overall_size; // 文件大小 - 8 char wave[4]; // \"WAVE\" char fmt_chunk_marker[4]; // \"fmt \" unsigned int length_of_fmt; // 格式数据块大小 unsigned short format_type; // 格式类别 (PCM = 1) unsigned short channels; // 通道数 unsigned int sample_rate; // 采样率 unsigned int byterate; // 每秒字节数 unsigned short block_align; // 一个样本的字节数 unsigned short bits_per_sample; // 每个样本的位数 char data_chunk_header[4]; // \"data\" unsigned int data_size; // 音频数据大小} wav_header_t;// 生成WAV文件头void write_wav_header(FILE *file, int channels, int sample_rate, int bits_per_sample, int data_size) { wav_header_t header; memcpy(header.riff, \"RIFF\", 4); header.overall_size = data_size + WAV_HEADER_SIZE - 8; memcpy(header.wave, \"WAVE\", 4); memcpy(header.fmt_chunk_marker, \"fmt \", 4); header.length_of_fmt = 16; header.format_type = 1; // PCM header.channels = channels; header.sample_rate = sample_rate; header.byterate = sample_rate * channels * bits_per_sample / 8; header.block_align = channels * bits_per_sample / 8; header.bits_per_sample = bits_per_sample; memcpy(header.data_chunk_header, \"data\", 4); header.data_size = data_size; fwrite(&header, 1, sizeof(wav_header_t), file);}// 检查键盘输入的函数int kbhit() { struct termios oldt, newt; int ch; int oldf; tcgetattr(STDIN_FILENO, &oldt); newt = oldt; newt.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &newt); oldf = fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); ch = getchar(); tcsetattr(STDIN_FILENO, TCSANOW, &oldt); fcntl(STDIN_FILENO, F_SETFL, oldf); if (ch != EOF) { ungetc(ch, stdin); return 1; } return 0;}// 主函数int main() { unsigned int sample_rate = SAMPLE_RATE; int channels = CHANNELS; snd_pcm_uframes_t frames = 32; // 每次读取32帧 // 打开 ALSA PCM 设备 snd_pcm_t *pcm_handle; snd_pcm_hw_params_t *params; snd_pcm_uframes_t frames_per_period; int pcm; pcm = snd_pcm_open(&pcm_handle, PCM_DEVICE, SND_PCM_STREAM_CAPTURE, 0); if (pcm < 0) { fprintf(stderr, \"ERROR: Can\'t open \\\"%s\\\" PCM device. %s\\n\", PCM_DEVICE, snd_strerror(pcm)); return -1; } // 设置硬件参数 snd_pcm_hw_params_malloc(¶ms); snd_pcm_hw_params_any(pcm_handle, params); snd_pcm_hw_params_set_access(pcm_handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); snd_pcm_hw_params_set_format(pcm_handle, params, FORMAT); snd_pcm_hw_params_set_channels(pcm_handle, params, channels); snd_pcm_hw_params_set_rate_near(pcm_handle, params, &sample_rate, 0); snd_pcm_hw_params_set_period_size_near(pcm_handle, params, &frames, 0); pcm = snd_pcm_hw_params(pcm_handle, params); if (pcm < 0) { fprintf(stderr, \"ERROR: Can\'t set hardware parameters. %s\\n\", snd_strerror(pcm)); return -1; } snd_pcm_hw_params_get_period_size(params, &frames_per_period, 0); // 打开WAV文件并写入头部 FILE *file = fopen(\"test.wav\", \"wb\"); if (!file) { fprintf(stderr, \"ERROR: Can\'t open output file.\\n\"); return -1; } write_wav_header(file, channels, sample_rate, BITS_PER_SAMPLE, 0); // 先写入空的WAV头 // 分配缓冲区 int buffer_size = frames_per_period * channels * BITS_PER_SAMPLE / 8; char *buffer = (char *) malloc(buffer_size); // 录音循环 int total_bytes = 0; int running = 1;printf(\"begin-----\"); while (running) { if (kbhit()) { char c = getchar(); if (c == \'q\') { running = 0; } } pcm = snd_pcm_readi(pcm_handle, buffer, frames_per_period); if (pcm == -EPIPE) { fprintf(stderr, \"XRUN.\\n\"); snd_pcm_prepare(pcm_handle); } else if (pcm < 0) { fprintf(stderr, \"ERROR: Can\'t read from PCM device. %s\\n\", snd_strerror(pcm)); } else { fwrite(buffer, 1, buffer_size, file); total_bytes += buffer_size; } } // 更新 WAV 头部文件大小信息 fseek(file, 0, SEEK_SET); write_wav_header(file, channels, sample_rate, BITS_PER_SAMPLE, total_bytes); // 清理 free(buffer); fclose(file); snd_pcm_drain(pcm_handle); snd_pcm_close(pcm_handle); return 0;}
编译:
gcc recorder.c -o recorder -lasound
开发总结
1.问题与解决
1、由于ubuntu2204本身系统较大并且docket下语音模型较大,要时刻注意系统磁盘空间,否则容易造成虚拟机卡顿并且重启无法开机,虚拟机在系统内分盘比较麻烦,因此调试过程中重装了两次虚拟机(建议新建虚拟机时分盘30G)
运用df -h 命令查看磁盘空间剩余
2、Ubuntu2204下安装qt,一开始是在ubuntu 自带的software安装qt,但在新建项目中kits套件是没得选的,即使后来另外下载qt库也无法解决,查阅资料发现推荐在qt官网下载离线安装包(.run文件)进行安装,里面有专门的大安装包,包含开发需要的东西,并且能自动配置好。
但是官网qt5.15以下版本都无法访问,而5.15及6版本都取消了离线安装,只支持在线(在线及其不稳定要梯子)
最后选择通过命令行下载
3、ssl是一种加密协议,用于通过互联网保护数据的传输安全。即使在启动服务是加入相关代码,funasr服务器的ssl无法关闭,无论是浏览器的网页访问还是官方客户端的--is_ssl 0连接命令,服务器都会报错
问题原因可能在于服务端代码是wss版本,不允许客户端的无ssl 访问。
解决办法是在qt客户端的连接时也使用qssl 类来建立自签名的证书QSslSocket::VerifyNone 表示在本地测试时,不验证服务器的 SSL 证书。似乎仅仅只能在本地连接时使用,而不使用这个参数时进行qssl连接会失败。
4、在实现音频数据的发送时,一开始没有考虑pcm的数据格式问题,直接发送了 float 数据,这可能导致服务器端处理数据的方式不同,使得服务器应答成功但是返回了一个空的文本结果。
while (audio.Fetch(buff, len, flag) > 0) { QBuffer buffer; buffer.setData(reinterpret_cast(buff), len * sizeof(float)); m_client.sendBinaryMessage(buffer.data()); }
解决:在进行排错检查以及与原来示例代码研究发现,将 float 数组转换为 short 数组,通过乘以 32768 来进行缩放,最后将short 数组分块发送到服务器,每块大小为 102400 字节。
随后对qt的处理方式进行更改,结果成功出来结果,查阅资料明白,是要将 float 数据乘以一个缩放因子,以将其映射到 short 数据的范围。对于16位PCM,这个缩放因子通常是 32768(即 2^15),因为 float 数据的范围是 -1.0 到 1.0,而 short 数据的范围是 -32768 到 32767。
5.在开发录音功能时一开始考虑使用qt内库实现功能,需要qt的multimedia库,通过命令下载:
apt-get install qtmultimedia5-dev
提示找不到这个包,更新软件包列表也用。
原因与解决:因为qt5multimedia软件包位于Universe仓库中,而我的ubuntu系统的软件源列表中没有这个Universe仓库,所以找不到。解决方法为运行sudo add-apt-repository universe命令添加这个软件源,再下载multimedia库。
6.在进行服务器的部署时需要启动服务,直接运行github官方的启动命令
然后没有结果产生,然后再多次重复输入启动,导致直接卡死。
原因:运行该命令已经开始启动服务并下载模型,但由于其中有参数:nohup以及--hotword /workspace/models/hotwords.txt > log.txt 2>&1 &,使得所有的日志输出到了log.txt当中,终端是不会产生输出,在调试时建议去除这行参数。
7.在qt与外部录音程序的交互时有外部录音程序可执行文件没问题,qt内用qprocess打开却报错如下
原因:由于我的qt只有用户权限,对文件的读写和文件的操作都有限制
解决方法:把wav和 可执行文件record执行chmod 777
8.在解决上面问题后,在qt中却无法结束录音,在录音程序中我通过 termios 和 fcntl 系统调用设置终端模式,允许非阻塞的键盘输入检测。当键盘点击q时结束录音。在qt中我试图通过对qprocess对象cmd写入q安全结束录音程序。
结果录音程序推出失败。
原因:cmd->write(\"q\\n\");是qprocess通过写一个q进去,并不触发键盘事件,两者并不一样。
解决:使用了有名管道实现进程间的通信,发现还是不能正常退出,qt进程会报错强制退出,而录音进程还在自己跑,最后我直接用qprocess的start启动了录音程序,kill结束录音程序,相当于强行退出,但最终功能实现了。
2.开发总结
对于funasr的运用开发开始时十分困难,由于这一语音识别框架还在不断更新,我用的0.1.12版本更是当天发布当天进行部署测试,网上的参考资料参差不齐,只能靠自己不断测试不断进行探究错误,是十分需要耐心以及消耗时间的,不过当自己写的qt客户端能够实现语音识别功能时,那种感觉还是很爽的。
这次开发遇到的困难我也并非全部解决,有不少都是直接绕过去,有的就算是解决了也不太清楚为什么会这样,有很多涉及更深层的知识还需要以后不断探究的,就像是ssl这一协议,如果我的连接不在本地又要如何解决,是要注册自签名证书吗,还有为什么在火狐浏览器上仍然不能网页连接我启动的服务器,是浏览器没有自己的ssl协议证书吗。想这些问题还是有挺多没有彻底搞明白的,以后或许会找到答案。