问题
由于uniapp录音上下文(uni.getRecorderManager())的帧功能(recordManager.onFrameRecorded)仅支持小程序,app端只会获取到一个临时的录音文件地址,因此需要把录音文件转成base64,在切片传输给讯飞的接口.
一、申请一个讯飞的apiKey和秘钥 控制台-讯飞开放平台
二、页面展示一个录音按钮,控制语音输入输出.
<template>
<div class="asr">
<up-button class="btn" shape="circle" type="info" @touchstart="openMedia" @touchend="stopMedia">按住
说话</up-button>
<!-- @click="handleShow" -->
<!-- <iatPopup v-model:moduleValue="show" /> -->
<view v-if="show" class="iating">
<up-image src="/static/images/main/iat.png" width="300rpx" height="120rpx" class="img"></up-image>
<up-text text="正在说话中...." color="#49ABFE" size="27rpx" line-height="60rpx" align="center"></up-text>
</view>
</div>
</template>
二、对接讯飞鉴权接口
import CryptoJS from "crypto-js";
// 科大讯飞接口配置
const config = {
hostUrl: "wss://iat-api.xfyun.cn/v2/iat",
host: "iat-api.xfyun.cn",
appid: "你的appid",
apiSecret: "你的秘钥",
apiKey: "你的apikey",
uri: "/v2/iat",
highWaterMark: 1280,
// file: "./16k_10.pcm",
};
// 鉴权签名
function getAuthStr(date) {
let signatureOrigin = `host: ${config.host}\ndate: ${date}\nGET ${config.uri} HTTP/1.1`;
let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret);
let signature = CryptoJS.enc.Base64.stringify(signatureSha);
let authorizationOrigin =
`api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
let authStr = CryptoJS.enc.Base64.stringify(
CryptoJS.enc.Utf8.parse(authorizationOrigin)
);
return authStr;
}
function getUrl() {
// 获取当前时间 RFC1123格式
let date = new Date().toUTCString();
//科大讯飞远程连接地址
let wssUrl =
config.hostUrl +
"?authorization=" +
getAuthStr(date) +
"&date=" +
encodeURIComponent(date) +
"&host=" +
config.host;
console.log("websocke科大讯飞的地址为", wssUrl);
return wssUrl;
}
三、监听录音点击开始事件,录音开始的时候连接讯飞的websokect,url是鉴权返回的url.
//打开麦克风
function openMedia() {
connectSocket(); //创建websocket连接
}
//创建连接并返回数据
function connectSocket() {
//创建socketTask实例
if (uniSocketTask === null) {
uniSocketTask = uni.connectSocket({
url: getUrl(),
success() {},
});
//监听连接成功的事件
uniSocketTask.onOpen(() => {
console.log("监听到开启连接成功");
startRecord(); //打开录音
});
//监听连接关闭的事件
uniSocketTask.onClose(() => {
console.log("监听到关闭连接成功");
uniSocketTask = null;
});
uniSocketTask.onError(() => {
console.log("监听到连接发生错误");
});
//监听科大讯飞消息返回
uniSocketTask.onMessage((res) => {
//收到消息
const message = JSON.parse(res.data);
//判断是否存在数据
if (res.data) {
console.log("收到服务器消息,并开始渲染", message);
renderResult(message);
if (message.code === 0 && message.data.status === 2) {
//该函数为当前页唯一的关闭连接函数
console.log('最后一条', renderText.value)
closeSocket();
emit('renderText', renderText.value)
}
//收到不正常服务器消息,返回错误到控制台
if (message.code !== 0) {
closeSocket();
console.error(message);
}
} else {
console.log("未监听到消息:原因:", JSON.stringify(res));
}
});
} else {
console.log("socketTask实例已存在");
}
}
//发送给科大讯飞的第一帧的模板数据格式
let frame = {
common: {
app_id: config.appid,
},
business: {
language: "zh_cn",
domain: "iat",
accent: "mandarin",
dwa: "wpgs", // 可选参数,动态修正
vad_eos: 5000,
},
data: {
status: 0,
format: "audio/L16;rate=16000",
encoding: "lame"
},
};
//发送消息
function sendMessage(sendData) {
console.log('发送', JSON.stringify(sendData))
uniSocketTask.send({
data: JSON.stringify(sendData),
success() {
// console.log("发送成功");
},
fail() {
console.log("发送失败");
},
});
}
//关闭连接
function closeSocket() {
console.log("开始尝试关闭连接");
// 关闭连接
uniSocketTask.close();
}
四、录音结束的时候,先传输第一帧信息,返回文件地址,由于uniapp小程序可以直接监听录音帧,但是app不能监听到帧所以需要将录音文件转成base64,在切片传输.
// 录音配置项
const recordOption = {
sampleRate: 16000, // 采样率(pc不支持)
format: "mp3", // 音频格式,默认是 aac
};
const recordManager = uni.getRecorderManager();
//开启录音
const startRecord = () => {
recordManager.onStart(() => {
console.log("开始录音");
show.value = true
// ...
});
recordManager.onStop((res) => {
// tempFilePath String 录音文件的临时路径
console.log("录音停止,文件路径为:", res.tempFilePath);
sendMessage(frame)
pathToBase64(res.tempFilePath).then(base64 => {
console.log('base64--', base64);
// 这里一定要注意:当需要将base64字符串转换为Buffer时,通常会去掉前面部分的数据URL标识(如"data:image/png;base64,"),因为这部分内容不是实际的base64编码数据。
let buff = base64.split(",")[1]
const arrayBuffer = uni.base64ToArrayBuffer(buff)
const audioString = toString(arrayBuffer)
console.log("文件读取成功", audioString.length);
let offset = 0;
while (offset < audioString.length) {
const subString = audioString.substring(offset, offset + 1280)
offset += 1280
const isEnd = offset >= audioString.length
let params = {
data: {
status: isEnd ? 2 : 1,
format: "audio/L16;rate=16000",
encoding: "lame",
audio: btoa(subString)
}
}
sendMessage(params);
}
}).catch(error => {
console.error(error)
})
});
recordManager.onError((err) => {
// errMsg String 错误信息
console.log("录音出现错误", err);
});
recordManager.start(recordOption);
};
//关闭录音
const stopRecord = () => {
recordManager.stop();
};
const toString = (buffer) => {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return binary
}
//录音文件路径转base64
function pathToBase64(path) {
return new Promise(function(resolve, reject) {
// app
if (typeof plus === 'object') {
plus.io.resolveLocalFileSystemURL(path, function(entry) {
entry.file(function(file) {
var fileReader = new plus.io.FileReader()
fileReader.onload = function(evt) {
resolve(evt.target.result)
}
fileReader.onerror = function(error) {
reject(error)
}
fileReader.readAsDataURL(file)
}, function(error) {
reject(error)
})
}, function(error) {
reject(error)
})
return
}
reject(new Error('not support'))
})
}
五、处理讯飞返回的数据
let resultText = "";
let resultTextTemp = "";
let renderText = ref("");
//讯飞回复字段拼接
function renderResult(jsonData) {
//console.log("开始执行渲染函数", jsonData);
if (jsonData.data && jsonData.data.result) {
let data = jsonData.data.result;
let str = ""; // 初始化一个字符串变量用于存储拼接后的识别结果
let ws = data.ws;
for (let i = 0; i < ws.length; i++) {
str = str + ws[i].cw[0].w;
}
// 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
// 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
if (data.pgs) {
if (data.pgs === "apd") {
// 将resultTextTemp同步给resultText
resultText = resultTextTemp;
}
// 将结果存储在resultTextTemp中
resultTextTemp = resultText + str;
} else {
resultText = resultText + str;
}
renderText.value = resultTextTemp || resultText || "";
}
console.log("渲染后的数据为");
console.log(renderText.value);
}
总结
在项目过程中遇到讯飞返回给我的一直是空数据问题,感觉是因为传过去的数据有问题,因此要确定第一帧的配置信息是否正确,跟每一帧发送的base64是不是不一样的数据,这样返回的数据就不用有问题了。