前言--踩坑过程
一时间心血来潮,想用科大讯飞的api来做一个语音实时转文字,也是走了很多弯路,边写边查边生成,最后算是完成了。功能实现了但是没有做UI。
本来想试试光靠不要服务端光靠前端直接调用科大讯飞的api来实现,但是发现太慢了,四五秒才蹦出来一个字。
然后没办法,搭建了一个服务端,一开始用的是直接用上传的文件来做,但是也还是很慢,当然可能是我代码写得烂。
后面网上搜了一下,试着把上传的文件保存为pcm文件,然后读取pcm,快了特别多。
还有流式传输,一开始思路错了,我以为的是分段截取然后上传,但是这样识别的正确率简直是不堪入目。后面使用不暂停录音来截取而是直接上传目前已经录入的。
注册讯飞应用获取免费服务
自行注册,如果一天500免费额度不够可以去买一个五万的免费的,一年内。
使用socket.io搭建服务
前面还有创建react项目我就跳过了。
下载socket.io和recorder
npm i js-audio-recorder
npm i socket.io-client
用户端搭建一个连接ws和recorder(录音的),同时加入房间。
import Recorder from 'js-audio-recorder';
import { useEffect, useState } from "react";
import io from 'socket.io-client';
const [roomId, setrooId] = useState('') // 定义 roomId 状态,初始值为空字符串
const [ws, setWs] = useState(null) // 定义 ws 状态,用于存储 WebSocket 连接,初始值为 null
const [recorder, setrecorder] = useState(null) // 定义 recorder 状态,用于存储录音器实例,初始值为 null
useEffect(() => {
// 创建新的 WebSocket 连接
const socketIo = io(url);
setWs(socketIo); // 保存 WebSocket 连接实例到 ws 状态中
const roomid = new Date().getTime() // 获取当前时间的时间戳,作为房间 ID
setrooId(roomid) // 保存房间 ID 到 roomId 状态中
socketIo.emit('joinRoom', roomid) // 发送 joinRoom 事件,附带房间 ID,通知服务器加入房间
socketIo.on('value', val => {
setTemp(val); // 当从服务器接收到 value 事件时,更新 temp 状态
})
setrecorder(new Recorder({
bitRate: 16, // 设置录音比特率为 16 kbps
sampleRate: 16000, // 设置录音采样率为 16000 Hz
bufferSize: 8192, // 设置录音缓冲区大小为 8192 字节
}))
// 清理函数,在组件卸载时断开 WebSocket 连接
return () => {
socketIo.disconnect();
};
}, [url]); // useEffect 钩子依赖 url,当 url 改变时重新执行
服务端
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const { Server } = require('socket.io');
const cors = require('cors');
// 创建 Express 应用
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: '*',
methods: ['GET', 'POST']
}
});
// 配置 Socket.IO 事件
io.on('connection', (socket) => {
console.log('a user connected');
socket.on('disconnect', () => {
console.log('user disconnected');
});
socket.on('joinRoom', (roomId) => {
socket.join(roomId)
})
});
// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
基本服务就起了。
录音并采用流式传输传递音频数据
现在开始就是要开始录音并且传递出去了。获取的Blob格式不适合用于传输,所以这里就转化成 base64 编码来传输。
注意这里的流式传输,最开始我写的是每隔两秒就结束录音然后截取发过去再重新启动录音,但是效果不好,这里采用的是200毫秒发送一次,但是不暂停录音器。这样就很有效果了。
const startR = () => {
console.log('开始');
recorder.start(); // 开始录音
//流式传输**************
const sendAudioData = () => {
const pcmBlob = recorder.getPCMBlob(); // 获取 PCM 格式的音频 Blob 数据
console.log(pcmBlob);
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result; // 将 Blob 数据转换为 ArrayBuffer
const base64 = arrayBufferToBase64(arrayBuffer); // 将 ArrayBuffer 转换为 base64 编码
// 发送 base64 编码的音频数据到 WebSocket 服务器
ws.emit('other', {
roomId: roomId,
value: base64 // 传递 base64 编码的音频数据
});
};
reader.readAsArrayBuffer(pcmBlob); // 将 Blob 数据读取为 ArrayBuffer
};
const intervalId = setInterval(sendAudioData, 200); // 每 200 毫秒调用一次 sendAudioData 函数,发送音频数据
setT(intervalId); // 保存定时器 ID 到 T 状态中
//流式传输**************
};
// 停止录音的函数
cons