-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
diff --git a/src/utils/recorder.js b/src/utils/recorder.js
new file mode 100644
index 0000000000000000000000000000000000000000..510a42a26b4130391425161f121ece4de94a271c
--- /dev/null
+++ b/src/utils/recorder.js
@@ -0,0 +1,188 @@
+
+import recordWorker from './transcode.worker'
+
+const URL = window.URL || window.webkitURL // 获取浏览器API
+
+// 加载并启动 record worker
+let workerString = recordWorker.toString()
+// 移除函数包裹
+workerString = workerString.substr(workerString.indexOf("{") + 1)
+workerString = workerString.substr(0, workerString.lastIndexOf("}"))
+const workerBlob = new Blob([workerString])
+const workerURL = URL.createObjectURL(workerBlob)
+const worker = new Worker(workerURL)
+
+/**
+ * class IatRecorder 语音听写类
+ * @param {Object} config 参数
+ */
+class IatRecorder {
+ constructor(config) {
+ this.status = "null" // 当前录音的状态 null为开始 ing为录音中 end为结束
+
+ // 记录音频数据
+ this.audioData = []
+ this.buffer = []
+ // 记录听写结果
+ worker.onmessage = (event) => {
+ this.audioData.push(...event.data)
+ console.log('处理后的数据长度', this.audioData.length)
+ }
+ }
+ // 修改录音听写状态
+ setStatus (status) {
+ this.onWillStatusChange &&
+ this.status !== status &&
+ this.onWillStatusChange(this.status, status)
+ this.status = status
+ }
+ // 初始化浏览器录音
+ recorderInit () {
+ navigator.getUserMedia =
+ navigator.getUserMedia ||
+ navigator.webkitGetUserMedia ||
+ navigator.mozGetUserMedia ||
+ navigator.msGetUserMedia
+
+ // 创建音频环境
+ try {
+ this.audioContext = new (window.AudioContext ||
+ window.webkitAudioContext)()
+ console.log('audioContext sampleRate:', this.audioContext.sampleRate)
+ console.log('UA', navigator.userAgent)
+ if (!this.audioContext) {
+ alert("浏览器不支持webAudioApi相关接口,请前往Chrome,FireFox浏览器体验")
+ return
+ }
+ } catch (e) {
+ if (!this.audioContext) {
+ alert("浏览器不支持webAudioApi相关接口,请前往Chrome,FireFox浏览器体验")
+ return
+ }
+ }
+
+ // 获取浏览器录音权限
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+ navigator.mediaDevices
+ .getUserMedia({
+ audio: true,
+ video: false,
+ })
+ .then((stream) => {
+ getMediaSuccess(stream)
+ })
+ .catch((e) => {
+ getMediaFail(e)
+ })
+ } else if (navigator.getUserMedia) {
+ navigator.getUserMedia(
+ {
+ audio: true,
+ video: false,
+ },
+ (stream) => {
+ getMediaSuccess(stream)
+ },
+ function (e) {
+ getMediaFail(e)
+ }
+ )
+ } else {
+ if (
+ navigator.userAgent.toLowerCase().match(/chrome/) &&
+ location.origin.indexOf("https://") < 0
+ ) {
+ alert(
+ "chrome下获取浏览器录音功能,因为安全性问题,需要在localhost或127.0.0.1或https下才能获取权限"
+ )
+ } else {
+ alert("无法获取浏览器录音功能,请升级浏览器或使用chrome")
+ }
+ this.audioContext && this.audioContext.close()
+ return
+ }
+ // 获取浏览器录音权限成功的回调
+ let getMediaSuccess = (stream) => {
+ // 创建一个用于通过JavaScript直接处理音频
+ this.scriptProcessor = this.audioContext.createScriptProcessor(
+ 4096,
+ 1,
+ 1
+ )
+ this.scriptProcessor.onaudioprocess = (e) => {
+ // 去处理音频数据
+ this.buffer.push(...e.inputBuffer.getChannelData(0))
+ console.log('录音原始数据长度,类型为 Float32Array', e.inputBuffer.getChannelData(0).length)
+ worker.postMessage({
+ command: "transform",
+ buffer: e.inputBuffer.getChannelData(0),
+ is16K: this.audioContext.sampleRate,
+ })
+ }
+ // 创建一个新的MediaStreamAudioSourceNode 对象,使来自MediaStream的音频可以被播放和操作
+ this.mediaSource = this.audioContext.createMediaStreamSource(stream)
+ // 连接
+ this.mediaSource.connect(this.scriptProcessor)
+ this.scriptProcessor.connect(this.audioContext.destination)
+ };
+
+ let getMediaFail = () => {
+ alert("请求麦克风失败")
+ this.audioContext && this.audioContext.close()
+ this.audioContext = undefined
+ }
+ }
+
+ /**
+ * 开始录音
+ */
+ recorderStart () {
+ this.setStatus("ing")
+ this.recorderStop()
+ this.recorderInit()
+ }
+ /**
+ * 暂停录音
+ * @param e
+ */
+ recorderStop (e) {
+ // this.download();
+ this.setStatus("end")
+ // 关闭录音
+ this.audioContext && this.audioContext.close()
+ this.audioContext = undefined
+ }
+
+ getBuffer () {
+ let output = this.to16BitPCM(this.buffer)
+ return output
+ }
+ to16BitPCM (input) {
+ var dataLength = input.length * (16 / 8)
+ var dataBuffer = new ArrayBuffer(dataLength)
+ var dataView = new DataView(dataBuffer)
+ var offset = 0
+ for (var i = 0; i < input.length; i++, offset += 2) {
+ var s = Math.max(-1, Math.min(1, input[i]))
+ dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
+ }
+ return dataView
+ }
+ download () {
+ const blob = new Blob([this.getBuffer()]) //处理文档流
+ if ("msSaveOrOpenBlob" in navigator) {
+ window.navigator.msSaveOrOpenBlob(blob, "test.pcm")
+ return
+ }
+ const elink = document.createElement("a")
+ elink.download = "test.pcm"
+ elink.style.display = "none"
+ elink.href = URL.createObjectURL(blob)
+ document.body.appendChild(elink)
+ elink.click()
+ URL.revokeObjectURL(elink.href) // 释放URL 对象
+ document.body.removeChild(elink)
+ }
+}
+
+export default IatRecorder
diff --git a/src/utils/transcode.worker.js b/src/utils/transcode.worker.js
new file mode 100644
index 0000000000000000000000000000000000000000..f52e93270a9470b4d7a538196e10095ee9fc9256
--- /dev/null
+++ b/src/utils/transcode.worker.js
@@ -0,0 +1,112 @@
+const recordWorker = function () {
+ let self = this;
+ this.onmessage = function (e) {
+ switch (e.data.command) {
+ case "transform":
+ transform.transaction(e.data, e.data.is16K);
+ break;
+ }
+ };
+
+ let transform = {
+ transaction(audioData, is16K) {
+ let output;
+ if (is16K === 16000) {
+ output = transform.to16BitPCM(audioData.buffer, is16K);
+ } else if (is16K > 16000) {
+ output = transform.to16kHz(audioData.buffer, is16K);
+ output = transform.to16BitPCM(output);
+ } else if (is16K < 16000) {
+ output = transform.lessTo16kHz(audioData.buffer, is16K);
+ output = transform.to16BitPCM(output);
+ }
+ output = Array.from(new Uint8Array(output.buffer));
+ self.postMessage(output);
+ },
+ /**
+ * 大于16kHz降采样到16kHz
+ * @param audioData
+ * @param originalSampleRate
+ * @return {*|Float32Array}
+ */
+ to16kHz(audioData, originalSampleRate) {
+ if (originalSampleRate <= 16000) {
+ return audioData;
+ }
+
+ let data = new Float32Array(audioData);
+ let fitCount = Math.round(data.length * (16000 / originalSampleRate));
+ let newData = new Float32Array(fitCount);
+ let springFactor = (data.length - 1) / (fitCount - 1);
+
+ newData[0] = data[0];
+ for (let i = 1; i < fitCount - 1; i++) {
+ let tmp = i * springFactor;
+ let before = Math.floor(tmp).toFixed();
+ let after = Math.ceil(tmp).toFixed();
+ let atPoint = tmp - before;
+ newData[i] = data[before] + (data[after] - data[before]) * atPoint;
+ }
+ newData[fitCount - 1] = data[data.length - 1];
+
+ return newData;
+ },
+ /**
+ * 小于16kHz升采样到16kHz
+ * @param audioData
+ * @param originalSampleRate
+ * @return {*|Float32Array}
+ */
+ lessTo16kHz(audioData, originalSampleRate) {
+ // 如果原始采样率已经是16kHz或更高,直接返回原始数据
+ if (originalSampleRate >= 16000) {
+ return audioData;
+ }
+ // 计算新的采样率与原始采样率的比例
+ const upsampleFactor = 16000 / originalSampleRate;
+ // 计算新的数据长度
+ const newLength = Math.ceil(audioData.length * upsampleFactor);
+ // 创建一个新的 Float32Array 用于存储升采样后的数据
+ const newData = new Float32Array(newLength);
+ // 进行最近邻插值
+ for (let i = 0; i < newLength; i++) {
+ // 计算原始数据中对应的样本索引
+ const originalIndex = Math.floor(i / upsampleFactor);
+ // 将原始样本值复制到新数据中
+ newData[i] = audioData[originalIndex];
+ }
+ return newData;
+ },
+ to16BitPCM(input) {
+ let dataLength = input.length * (16 / 8); // 计算了将要创建的缓冲区的长度,这里使用了位操作来表示字节数,即 16 / 8 表示每个样本占用2个字节(16位PCM格式)。
+ let dataBuffer = new ArrayBuffer(dataLength); // 是一个用于存储二进制数据的固定大小的缓冲区,其长度为 dataLength 字节。
+ let dataView = new DataView(dataBuffer); // 则是一个用于读写 dataBuffer 中数据的视图,通过它可以以不同的数据格式(如整数、浮点数)访问缓冲区中的数据。
+
+ let offset = 0; // offset 变量用于追踪当前写入到 dataView 中的位置。初始为0,每次迭代增加2,因为每个16位样本占用2个字节。
+ for (let i = 0; i < input.length; i++, offset += 2) {
+ let s = Math.max(-1, Math.min(1, input[i])); // Math.max(-1, Math.min(1, input[i])) 确保将输入值限制在 -1 到 1 之间,因为16位PCM格式只能表示在 -32768 到 32767 之间的整数。
+ // dataView.setInt16(offset, value, littleEndian) 将处理过的音频数据写入到 dataView 中:
+ // offset 是数据在缓冲区中的偏移量。
+ // value 是通过将输入数据映射到16位有符号整数的方式得到的数值。
+ // littleEndian 参数设为 true,表示使用小端序(低位字节在前,高位字节在后)存储数据,这是常见的音频格式存储方式。
+ dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
+ }
+ return dataView;
+ },
+ to8BitPCM(input) {
+ var dataLength = input.length;
+ var dataBuffer = new ArrayBuffer(dataLength);
+ var dataView = new DataView(dataBuffer);
+ var offset = 0;
+ for (var i = 0; i < input.length; i++, offset++) {
+ var s = Math.max(-1, Math.min(1, input[i]));
+ var val = s < 0 ? s * 0x80 : s * 0x7F;
+ var intVal = Math.round(val); // Round to nearest integer
+ dataView.setInt8(offset, intVal);
+ }
+ return dataView;
+ },
+ };
+};
+
+export default recordWorker;