背景:
实现移动端录制音频,音频上传与播放,使用原生app 对外提供的录音api (原生的h5 getUserMedia,受app 的内核浏览器限制 不支持 getUserMedia方法,同时流行的插件 js-audio-record 与 record-core pc端调试支持,移动端不支持。
app 提供的api 接口类似于 字节跳动对外提供的接口 但是具体获取数据格式是否一致未知
遇到问题:与常规插件直接获取MP3,WAV 的音频文件不同,onframeRecord 获取的是二进制流式pcm 音频,与getUserMedia 获取的音频还又区别
问题一:不可以像MP3 或wav 文件一样直接使用audio 进行播放
问题二:实时获取的数据流无法直接像非流式文件一样 使用blob 对象上传。
尝试解决思路:
1.网络现有的将pcm 转换成wav格式 的方法 ,使用方法转换文件类型,方便上传,与播放
最终结果:尝试失败;
原因:app 封装的的api onframeRecord 获取的音频流与原生getUserMedia获取的音频流有出入,直接添加wav文件头,播放后是白噪音。
2.引入pcmPlayer 播放器 播放实时音频流
最终结果:成功播放实时音频流,但是要将数据上传后 在获取音频流再次播放还无法解决
3.通过将实时音频流使用jsZip 压缩后作为zip 文件上传给后端,之后在获取该文件进行解压 播放
最终结果:失败,解压失败
4.不进行后台上传直接将实时音频流push 到一个变量中然后作为参数传给接口,再次获取时从接口中获取
最终结果:失败
原因:不可行:1音频文件过大,2,文件流存入数组中传给后台,后台无法识别,保存失败返回值为空。
5.使用unit8Array将arrayBuffer 类型的音频流可视化,在转为字符串按照方式4的方法作为参数传给后台。
最终结果:失败
原因:从后台获取字符串后要转换为unit8array 再从unit8Array转换为buffer 整个过程繁琐,转化存在乱码,播放失败。同时也存在参数内存过大问题
6.通过各种尝试后,想到通过变量数据接收所有的音频流然后将他转换为txt的文件 通过blob 上传给后台,再通过下载接口,通过获取的url 获取该txt文件,读取该文件返回的数据流,然后再次使用pcmPlayer 播放。
最终结果:成功,既解决了大文件上传问题,也解决了上传后播放的问题。
部分开发代码:
<button
@touchstart.prevent="handelLongPress"
@touchend.prevent="touchend"
></button>
//长按录音,松开停止
// 长按录音开始
const handelLongPress = () => {
clearTimeout(loop.value);
longTouch.value = false;
loop.value = setTimeout(() => {
longTouch.value = true;
handRecordAudio();
}, 300);
};
// 录音
const handRecordAudio = () => {
//录音监听
eventInfo.value = toongine.recorder.onFrameRecorded({
callback: (res) => {
if (res.data.isLastFrame) {
console.log(
"toongine::recorder::onFrameRecorded::ended",
"回调获取数据结束",
);
if (recordedChunks.value.length) {
//作为txt 文件上传
const txtBlob = new Blob(recordedChunks.value, {
type: "text/plain",
});
let fileOfBlob = new File(
[txtBlob],
`录音${new Date().getTime()}` + ".txt"
);
const formData = new FormData();
formData.append("multipartFiles", fileOfBlob);
//上传接口
uploadFile(formData).then((res) => {
if (res.code == 200) {
recordedChunks.value = [];
audioFileList.value.push(res.data[0]);
} else {
showToast("语音上传失败");
}
});
return;
}
}
// 操作数据需要使用typed array view 或 DataView
// var pcm = new Int16Array(res.data.frameBuffer);
let pcmData = res.data.frameBuffer;
let list = [...recordedChunks.value];
list.push(pcmData);
recordedChunks.value = list;
},
});
//录音开始
toongine.recorder.start({
params: {
sampleRate: 32000,
numberOfChannels: 2,
frameSize: 1,
bitsPerChannel: 16,
format: "PCM",
},
callback: (res) => {
console.log("录音1开始");
},
});
};
// 长按录音松开
const touchend = () => {
clearTimeout(loop.value); // 清空定时器,防止重复注册定时器
if (!longTouch.value) {
//如果不是长按,执行点击事件
console.log("点击");
} else {
toongine.recorder.stop({
callback: (res) => {
//移除录音事件监听
toongine.removeEventListener(eventInfo.value);
console.log("recordedChunks.value", recordedChunks.value);
if (!recordedChunks.value.length) {
showToast("录音失败,为获取到音频文件");
}
},
});
}
};
//录音播放
const playRecord = (item) => {
console.log("播放", item);
if (item.fileUrl) {
axios
.get("/api/disposal/oss/file/download", {
params: {
fileUrl: item.fileUrl,
},
responseType: "blob", //定义返回数据格式为Blob
})
.then((res) => {
console.log("返回值blob", res, res.data);
let reader = new FileReader();
reader.onload = function (e) { //读取获取到的 txt 文件
// let zipFile = e.target.result[0];
console.log("e", e.target.result);
pcmPlay.value.feed(e.target.result);
};
reader.readAsArrayBuffer(res.data);
});
} else {
showToast("播放失败");
}
};
注意点:
1.引入pcm-player播放器
2.获取txt 流文件时要设置相应类型为blob ,然后读取文件流进行播放
3.pcmPlayer 播放器对应得音频 采样数,声道数等信息得设置最好与 调用录音start方法得参数保持一致,以防止声音播放异常