UNPKG

js-recorder-rtc

Version:
412 lines (411 loc) 20.1 kB
var C = Object.defineProperty; var u = (l, t, e) => t in l ? C(l, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : l[t] = e; var c = (l, t, e) => u(l, typeof t != "symbol" ? t + "" : t, e); var r = /* @__PURE__ */ ((l) => (l.INACTIVE = "inactive", l.RECORDING = "recording", l.PAUSED = "paused", l))(r || {}); class d { // /** // * 录音时长(以秒为单位) // * @private // */ // private duration: number = 0; // /** // * 定时器 ID // * @private // */ // private timer: number | null = null; /** * 初始化录音器的配置和状态。 * @param options - 录音配置选项 */ constructor(t = {}) { /** * 录音配置参数:采样率、采样位数、声道数、缓冲区大小 * @private */ c(this, "config"); /** * Web Audio API 音频上下文 * @private */ c(this, "audioContext"); /** * 媒体流对象,表示从麦克风获取的音频流 * @private */ c(this, "stream"); /** * AudioWorkletNode 或 ScriptProcessorNode 节点 * @private */ c(this, "recorderNode"); /** * 存储录音的音频数据数组,存储左声道和右声道 * @private */ c(this, "audioData"); /** * 实际使用的采样率,可能与请求的采样率不同。 * 浏览器可能会调整实际采样率以适应设备。(这个是设备决定的) * @private */ c(this, "actualSampleRate"); /** * 当前录音状态 未激活/录音中/已暂停 * @private */ c(this, "status"); /** * 实时 PCM 数据回调函数 * @private */ c(this, "onPCMDataCallback", null); this.config = { sampleRate: t.sampleRate || 48e3, sampleBits: t.sampleBits || 8, channels: t.channels || 1, bufferSize: t.bufferSize || 4096 }, this.validateConfig(), this.audioContext = null, this.stream = null, this.recorderNode = null, this.audioData = { l: [], r: [] }, this.actualSampleRate = this.config.sampleRate, this.status = r.INACTIVE; } /** * 验证录音配置参数是否有效。 * 检查采样位数、声道数和采样率是否在允许范围内。 * @throws {Error} 当参数无效时抛出错误 * @private */ validateConfig() { if (![8, 16].includes(this.config.sampleBits)) throw new Error("采样位数必须是 8 或 16"); if (![1, 2].includes(this.config.channels)) throw new Error("声道数必须是 1 或 2"); if (this.config.sampleRate < 8e3 || this.config.sampleRate > 96e3) throw new Error("采样率必须在 8000 至 96000 之间"); } /** * 获取当前录音状态 * @returns {RecorderStatus} 当前状态 */ getStatus() { return this.status; } /** * 初始化录音功能 * 请求麦克风权限,并设置音频处理节点(AudioWorkletNode 或 ScriptProcessorNode)。 * @throws {Error} 初始化失败时抛出错误 */ async init() { try { const t = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: this.config.sampleRate, // 请求的采样率 约束条件 channelCount: this.config.channels, // 请求的声道数 约束条件 echoCancellation: !0 // 启用回声消除 } }), e = t.getAudioTracks()[0]; console.log("音频轨道设置:", e.getSettings()), console.log("音频轨道约束:", e.getConstraints()); const o = window.AudioContext || window.webkitAudioContext; if (!o) throw new Error("当前浏览器不支持 Web Audio API"); this.audioContext = new o({ // sampleRate: this.config.sampleRate, // 请求的采样率 设置了但是设备不一定支持和使用这个 }), this.stream = t; const i = this.audioContext.createMediaStreamSource(t); if (this.actualSampleRate = this.audioContext.sampleRate, "audioWorklet" in this.audioContext) try { const s = new URL("data:video/mp2t;base64,LyoNCiAqIEBBdXRob3I6IHdlYmJlclFpYW4NCiAqIEBEYXRlOiAyMDI0LTExLTE0IDE0OjI3OjM0DQogKiBATGFzdEVkaXRUaW1lOiAyMDI0LTExLTI5IDEzOjU1OjM1DQogKiBATGFzdEVkaXRvcnM6IHdlYmJlclFpYW4NCiAqIEBEZXNjcmlwdGlvbjpSZWNvcmRlclByb2Nlc3Nvcg0KICog5rKh5pyJ55CG5oOz77yM5L2V5b+F6L+c5pa544CCDQogKi8NCi8qKg0KICogUmVjb3JkZXJQcm9jZXNzb3Ig5piv5LiA5LiqIEF1ZGlvV29ya2xldFByb2Nlc3Nvcu+8jOeUqOS6juWkhOeQhumfs+mikeaVsOaNruOAgg0KICog5a6D5o6l5pS25p2l6Ieq5Li757q/56iL55qE5ZG95Luk5p2l5o6n5Yi25b2V6Z+z54q25oCB77yM5bm25bCG5b2V5Yi255qE6Z+z6aKR5pWw5o2u5Y+R6YCB5Zue5Li757q/56iL44CCDQogKi8NCmNsYXNzIFJlY29yZGVyUHJvY2Vzc29yIGV4dGVuZHMgQXVkaW9Xb3JrbGV0UHJvY2Vzc29yIHsNCiAgLy8g5b2V6Z+z54q25oCB5qCH5b+X77yM5Yid5aeL5Li65LiN5b2V6Z+zDQogIHByaXZhdGUgcmVjb3JkaW5nOiBib29sZWFuOw0KICBidWZmZXJTaXplOiBudW1iZXI7DQogIGNoYW5uZWxzOiBudW1iZXI7DQoNCiAgY29uc3RydWN0b3Iob3B0aW9uczogQXVkaW9Xb3JrbGV0Tm9kZU9wdGlvbnMpIHsNCiAgICBzdXBlcihvcHRpb25zKTsNCiAgICAvLyDnm5HlkKzmnaXoh6rkuLvnur/nqIvnmoTmtojmga8NCiAgICB0aGlzLnBvcnQub25tZXNzYWdlID0gdGhpcy5oYW5kbGVNZXNzYWdlLmJpbmQodGhpcyk7DQogICAgLy8g5Yid5aeL5YyW5b2V6Z+z54q25oCB5Li65LiN5b2V6Z+zDQogICAgdGhpcy5yZWNvcmRpbmcgPSBmYWxzZTsNCiAgICAvLyDor7vlj5YgcHJvY2Vzc29yT3B0aW9ucyDkuK3nmoQgYnVmZmVyU2l6ZSDvvIjnvJPlhrLlpKflsI/vvIkNCiAgICB0aGlzLmJ1ZmZlclNpemUgPSBvcHRpb25zLnByb2Nlc3Nvck9wdGlvbnMuYnVmZmVyU2l6ZTsNCiAgICAvLyDlo7DpgZPmlbDph48gMXwyDQogICAgdGhpcy5jaGFubmVscyA9IG9wdGlvbnMucHJvY2Vzc29yT3B0aW9ucy5jaGFubmVsczsNCiAgfQ0KDQogIC8qKg0KICAgKiDlpITnkIbmnaXoh6rkuLvnur/nqIvnmoTmtojmga8NCiAgICog5qC55o2u5o6l5pS25Yiw55qE5ZG95Luk5o6n5Yi25b2V6Z+z54q25oCBDQogICAqIEBwYXJhbSBldmVudCAtIOa2iOaBr+S6i+S7tuWvueixoQ0KICAgKi8NCiAgcHJpdmF0ZSBoYW5kbGVNZXNzYWdlKGV2ZW50OiBNZXNzYWdlRXZlbnQpOiB2b2lkIHsNCiAgICBjb25zdCB7IGNvbW1hbmQgfSA9IGV2ZW50LmRhdGE7DQogICAgc3dpdGNoIChjb21tYW5kKSB7DQogICAgICBjYXNlICJzdGFydCI6DQogICAgICAgIC8vIOW8gOWni+W9lemfsw0KICAgICAgICB0aGlzLnJlY29yZGluZyA9IHRydWU7DQogICAgICAgIGJyZWFrOw0KICAgICAgY2FzZSAicGF1c2UiOg0KICAgICAgICAvLyDmmoLlgZzlvZXpn7MNCiAgICAgICAgdGhpcy5yZWNvcmRpbmcgPSBmYWxzZTsNCiAgICAgICAgYnJlYWs7DQogICAgICBjYXNlICJyZXN1bWUiOg0KICAgICAgICAvLyDmgaLlpI3lvZXpn7MNCiAgICAgICAgdGhpcy5yZWNvcmRpbmcgPSB0cnVlOw0KICAgICAgICBicmVhazsNCiAgICAgIGNhc2UgInN0b3AiOg0KICAgICAgICAvLyDlgZzmraLlvZXpn7PlubbmuIXnqbrpn7PpopHmlbDmja4NCiAgICAgICAgdGhpcy5yZWNvcmRpbmcgPSBmYWxzZTsNCiAgICAgICAgYnJlYWs7DQogICAgICBkZWZhdWx0Og0KICAgICAgICAvLyDlpITnkIbmnKror4bliKvnmoTlkb3ku6QNCiAgICAgICAgY29uc29sZS53YXJuKGDmnKrnn6XnmoTlkb3ku6Q6ICR7Y29tbWFuZH1gKTsNCiAgICB9DQogIH0NCg0KICAvKioNCiAgICogQXVkaW9Xb3JrbGV0UHJvY2Vzc29yIOeahOaguOW/g+WkhOeQhuaWueazlQ0KICAgKiDlnKjmr4/kuKrpn7PpopHlpITnkIblkajmnJ/osIPnlKjkuIDmrKENCiAgICogQXVkaW9Xb3JrbGV0UHJvY2Vzc29yIOWkhOeQhumfs+mikeaVsOaNrueahOWdl+Wkp+Wwj+WbuuWumuS4uiAxMjjluKfvvIzov5nkuKrlpKflsI/mmK/nlLEgV2ViIEF1ZGlvIEFQSSDop4TojIPlrprkuYnnmoTvvIzml6Dms5Xmm7TmlLnjgIINCiAgICog5q+P5bin55qE5pe26Ze06ZW/5bqm5Y+W5Yaz5LqO6Z+z6aKR5LiK5LiL5paH55qE6YeH5qC3546H44CC5L6L5aaC77yaDQogICAqICDlpoLmnpzph4fmoLfnjofmmK8gNDhrSHrvvIzliJnmr4/luKfnmoTml7bpl7TkuLogMS80ODAwMCDnp5LjgIINCiAgICogIDEyOOW4p+eahOaVsOaNruaMgee7reaXtumXtOaYryAxMjgvNDgwMDAg56eSIOKJiCAyLjY3bXPjgIINCiAgICogQHBhcmFtIGlucHV0cyAtIOi+k+WFpeeahOmfs+mikemAmumBk+aVsOaNrg0KICAgKiBAcGFyYW0gb3V0cHV0cyAtIOi+k+WHuueahOmfs+mikemAmumBk+aVsOaNrg0KICAgKiBAcGFyYW0gcGFyYW1ldGVycyAtIOWPr+iwg+WPguaVsA0KICAgKiBAcmV0dXJucyDov5Tlm54gdHJ1ZSDku6Xkv53mjIHlpITnkIblmajlpITkuo7mtLvliqjnirbmgIENCiAgICovDQogIHByb2Nlc3MoDQogICAgaW5wdXRzOiBGbG9hdDMyQXJyYXlbXVtdLA0KICAgIG91dHB1dHM6IEZsb2F0MzJBcnJheVtdW10sDQogICAgLy8gcGFyYW1ldGVyczogUmVjb3JkPHN0cmluZywgRmxvYXQzMkFycmF5Pg0KICApOiBib29sZWFuIHsNCiAgICBpZiAodGhpcy5yZWNvcmRpbmcpIHsNCiAgICAgIGNvbnN0IGlucHV0ID0gaW5wdXRzWzBdOyAvLyDovpPlhaXpn7PpopHmtYENCiAgICAgIGNvbnN0IG91dHB1dCA9IG91dHB1dHNbMF07IC8vIOi+k+WHuumfs+mikea1gQ0KICAgICAgLy8g5qOA5p+l5piv5ZCm5pyJ6L6T5YWl6Z+z6aKR5pWw5o2uDQogICAgICBpZiAoaW5wdXQubGVuZ3RoID4gMCkgew0KICAgICAgICAvLyDpgY3ljobmr4/kuKrlo7DpgZPnmoTmlbDmja7vvIzlubblpI3liLbkuIDku70NCiAgICAgICAgY29uc3QgY2hhbm5lbERhdGEgPSBpbnB1dC5tYXAoKGNoYW5uZWwpID0+IGNoYW5uZWwuc2xpY2UoKSk7DQogICAgICAgIC8vIOWwhuWkjeWItueahOmfs+mikeaVsOaNrumAmui/h+err+WPo+WPkemAgeWbnuS4u+e6v+eoiw0KICAgICAgICB0aGlzLnBvcnQucG9zdE1lc3NhZ2UoY2hhbm5lbERhdGEpOw0KICAgICAgfQ0KICAgIH0NCiAgICAvLyDov5Tlm54gdHJ1ZSDku6Xnu6fnu63lpITnkIbpn7PpopENCiAgICByZXR1cm4gdHJ1ZTsNCiAgfQ0KfQ0KDQovLyDms6jlhozlpITnkIblmajvvIzkvb/lhbblnKggQXVkaW9Xb3JrbGV0IOS4reWPr+eUqA0KcmVnaXN0ZXJQcm9jZXNzb3IoInJlY29yZGVyLXByb2Nlc3NvciIsIFJlY29yZGVyUHJvY2Vzc29yKTsNCg0KLy8g5Li6IFR5cGVTY3JpcHQg5aKe5Yqg5YWo5bGA57G75Z6L5aOw5piODQpkZWNsYXJlIGdsb2JhbCB7DQogIGludGVyZmFjZSBBdWRpb1dvcmtsZXRHbG9iYWxTY29wZSB7DQogICAgcmVnaXN0ZXJQcm9jZXNzb3IoDQogICAgICBuYW1lOiBzdHJpbmcsDQogICAgICBwcm9jZXNzb3JDdG9yOiB0eXBlb2YgUmVjb3JkZXJQcm9jZXNzb3INCiAgICApOiB2b2lkOw0KICB9DQp9DQoNCmRlY2xhcmUgY2xhc3MgQXVkaW9Xb3JrbGV0UHJvY2Vzc29yIHsNCiAgY29uc3RydWN0b3Iob3B0aW9ucz86IEF1ZGlvV29ya2xldE5vZGVPcHRpb25zKTsNCiAgcmVhZG9ubHkgcG9ydDogTWVzc2FnZVBvcnQ7DQogIHByb2Nlc3MoDQogICAgaW5wdXRzOiBGbG9hdDMyQXJyYXlbXVtdLA0KICAgIG91dHB1dHM6IEZsb2F0MzJBcnJheVtdW10sDQogICAgcGFyYW1ldGVyczogUmVjb3JkPHN0cmluZywgRmxvYXQzMkFycmF5Pg0KICApOiBib29sZWFuOw0KfQ0KDQpkZWNsYXJlIGZ1bmN0aW9uIHJlZ2lzdGVyUHJvY2Vzc29yKA0KICBuYW1lOiBzdHJpbmcsDQogIHByb2Nlc3NvckN0b3I6IHR5cGVvZiBBdWRpb1dvcmtsZXRQcm9jZXNzb3INCik6IHZvaWQ7DQo=", import.meta.url).href; await this.audioContext.audioWorklet.addModule(s), this.recorderNode = new AudioWorkletNode( this.audioContext, "recorder-processor", { processorOptions: { channels: this.config.channels, bufferSize: this.config.bufferSize } } ), this.recorderNode.port.onmessage = this.handleWorkletMessage.bind(this), i.connect(this.recorderNode), this.recorderNode.connect(this.audioContext.destination); } catch (s) { console.warn( "AudioWorklet 初始化失败,尝试使用 ScriptProcessorNode:", s ), this.setupScriptProcessorNode(i); } else this.setupScriptProcessorNode(i); } catch (t) { throw console.error("录音初始化失败:", t), t; } } /** * 设置 ScriptProcessorNode * ScriptProcessorNode 是 Web Audio API 提供的用于处理音频数据的节点,但已被弃用,推荐使用 AudioWorklet。 * @param source - 音频源节点 * @private */ setupScriptProcessorNode(t) { const e = this.audioContext.createScriptProcessor( this.config.bufferSize, // 缓冲区大小 this.config.channels, // 输入声道数 this.config.channels // 输出声道数 ); e.onaudioprocess = this.handleScriptProcessorProcess.bind(this), t.connect(e), e.connect(this.audioContext.destination), this.recorderNode = e; } /** * 处理来自 AudioWorklet 的消息 * 将音频数据保存到 audioData 数组中 * @param event - 消息事件对象 * @private */ handleWorkletMessage(t) { if (this.status !== r.RECORDING) return; const e = t.data, o = this.config.channels; o === 1 ? this.audioData.l.push(new Float32Array(e[0])) : o === 2 && (this.audioData.l.push(new Float32Array(e[0])), this.audioData.r.push(new Float32Array(e[1] || []))); const i = [ this.resampleSync( new Float32Array(e[0]), this.actualSampleRate, this.config.sampleRate ) ]; if (o === 2 && i.push( this.resampleSync( new Float32Array(e[1]), this.actualSampleRate, this.config.sampleRate ) ), this.onPCMDataCallback) { const s = this.interleaveChannels(i); this.onPCMDataCallback(s); } } /** * 处理来自 ScriptProcessorNode 的音频数据 * 将接收到的音频数据保存到 audioData 数组中,并进行重采样处理。 * @param event - 音频处理事件对象 * @private */ handleScriptProcessorProcess(t) { if (this.status !== r.RECORDING) return; const e = t.inputBuffer, o = e.numberOfChannels; for (let s = 0; s < o; s++) { const a = e.getChannelData(s); o === 1 ? this.audioData.l.push(new Float32Array(a)) : o === 2 && (s === 0 ? this.audioData.l.push(new Float32Array(a)) : s === 1 && this.audioData.r.push(new Float32Array(a))); } const i = [ this.resampleSync( new Float32Array(e.getChannelData(0)), this.actualSampleRate, this.config.sampleRate ) ]; if (o === 2 && i.push( this.resampleSync( new Float32Array(e.getChannelData(1)), this.actualSampleRate, this.config.sampleRate ) ), this.onPCMDataCallback) { const s = this.interleaveChannels(i); this.onPCMDataCallback(s); } } /** * 开始录音 * 如果当前状态不是录音中,则清空已有的音频数据,设置状态为录音中,并开始录音。 */ start() { this.status !== r.RECORDING && (this.audioData.l = [], this.audioData.r = [], this.status = r.RECORDING, this.recorderNode instanceof AudioWorkletNode && this.recorderNode.port.postMessage({ command: "start" })); } /** * 暂停录音 * 如果当前状态是录音中,则设置状态为暂停,并暂停录音。 */ pause() { this.status === r.RECORDING && this.recorderNode && (this.status = r.PAUSED, this.recorderNode instanceof AudioWorkletNode && this.recorderNode.port.postMessage({ command: "pause" })); } /** * 恢复录音 * 如果当前状态是暂停,则设置状态为录音中,并恢复录音。 */ resume() { this.status === r.PAUSED && this.recorderNode && (this.status = r.RECORDING, this.recorderNode instanceof AudioWorkletNode && this.recorderNode.port.postMessage({ command: "resume" })); } /** * 停止录音 * 设置状态为非活动状态,停止所有音频轨道,断开音频节点连接,关闭音频上下文。 */ stop() { this.status = r.INACTIVE, this.stream && this.stream.getTracks().forEach((t) => t.stop()), this.recorderNode && (this.recorderNode instanceof AudioWorkletNode && this.recorderNode.port.postMessage({ command: "stop" }), this.recorderNode.disconnect()), this.audioContext && this.audioContext.close(); } /** * 设置实时 PCM 数据的回调函数。 * 当有新的 PCM 数据时,会调用该回调函数。 * @param callback - 接收 PCM 数据的回调函数 */ setPCMDataCallback(t) { this.onPCMDataCallback = t; } /** * 导出 PCM 数据 * @returns { Uint8Array | int16Array} PCM 格式的音频数据 */ exportPCM() { if (this.status === r.RECORDING) throw new Error("录音进行中,无法导出PCM数据。请先停止录音。"); const t = this.mergeAudioData(); let e; if (this.actualSampleRate !== this.config.sampleRate ? e = this.resampleSync( t, this.actualSampleRate, this.config.sampleRate ) : e = t, this.config.sampleBits === 8) { const o = new Uint8Array(e.length); for (let i = 0; i < e.length; i++) o[i] = Math.max( 0, Math.min(255, Math.floor((e[i] + 1) * 127.5)) ); return o; } else { const o = new Int16Array(e.length); for (let i = 0; i < e.length; i++) o[i] = Math.max( -32768, Math.min(32767, Math.floor(e[i] * 32767)) ); return o; } } /** * 重采样可以异步可以同步并且有多种算法 * 线性插值:简单且计算量小,但在某些情况下可能导致音质下降或失真。 * 高阶插值(如立方插值):提供更平滑的结果,音质更高,但计算量较大。 * 窗函数法(如 FIR 滤波器):能够有效减少混叠和频谱泄漏,适合高质量音频处理。 * 多相滤波器:适用于高效的多倍频率转换,常用于专业音频处理。 * 这里使用线性插值进行同步重采样 * @param inputData - 输入的 Float32Array 音频数据 * @param inputSampleRate - 输入采样率 * @param outputSampleRate - 输出采样率 * @returns {Float32Array} 重采样后的音频数据 * @private */ resampleSync(t, e, o) { if (e === o) return t; const i = e / o, s = Math.floor(t.length / i), a = new Float32Array(s); for (let n = 0; n < s; n++) { const I = n * i, g = Math.floor(I), h = I - g; g + 1 < t.length ? a[n] = t[g] * (1 - h) + t[g + 1] * h : a[n] = t[g]; } return a; } /** * 合并音频数据。 * 将所有录音片段合并为一个连续的 Float32Array。 * @returns {Float32Array} 合并后的音频数据 * @private */ mergeAudioData() { const t = this.mergeFloat32Arrays(this.audioData.l); let e = null; if (this.config.channels === 2 && (e = this.mergeFloat32Arrays(this.audioData.r)), this.config.channels === 1) return t; if (this.config.channels === 2 && e) return this.interleaveChannels([t, e]); throw new Error("不支持的声道数"); } /** * 合并多个 Float32Array 为一个 * @param arrays - 需要合并的 Float32Array 数组 * @returns {Float32Array} 合并后的 Float32Array * @private */ mergeFloat32Arrays(t) { const e = t.reduce((s, a) => s + a.length, 0), o = new Float32Array(e); let i = 0; return t.forEach((s) => { o.set(s, i), i += s.length; }), o; } /** * 交错多个声道的数据 交换逻辑 L1 R1 L2 R2 * @param channelsData - 每个声道的 Float32Array * @returns {Float32Array} 交错后的音频数据 * @private */ interleaveChannels(t) { const e = t.length, o = t.reduce( (s, a) => Math.max(s, a.length), 0 ), i = new Float32Array(o * e); for (let s = 0; s < o; s++) for (let a = 0; a < e; a++) i[s * e + a] = t[a][s] || 0; return i; } /** * 导出 WAV 格式音频文件。 * 先导出 PCM 数据,然后将其封装为 WAV 格式的 Blob 对象。 * @returns {Blob} WAV 格式的 Blob 对象 * @throws {Error} 如果录音正在进行中,抛出错误 */ exportWAV() { if (this.status === r.RECORDING) throw new Error("录音进行中,无法导出WAV文件。请先停止录音。"); const t = this.exportPCM(), e = this.createWavFile(t); return new Blob([e], { type: "audio/wav" }); } /** * 创建 WAV 文件。 * 根据 PCM 数据和录音配置,生成包含 WAV 头部信息的 ArrayBuffer。 * WAV 文件包含 RIFF 头、fmt 子块和 data 子块。 * @param pcmData - PCM 音频数据,可以是 Uint8Array 或 Int16Array * @returns {ArrayBuffer} 包含 WAV 头部信息的音频数据 * @private */ createWavFile(t) { const e = t.length * (this.config.sampleBits / 8), o = new ArrayBuffer(44 + e), i = new DataView(o); this.writeString(i, 0, "RIFF"), i.setUint32(4, 36 + e, !0), this.writeString(i, 8, "WAVE"), this.writeString(i, 12, "fmt "), i.setUint32(16, 16, !0), i.setUint16(20, 1, !0), i.setUint16(22, this.config.channels, !0), i.setUint32(24, this.config.sampleRate, !0), i.setUint32( 28, this.config.sampleRate * this.config.channels * (this.config.sampleBits / 8), !0 ), i.setUint16( 32, this.config.channels * (this.config.sampleBits / 8), !0 ), i.setUint16(34, this.config.sampleBits, !0), this.writeString(i, 36, "data"), i.setUint32(40, e, !0); const s = 44; if (this.config.sampleBits === 8) { const a = t; for (let n = 0; n < a.length; n++) i.setUint8(s + n, a[n]); } else { const a = t; for (let n = 0; n < a.length; n++) i.setInt16(s + n * 2, a[n], !0); } return o; } /** * 将字符串写入 DataView * 用于在 WAV 文件头部写入标识符,如 "RIFF"、"WAVE" 等。 * @param view - DataView 对象 * @param offset - 写入位置的偏移量 * @param string - 要写入的字符串 * @private */ writeString(t, e, o) { for (let i = 0; i < o.length; i++) t.setUint8(e + i, o.charCodeAt(i)); } } export { d as Recorder };