a2bei4-utils 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -2
  3. package/dist/a2bei4.utils.cjs.js +1051 -250
  4. package/dist/a2bei4.utils.cjs.js.map +1 -1
  5. package/dist/a2bei4.utils.cjs.min.js +1 -1
  6. package/dist/a2bei4.utils.cjs.min.js.map +1 -1
  7. package/dist/a2bei4.utils.esm.js +1047 -251
  8. package/dist/a2bei4.utils.esm.js.map +1 -1
  9. package/dist/a2bei4.utils.esm.min.js +1 -1
  10. package/dist/a2bei4.utils.esm.min.js.map +1 -1
  11. package/dist/a2bei4.utils.umd.js +1051 -250
  12. package/dist/a2bei4.utils.umd.js.map +1 -1
  13. package/dist/a2bei4.utils.umd.min.js +1 -1
  14. package/dist/a2bei4.utils.umd.min.js.map +1 -1
  15. package/dist/arr.cjs +27 -27
  16. package/dist/arr.cjs.map +1 -1
  17. package/dist/arr.js +27 -27
  18. package/dist/arr.js.map +1 -1
  19. package/dist/audio.cjs +281 -0
  20. package/dist/audio.cjs.map +1 -0
  21. package/dist/audio.js +278 -0
  22. package/dist/audio.js.map +1 -0
  23. package/dist/common.cjs +6 -6
  24. package/dist/common.cjs.map +1 -1
  25. package/dist/common.js +6 -6
  26. package/dist/common.js.map +1 -1
  27. package/dist/download.cjs +43 -0
  28. package/dist/download.cjs.map +1 -1
  29. package/dist/download.js +43 -1
  30. package/dist/download.js.map +1 -1
  31. package/dist/evt.cjs +148 -148
  32. package/dist/evt.cjs.map +1 -1
  33. package/dist/evt.js +148 -148
  34. package/dist/evt.js.map +1 -1
  35. package/dist/id.cjs +68 -68
  36. package/dist/id.cjs.map +1 -1
  37. package/dist/id.js +68 -68
  38. package/dist/id.js.map +1 -1
  39. package/dist/timer.cjs +0 -1
  40. package/dist/timer.cjs.map +1 -1
  41. package/dist/timer.js +0 -1
  42. package/dist/timer.js.map +1 -1
  43. package/dist/tree.cjs +75 -0
  44. package/dist/tree.cjs.map +1 -1
  45. package/dist/tree.js +75 -1
  46. package/dist/tree.js.map +1 -1
  47. package/dist/webSocket.cjs +409 -0
  48. package/dist/webSocket.cjs.map +1 -0
  49. package/dist/webSocket.js +407 -0
  50. package/dist/webSocket.js.map +1 -0
  51. package/package.json +11 -1
  52. package/readme.txt +8 -5
  53. package/types/audio.d.ts +57 -0
  54. package/types/download.d.ts +12 -1
  55. package/types/index.d.ts +207 -1
  56. package/types/tree.d.ts +17 -1
  57. package/types/webSocket.d.ts +124 -0
package/dist/audio.cjs ADDED
@@ -0,0 +1,281 @@
1
+ 'use strict';
2
+
3
+ const AudioStreamResamplerProcessorCode = `
4
+ class AudioStreamResamplerProcessor extends AudioWorkletProcessor {
5
+ constructor(options) {
6
+ super();
7
+ const config = options.processorOptions || {};
8
+ this.targetSampleRate = config.targetSampleRate || 16000;
9
+ this.sourceSampleRate = sampleRate;
10
+ this.downsampleRatio = this.sourceSampleRate / this.targetSampleRate;
11
+
12
+ // 16000Hz 用 60ms chunk (960 samples),其他用 1024
13
+ this.chunkSize = this.targetSampleRate === 16000 ? 960 : 1024;
14
+
15
+ const sourceBufferSize = config.sourceBufferSize || 16384; // ~340ms @48kHz
16
+ const pcmBufferSize = config.pcmBufferSize || (this.chunkSize * 10); // 更大缓冲,减少溢出概率
17
+
18
+ this.sourceBuffer = new Float32Array(sourceBufferSize);
19
+ this.sourceBufferLength = 0;
20
+
21
+ this.pcmBuffer = new Int16Array(pcmBufferSize);
22
+ this.pcmBufferIndex = 0;
23
+ }
24
+
25
+ process(inputs, outputs, parameters) {
26
+ const input = inputs[0];
27
+ if (!input || input.length === 0 || input[0].length === 0) return true;
28
+ const inputChannel = input[0];
29
+
30
+ // 1. 写入源缓冲区(溢出时覆盖最旧数据)
31
+ const newLength = this.sourceBufferLength + inputChannel.length;
32
+ if (newLength > this.sourceBuffer.length) {
33
+ const overflow = newLength - this.sourceBuffer.length;
34
+ this.sourceBuffer.copyWithin(0, overflow);
35
+ this.sourceBufferLength = this.sourceBuffer.length - inputChannel.length;
36
+ }
37
+ this.sourceBuffer.set(inputChannel, this.sourceBufferLength);
38
+ this.sourceBufferLength += inputChannel.length;
39
+
40
+ // 2. 计算可降采样样本数
41
+ const availableOutputSamples = Math.floor(this.sourceBufferLength / this.downsampleRatio);
42
+ if (availableOutputSamples === 0) return true;
43
+
44
+ // 3. 线性插值降采样
45
+ const downsampled = new Float32Array(availableOutputSamples);
46
+ for (let i = 0; i < availableOutputSamples; i++) {
47
+ const srcIndex = i * this.downsampleRatio;
48
+ const srcIndexInt = Math.floor(srcIndex);
49
+ const fraction = srcIndex - srcIndexInt;
50
+ const val0 = this.sourceBuffer[srcIndexInt];
51
+ const val1 = srcIndexInt + 1 < this.sourceBufferLength ? this.sourceBuffer[srcIndexInt + 1] : val0;
52
+ downsampled[i] = val0 + (val1 - val0) * fraction;
53
+ }
54
+
55
+ // 4. Float32 → Int16 PCM
56
+ const pcmData = this.floatTo16BitPCM(downsampled);
57
+
58
+ // 5. 写入 PCM 缓冲区(空间不足时直接覆盖最旧,缓冲区足够大基本不会触发)
59
+ if (this.pcmBufferIndex + pcmData.length > this.pcmBuffer.length) {
60
+ // 简单策略:从头覆盖(丢弃最旧数据)
61
+ this.pcmBufferIndex = 0;
62
+ }
63
+ this.pcmBuffer.set(pcmData, this.pcmBufferIndex);
64
+ this.pcmBufferIndex += pcmData.length;
65
+
66
+ // 6. 发送所有完整的 chunk(关键:复制到新数组再转移)
67
+ while (this.pcmBufferIndex >= this.chunkSize) {
68
+ // 方法1:推荐,使用 slice(隐式复制到新 ArrayBuffer)
69
+ const chunk = this.pcmBuffer.slice(0, this.chunkSize);
70
+
71
+ // 方法2:等价写法
72
+ // const chunk = new Int16Array(this.pcmBuffer.subarray(0, this.chunkSize));
73
+
74
+ this.port.postMessage(chunk, [chunk.buffer]); // 转移新缓冲区,安全!
75
+
76
+ // 移动剩余数据到开头
77
+ this.pcmBuffer.copyWithin(0, this.chunkSize, this.pcmBufferIndex);
78
+ this.pcmBufferIndex -= this.chunkSize;
79
+ }
80
+
81
+ // 7. 清理已消费的源数据
82
+ const consumedSrc = Math.floor(availableOutputSamples * this.downsampleRatio + 0.5); // 四舍五入更准
83
+ if (consumedSrc > 0) {
84
+ this.sourceBuffer.copyWithin(0, consumedSrc, this.sourceBufferLength);
85
+ this.sourceBufferLength -= consumedSrc;
86
+ }
87
+
88
+ return true;
89
+ }
90
+
91
+ floatTo16BitPCM(input) {
92
+ const output = new Int16Array(input.length);
93
+ for (let i = 0; i < input.length; i++) {
94
+ const s = Math.max(-1, Math.min(1, input[i]));
95
+ output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
96
+ }
97
+ return output;
98
+ }
99
+ }
100
+
101
+ registerProcessor('audio-stream-resampler-processor', AudioStreamResamplerProcessor);
102
+ `;
103
+
104
+ /**
105
+ * 浏览器端实时音频流重采样器。
106
+ * 基于 AudioWorklet 将麦克风/媒体流转换为 16 kHz、16-bit、单声道 PCM,
107
+ * 并通过回调逐块输出,可选保存完整 PCM 用于后续合并。
108
+ */
109
+ class AudioStreamResampler {
110
+ /**
111
+ * @param {object} config
112
+ * @param {function(Int16Array)} config.onData - 收到一个 chunk PCM 数据的回调
113
+ * @param {function(string, string)} [config.onStateChange] - 状态变化回调
114
+ * @param {object} [config.processorOptions] - 传递给 AudioWorklet 的选项
115
+ * @param {boolean} [config.saveFullPcm=false] - 是否在内部保存所有 PCM 用于 stop 时合并(长时间录音建议关闭)
116
+ */
117
+ constructor(config) {
118
+ this.onData = config.onData || (() => {});
119
+ this.onStateChange = config.onStateChange || (() => {});
120
+ this.processorOptions = config.processorOptions || {};
121
+ this.saveFullPcm = config.saveFullPcm ?? false;
122
+
123
+ this.audioContext = null;
124
+ this.workletNode = null;
125
+ this.source = null;
126
+ this.workletUrl = null;
127
+ this.fullPcmData = this.saveFullPcm ? [] : null;
128
+
129
+ this.isInitialized = false;
130
+ this.isProcessing = false;
131
+ }
132
+
133
+ /**
134
+ * 初始化 AudioContext 并加载 AudioWorklet。
135
+ * 完成后状态变为 `"ready"`。
136
+ */
137
+ async init() {
138
+ this.onStateChange("initializing", "正在初始化音频环境...");
139
+ try {
140
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
141
+
142
+ const blob = new Blob([AudioStreamResamplerProcessorCode], { type: "application/javascript" });
143
+ this.workletUrl = URL.createObjectURL(blob);
144
+ await this.audioContext.audioWorklet.addModule(this.workletUrl);
145
+
146
+ this.workletNode = new AudioWorkletNode(this.audioContext, "audio-stream-resampler-processor", {
147
+ processorOptions: this.processorOptions
148
+ });
149
+
150
+ this.workletNode.port.onmessage = (event) => {
151
+ const chunk = event.data; // Int16Array
152
+ this.onData(chunk);
153
+ if (this.saveFullPcm) {
154
+ this.fullPcmData.push(chunk);
155
+ }
156
+ };
157
+
158
+ this.isInitialized = true;
159
+ this.onStateChange("ready", "音频环境已就绪");
160
+ } catch (err) {
161
+ console.error("AudioStreamResampler init error:", err);
162
+ this.onStateChange("error", `初始化失败: ${err.message}`);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * 绑定媒体流,开始实时处理。
168
+ * @param {MediaStream} stream - 通过 getUserMedia 或其他方式获得的流
169
+ */
170
+ setMediaStream(stream) {
171
+ if (!this.isInitialized) {
172
+ console.error("请先调用 init()");
173
+ return;
174
+ }
175
+
176
+ if (this.source) {
177
+ this.source.disconnect();
178
+ }
179
+
180
+ this.source = this.audioContext.createMediaStreamSource(stream);
181
+ this.source.connect(this.workletNode);
182
+ // workletNode 默认连接到 destination,可选断开以静音
183
+ // this.workletNode.connect(this.audioContext.destination);
184
+
185
+ this.isProcessing = true;
186
+ this.onStateChange("processing", "正在处理音频流...");
187
+ }
188
+
189
+ /**
190
+ * 停止处理并释放资源。
191
+ * @param {(fullPcm: Int16Array) => void} [callback] - 若构造时 `saveFullPcm=true`,会把合并后的完整 PCM 通过此回调传出
192
+ */
193
+ stop(callback) {
194
+ if (!this.isProcessing) return;
195
+
196
+ this.onStateChange("stopping", "正在停止...");
197
+
198
+ if (this.source) {
199
+ this.source.disconnect();
200
+ this.source = null;
201
+ }
202
+
203
+ this._cleanup();
204
+
205
+ this.onStateChange("stopped", "已停止");
206
+
207
+ if (this.saveFullPcm && callback && typeof callback === "function") {
208
+ const totalLength = this.fullPcmData.reduce((sum, chunk) => sum + chunk.length, 0);
209
+ const combined = new Int16Array(totalLength);
210
+ let offset = 0;
211
+ for (const chunk of this.fullPcmData) {
212
+ combined.set(chunk, offset);
213
+ offset += chunk.length;
214
+ }
215
+ callback(combined);
216
+ }
217
+ }
218
+
219
+ _cleanup() {
220
+ if (this.workletNode) {
221
+ this.workletNode.disconnect();
222
+ this.workletNode.port.close();
223
+ this.workletNode = null;
224
+ }
225
+ if (this.audioContext && this.audioContext.state !== "closed") {
226
+ this.audioContext.close();
227
+ this.audioContext = null;
228
+ }
229
+ if (this.workletUrl) {
230
+ URL.revokeObjectURL(this.workletUrl);
231
+ this.workletUrl = null;
232
+ }
233
+
234
+ this.isProcessing = false;
235
+ this.isInitialized = false;
236
+ if (this.fullPcmData) {
237
+ this.fullPcmData.length = 0;
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * 将 16-bit 单声道 PCM 数据封装成标准 WAV Blob。
244
+ *
245
+ * @param {Int16Array} pcmData - PCM 采样数据
246
+ * @param {number} [sampleRate=16000] - 采样率,默认 16 kHz
247
+ * @returns {Blob} audio/wav Blob
248
+ */
249
+ function pcmToWavBlob(pcmData, sampleRate = 16000) {
250
+ const length = pcmData.length;
251
+ const buffer = new ArrayBuffer(44 + length * 2);
252
+ const view = new DataView(buffer);
253
+ const writeString = (offset, string) => {
254
+ for (let i = 0; i < string.length; i++) {
255
+ view.setUint8(offset + i, string.charCodeAt(i));
256
+ }
257
+ };
258
+ writeString(0, "RIFF");
259
+ view.setUint32(4, 36 + length * 2, true);
260
+ writeString(8, "WAVE");
261
+ writeString(12, "fmt ");
262
+ view.setUint32(16, 16, true);
263
+ view.setUint16(20, 1, true);
264
+ view.setUint16(22, 1, true);
265
+ view.setUint32(24, sampleRate, true);
266
+ view.setUint32(28, sampleRate * 2, true);
267
+ view.setUint16(32, 2, true);
268
+ view.setUint16(34, 16, true);
269
+ writeString(36, "data");
270
+ view.setUint32(40, length * 2, true);
271
+ let offset = 44;
272
+ for (let i = 0; i < length; i++) {
273
+ view.setInt16(offset, pcmData[i], true);
274
+ offset += 2;
275
+ }
276
+ return new Blob([buffer], { type: "audio/wav" });
277
+ }
278
+
279
+ exports.AudioStreamResampler = AudioStreamResampler;
280
+ exports.pcmToWavBlob = pcmToWavBlob;
281
+ //# sourceMappingURL=audio.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.cjs","sources":["../src/source/audio.js"],"sourcesContent":["const AudioStreamResamplerProcessorCode = `\nclass AudioStreamResamplerProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n const config = options.processorOptions || {};\n this.targetSampleRate = config.targetSampleRate || 16000;\n this.sourceSampleRate = sampleRate;\n this.downsampleRatio = this.sourceSampleRate / this.targetSampleRate;\n\n // 16000Hz 用 60ms chunk (960 samples),其他用 1024\n this.chunkSize = this.targetSampleRate === 16000 ? 960 : 1024;\n\n const sourceBufferSize = config.sourceBufferSize || 16384; // ~340ms @48kHz\n const pcmBufferSize = config.pcmBufferSize || (this.chunkSize * 10); // 更大缓冲,减少溢出概率\n\n this.sourceBuffer = new Float32Array(sourceBufferSize);\n this.sourceBufferLength = 0;\n\n this.pcmBuffer = new Int16Array(pcmBufferSize);\n this.pcmBufferIndex = 0;\n }\n\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n if (!input || input.length === 0 || input[0].length === 0) return true;\n const inputChannel = input[0];\n\n // 1. 写入源缓冲区(溢出时覆盖最旧数据)\n const newLength = this.sourceBufferLength + inputChannel.length;\n if (newLength > this.sourceBuffer.length) {\n const overflow = newLength - this.sourceBuffer.length;\n this.sourceBuffer.copyWithin(0, overflow);\n this.sourceBufferLength = this.sourceBuffer.length - inputChannel.length;\n }\n this.sourceBuffer.set(inputChannel, this.sourceBufferLength);\n this.sourceBufferLength += inputChannel.length;\n\n // 2. 计算可降采样样本数\n const availableOutputSamples = Math.floor(this.sourceBufferLength / this.downsampleRatio);\n if (availableOutputSamples === 0) return true;\n\n // 3. 线性插值降采样\n const downsampled = new Float32Array(availableOutputSamples);\n for (let i = 0; i < availableOutputSamples; i++) {\n const srcIndex = i * this.downsampleRatio;\n const srcIndexInt = Math.floor(srcIndex);\n const fraction = srcIndex - srcIndexInt;\n const val0 = this.sourceBuffer[srcIndexInt];\n const val1 = srcIndexInt + 1 < this.sourceBufferLength ? this.sourceBuffer[srcIndexInt + 1] : val0;\n downsampled[i] = val0 + (val1 - val0) * fraction;\n }\n\n // 4. Float32 → Int16 PCM\n const pcmData = this.floatTo16BitPCM(downsampled);\n\n // 5. 写入 PCM 缓冲区(空间不足时直接覆盖最旧,缓冲区足够大基本不会触发)\n if (this.pcmBufferIndex + pcmData.length > this.pcmBuffer.length) {\n // 简单策略:从头覆盖(丢弃最旧数据)\n this.pcmBufferIndex = 0;\n }\n this.pcmBuffer.set(pcmData, this.pcmBufferIndex);\n this.pcmBufferIndex += pcmData.length;\n\n // 6. 发送所有完整的 chunk(关键:复制到新数组再转移)\n while (this.pcmBufferIndex >= this.chunkSize) {\n // 方法1:推荐,使用 slice(隐式复制到新 ArrayBuffer)\n const chunk = this.pcmBuffer.slice(0, this.chunkSize);\n\n // 方法2:等价写法\n // const chunk = new Int16Array(this.pcmBuffer.subarray(0, this.chunkSize));\n\n this.port.postMessage(chunk, [chunk.buffer]); // 转移新缓冲区,安全!\n\n // 移动剩余数据到开头\n this.pcmBuffer.copyWithin(0, this.chunkSize, this.pcmBufferIndex);\n this.pcmBufferIndex -= this.chunkSize;\n }\n\n // 7. 清理已消费的源数据\n const consumedSrc = Math.floor(availableOutputSamples * this.downsampleRatio + 0.5); // 四舍五入更准\n if (consumedSrc > 0) {\n this.sourceBuffer.copyWithin(0, consumedSrc, this.sourceBufferLength);\n this.sourceBufferLength -= consumedSrc;\n }\n\n return true;\n }\n\n floatTo16BitPCM(input) {\n const output = new Int16Array(input.length);\n for (let i = 0; i < input.length; i++) {\n const s = Math.max(-1, Math.min(1, input[i]));\n output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;\n }\n return output;\n }\n}\n\nregisterProcessor('audio-stream-resampler-processor', AudioStreamResamplerProcessor);\n`;\n\n/**\n * 浏览器端实时音频流重采样器。\n * 基于 AudioWorklet 将麦克风/媒体流转换为 16 kHz、16-bit、单声道 PCM,\n * 并通过回调逐块输出,可选保存完整 PCM 用于后续合并。\n */\nexport class AudioStreamResampler {\n /**\n * @param {object} config\n * @param {function(Int16Array)} config.onData - 收到一个 chunk PCM 数据的回调\n * @param {function(string, string)} [config.onStateChange] - 状态变化回调\n * @param {object} [config.processorOptions] - 传递给 AudioWorklet 的选项\n * @param {boolean} [config.saveFullPcm=false] - 是否在内部保存所有 PCM 用于 stop 时合并(长时间录音建议关闭)\n */\n constructor(config) {\n this.onData = config.onData || (() => {});\n this.onStateChange = config.onStateChange || (() => {});\n this.processorOptions = config.processorOptions || {};\n this.saveFullPcm = config.saveFullPcm ?? false;\n\n this.audioContext = null;\n this.workletNode = null;\n this.source = null;\n this.workletUrl = null;\n this.fullPcmData = this.saveFullPcm ? [] : null;\n\n this.isInitialized = false;\n this.isProcessing = false;\n }\n\n /**\n * 初始化 AudioContext 并加载 AudioWorklet。\n * 完成后状态变为 `\"ready\"`。\n */\n async init() {\n this.onStateChange(\"initializing\", \"正在初始化音频环境...\");\n try {\n this.audioContext = new (window.AudioContext || window.webkitAudioContext)();\n\n const blob = new Blob([AudioStreamResamplerProcessorCode], { type: \"application/javascript\" });\n this.workletUrl = URL.createObjectURL(blob);\n await this.audioContext.audioWorklet.addModule(this.workletUrl);\n\n this.workletNode = new AudioWorkletNode(this.audioContext, \"audio-stream-resampler-processor\", {\n processorOptions: this.processorOptions\n });\n\n this.workletNode.port.onmessage = (event) => {\n const chunk = event.data; // Int16Array\n this.onData(chunk);\n if (this.saveFullPcm) {\n this.fullPcmData.push(chunk);\n }\n };\n\n this.isInitialized = true;\n this.onStateChange(\"ready\", \"音频环境已就绪\");\n } catch (err) {\n console.error(\"AudioStreamResampler init error:\", err);\n this.onStateChange(\"error\", `初始化失败: ${err.message}`);\n }\n }\n\n /**\n * 绑定媒体流,开始实时处理。\n * @param {MediaStream} stream - 通过 getUserMedia 或其他方式获得的流\n */\n setMediaStream(stream) {\n if (!this.isInitialized) {\n console.error(\"请先调用 init()\");\n return;\n }\n\n if (this.source) {\n this.source.disconnect();\n }\n\n this.source = this.audioContext.createMediaStreamSource(stream);\n this.source.connect(this.workletNode);\n // workletNode 默认连接到 destination,可选断开以静音\n // this.workletNode.connect(this.audioContext.destination);\n\n this.isProcessing = true;\n this.onStateChange(\"processing\", \"正在处理音频流...\");\n }\n\n /**\n * 停止处理并释放资源。\n * @param {(fullPcm: Int16Array) => void} [callback] - 若构造时 `saveFullPcm=true`,会把合并后的完整 PCM 通过此回调传出\n */\n stop(callback) {\n if (!this.isProcessing) return;\n\n this.onStateChange(\"stopping\", \"正在停止...\");\n\n if (this.source) {\n this.source.disconnect();\n this.source = null;\n }\n\n this._cleanup();\n\n this.onStateChange(\"stopped\", \"已停止\");\n\n if (this.saveFullPcm && callback && typeof callback === \"function\") {\n const totalLength = this.fullPcmData.reduce((sum, chunk) => sum + chunk.length, 0);\n const combined = new Int16Array(totalLength);\n let offset = 0;\n for (const chunk of this.fullPcmData) {\n combined.set(chunk, offset);\n offset += chunk.length;\n }\n callback(combined);\n }\n }\n\n _cleanup() {\n if (this.workletNode) {\n this.workletNode.disconnect();\n this.workletNode.port.close();\n this.workletNode = null;\n }\n if (this.audioContext && this.audioContext.state !== \"closed\") {\n this.audioContext.close();\n this.audioContext = null;\n }\n if (this.workletUrl) {\n URL.revokeObjectURL(this.workletUrl);\n this.workletUrl = null;\n }\n\n this.isProcessing = false;\n this.isInitialized = false;\n if (this.fullPcmData) {\n this.fullPcmData.length = 0;\n }\n }\n}\n\n/**\n * 将 16-bit 单声道 PCM 数据封装成标准 WAV Blob。\n *\n * @param {Int16Array} pcmData - PCM 采样数据\n * @param {number} [sampleRate=16000] - 采样率,默认 16 kHz\n * @returns {Blob} audio/wav Blob\n */\nexport function pcmToWavBlob(pcmData, sampleRate = 16000) {\n const length = pcmData.length;\n const buffer = new ArrayBuffer(44 + length * 2);\n const view = new DataView(buffer);\n const writeString = (offset, string) => {\n for (let i = 0; i < string.length; i++) {\n view.setUint8(offset + i, string.charCodeAt(i));\n }\n };\n writeString(0, \"RIFF\");\n view.setUint32(4, 36 + length * 2, true);\n writeString(8, \"WAVE\");\n writeString(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, 1, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * 2, true);\n view.setUint16(32, 2, true);\n view.setUint16(34, 16, true);\n writeString(36, \"data\");\n view.setUint32(40, length * 2, true);\n let offset = 44;\n for (let i = 0; i < length; i++) {\n view.setInt16(offset, pcmData[i], true);\n offset += 2;\n }\n return new Blob([buffer], { type: \"audio/wav\" });\n}\n"],"names":[],"mappings":"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"}
package/dist/audio.js ADDED
@@ -0,0 +1,278 @@
1
+ const AudioStreamResamplerProcessorCode = `
2
+ class AudioStreamResamplerProcessor extends AudioWorkletProcessor {
3
+ constructor(options) {
4
+ super();
5
+ const config = options.processorOptions || {};
6
+ this.targetSampleRate = config.targetSampleRate || 16000;
7
+ this.sourceSampleRate = sampleRate;
8
+ this.downsampleRatio = this.sourceSampleRate / this.targetSampleRate;
9
+
10
+ // 16000Hz 用 60ms chunk (960 samples),其他用 1024
11
+ this.chunkSize = this.targetSampleRate === 16000 ? 960 : 1024;
12
+
13
+ const sourceBufferSize = config.sourceBufferSize || 16384; // ~340ms @48kHz
14
+ const pcmBufferSize = config.pcmBufferSize || (this.chunkSize * 10); // 更大缓冲,减少溢出概率
15
+
16
+ this.sourceBuffer = new Float32Array(sourceBufferSize);
17
+ this.sourceBufferLength = 0;
18
+
19
+ this.pcmBuffer = new Int16Array(pcmBufferSize);
20
+ this.pcmBufferIndex = 0;
21
+ }
22
+
23
+ process(inputs, outputs, parameters) {
24
+ const input = inputs[0];
25
+ if (!input || input.length === 0 || input[0].length === 0) return true;
26
+ const inputChannel = input[0];
27
+
28
+ // 1. 写入源缓冲区(溢出时覆盖最旧数据)
29
+ const newLength = this.sourceBufferLength + inputChannel.length;
30
+ if (newLength > this.sourceBuffer.length) {
31
+ const overflow = newLength - this.sourceBuffer.length;
32
+ this.sourceBuffer.copyWithin(0, overflow);
33
+ this.sourceBufferLength = this.sourceBuffer.length - inputChannel.length;
34
+ }
35
+ this.sourceBuffer.set(inputChannel, this.sourceBufferLength);
36
+ this.sourceBufferLength += inputChannel.length;
37
+
38
+ // 2. 计算可降采样样本数
39
+ const availableOutputSamples = Math.floor(this.sourceBufferLength / this.downsampleRatio);
40
+ if (availableOutputSamples === 0) return true;
41
+
42
+ // 3. 线性插值降采样
43
+ const downsampled = new Float32Array(availableOutputSamples);
44
+ for (let i = 0; i < availableOutputSamples; i++) {
45
+ const srcIndex = i * this.downsampleRatio;
46
+ const srcIndexInt = Math.floor(srcIndex);
47
+ const fraction = srcIndex - srcIndexInt;
48
+ const val0 = this.sourceBuffer[srcIndexInt];
49
+ const val1 = srcIndexInt + 1 < this.sourceBufferLength ? this.sourceBuffer[srcIndexInt + 1] : val0;
50
+ downsampled[i] = val0 + (val1 - val0) * fraction;
51
+ }
52
+
53
+ // 4. Float32 → Int16 PCM
54
+ const pcmData = this.floatTo16BitPCM(downsampled);
55
+
56
+ // 5. 写入 PCM 缓冲区(空间不足时直接覆盖最旧,缓冲区足够大基本不会触发)
57
+ if (this.pcmBufferIndex + pcmData.length > this.pcmBuffer.length) {
58
+ // 简单策略:从头覆盖(丢弃最旧数据)
59
+ this.pcmBufferIndex = 0;
60
+ }
61
+ this.pcmBuffer.set(pcmData, this.pcmBufferIndex);
62
+ this.pcmBufferIndex += pcmData.length;
63
+
64
+ // 6. 发送所有完整的 chunk(关键:复制到新数组再转移)
65
+ while (this.pcmBufferIndex >= this.chunkSize) {
66
+ // 方法1:推荐,使用 slice(隐式复制到新 ArrayBuffer)
67
+ const chunk = this.pcmBuffer.slice(0, this.chunkSize);
68
+
69
+ // 方法2:等价写法
70
+ // const chunk = new Int16Array(this.pcmBuffer.subarray(0, this.chunkSize));
71
+
72
+ this.port.postMessage(chunk, [chunk.buffer]); // 转移新缓冲区,安全!
73
+
74
+ // 移动剩余数据到开头
75
+ this.pcmBuffer.copyWithin(0, this.chunkSize, this.pcmBufferIndex);
76
+ this.pcmBufferIndex -= this.chunkSize;
77
+ }
78
+
79
+ // 7. 清理已消费的源数据
80
+ const consumedSrc = Math.floor(availableOutputSamples * this.downsampleRatio + 0.5); // 四舍五入更准
81
+ if (consumedSrc > 0) {
82
+ this.sourceBuffer.copyWithin(0, consumedSrc, this.sourceBufferLength);
83
+ this.sourceBufferLength -= consumedSrc;
84
+ }
85
+
86
+ return true;
87
+ }
88
+
89
+ floatTo16BitPCM(input) {
90
+ const output = new Int16Array(input.length);
91
+ for (let i = 0; i < input.length; i++) {
92
+ const s = Math.max(-1, Math.min(1, input[i]));
93
+ output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
94
+ }
95
+ return output;
96
+ }
97
+ }
98
+
99
+ registerProcessor('audio-stream-resampler-processor', AudioStreamResamplerProcessor);
100
+ `;
101
+
102
+ /**
103
+ * 浏览器端实时音频流重采样器。
104
+ * 基于 AudioWorklet 将麦克风/媒体流转换为 16 kHz、16-bit、单声道 PCM,
105
+ * 并通过回调逐块输出,可选保存完整 PCM 用于后续合并。
106
+ */
107
+ class AudioStreamResampler {
108
+ /**
109
+ * @param {object} config
110
+ * @param {function(Int16Array)} config.onData - 收到一个 chunk PCM 数据的回调
111
+ * @param {function(string, string)} [config.onStateChange] - 状态变化回调
112
+ * @param {object} [config.processorOptions] - 传递给 AudioWorklet 的选项
113
+ * @param {boolean} [config.saveFullPcm=false] - 是否在内部保存所有 PCM 用于 stop 时合并(长时间录音建议关闭)
114
+ */
115
+ constructor(config) {
116
+ this.onData = config.onData || (() => {});
117
+ this.onStateChange = config.onStateChange || (() => {});
118
+ this.processorOptions = config.processorOptions || {};
119
+ this.saveFullPcm = config.saveFullPcm ?? false;
120
+
121
+ this.audioContext = null;
122
+ this.workletNode = null;
123
+ this.source = null;
124
+ this.workletUrl = null;
125
+ this.fullPcmData = this.saveFullPcm ? [] : null;
126
+
127
+ this.isInitialized = false;
128
+ this.isProcessing = false;
129
+ }
130
+
131
+ /**
132
+ * 初始化 AudioContext 并加载 AudioWorklet。
133
+ * 完成后状态变为 `"ready"`。
134
+ */
135
+ async init() {
136
+ this.onStateChange("initializing", "正在初始化音频环境...");
137
+ try {
138
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
139
+
140
+ const blob = new Blob([AudioStreamResamplerProcessorCode], { type: "application/javascript" });
141
+ this.workletUrl = URL.createObjectURL(blob);
142
+ await this.audioContext.audioWorklet.addModule(this.workletUrl);
143
+
144
+ this.workletNode = new AudioWorkletNode(this.audioContext, "audio-stream-resampler-processor", {
145
+ processorOptions: this.processorOptions
146
+ });
147
+
148
+ this.workletNode.port.onmessage = (event) => {
149
+ const chunk = event.data; // Int16Array
150
+ this.onData(chunk);
151
+ if (this.saveFullPcm) {
152
+ this.fullPcmData.push(chunk);
153
+ }
154
+ };
155
+
156
+ this.isInitialized = true;
157
+ this.onStateChange("ready", "音频环境已就绪");
158
+ } catch (err) {
159
+ console.error("AudioStreamResampler init error:", err);
160
+ this.onStateChange("error", `初始化失败: ${err.message}`);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * 绑定媒体流,开始实时处理。
166
+ * @param {MediaStream} stream - 通过 getUserMedia 或其他方式获得的流
167
+ */
168
+ setMediaStream(stream) {
169
+ if (!this.isInitialized) {
170
+ console.error("请先调用 init()");
171
+ return;
172
+ }
173
+
174
+ if (this.source) {
175
+ this.source.disconnect();
176
+ }
177
+
178
+ this.source = this.audioContext.createMediaStreamSource(stream);
179
+ this.source.connect(this.workletNode);
180
+ // workletNode 默认连接到 destination,可选断开以静音
181
+ // this.workletNode.connect(this.audioContext.destination);
182
+
183
+ this.isProcessing = true;
184
+ this.onStateChange("processing", "正在处理音频流...");
185
+ }
186
+
187
+ /**
188
+ * 停止处理并释放资源。
189
+ * @param {(fullPcm: Int16Array) => void} [callback] - 若构造时 `saveFullPcm=true`,会把合并后的完整 PCM 通过此回调传出
190
+ */
191
+ stop(callback) {
192
+ if (!this.isProcessing) return;
193
+
194
+ this.onStateChange("stopping", "正在停止...");
195
+
196
+ if (this.source) {
197
+ this.source.disconnect();
198
+ this.source = null;
199
+ }
200
+
201
+ this._cleanup();
202
+
203
+ this.onStateChange("stopped", "已停止");
204
+
205
+ if (this.saveFullPcm && callback && typeof callback === "function") {
206
+ const totalLength = this.fullPcmData.reduce((sum, chunk) => sum + chunk.length, 0);
207
+ const combined = new Int16Array(totalLength);
208
+ let offset = 0;
209
+ for (const chunk of this.fullPcmData) {
210
+ combined.set(chunk, offset);
211
+ offset += chunk.length;
212
+ }
213
+ callback(combined);
214
+ }
215
+ }
216
+
217
+ _cleanup() {
218
+ if (this.workletNode) {
219
+ this.workletNode.disconnect();
220
+ this.workletNode.port.close();
221
+ this.workletNode = null;
222
+ }
223
+ if (this.audioContext && this.audioContext.state !== "closed") {
224
+ this.audioContext.close();
225
+ this.audioContext = null;
226
+ }
227
+ if (this.workletUrl) {
228
+ URL.revokeObjectURL(this.workletUrl);
229
+ this.workletUrl = null;
230
+ }
231
+
232
+ this.isProcessing = false;
233
+ this.isInitialized = false;
234
+ if (this.fullPcmData) {
235
+ this.fullPcmData.length = 0;
236
+ }
237
+ }
238
+ }
239
+
240
+ /**
241
+ * 将 16-bit 单声道 PCM 数据封装成标准 WAV Blob。
242
+ *
243
+ * @param {Int16Array} pcmData - PCM 采样数据
244
+ * @param {number} [sampleRate=16000] - 采样率,默认 16 kHz
245
+ * @returns {Blob} audio/wav Blob
246
+ */
247
+ function pcmToWavBlob(pcmData, sampleRate = 16000) {
248
+ const length = pcmData.length;
249
+ const buffer = new ArrayBuffer(44 + length * 2);
250
+ const view = new DataView(buffer);
251
+ const writeString = (offset, string) => {
252
+ for (let i = 0; i < string.length; i++) {
253
+ view.setUint8(offset + i, string.charCodeAt(i));
254
+ }
255
+ };
256
+ writeString(0, "RIFF");
257
+ view.setUint32(4, 36 + length * 2, true);
258
+ writeString(8, "WAVE");
259
+ writeString(12, "fmt ");
260
+ view.setUint32(16, 16, true);
261
+ view.setUint16(20, 1, true);
262
+ view.setUint16(22, 1, true);
263
+ view.setUint32(24, sampleRate, true);
264
+ view.setUint32(28, sampleRate * 2, true);
265
+ view.setUint16(32, 2, true);
266
+ view.setUint16(34, 16, true);
267
+ writeString(36, "data");
268
+ view.setUint32(40, length * 2, true);
269
+ let offset = 44;
270
+ for (let i = 0; i < length; i++) {
271
+ view.setInt16(offset, pcmData[i], true);
272
+ offset += 2;
273
+ }
274
+ return new Blob([buffer], { type: "audio/wav" });
275
+ }
276
+
277
+ export { AudioStreamResampler, pcmToWavBlob };
278
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.js","sources":["../src/source/audio.js"],"sourcesContent":["const AudioStreamResamplerProcessorCode = `\nclass AudioStreamResamplerProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n const config = options.processorOptions || {};\n this.targetSampleRate = config.targetSampleRate || 16000;\n this.sourceSampleRate = sampleRate;\n this.downsampleRatio = this.sourceSampleRate / this.targetSampleRate;\n\n // 16000Hz 用 60ms chunk (960 samples),其他用 1024\n this.chunkSize = this.targetSampleRate === 16000 ? 960 : 1024;\n\n const sourceBufferSize = config.sourceBufferSize || 16384; // ~340ms @48kHz\n const pcmBufferSize = config.pcmBufferSize || (this.chunkSize * 10); // 更大缓冲,减少溢出概率\n\n this.sourceBuffer = new Float32Array(sourceBufferSize);\n this.sourceBufferLength = 0;\n\n this.pcmBuffer = new Int16Array(pcmBufferSize);\n this.pcmBufferIndex = 0;\n }\n\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n if (!input || input.length === 0 || input[0].length === 0) return true;\n const inputChannel = input[0];\n\n // 1. 写入源缓冲区(溢出时覆盖最旧数据)\n const newLength = this.sourceBufferLength + inputChannel.length;\n if (newLength > this.sourceBuffer.length) {\n const overflow = newLength - this.sourceBuffer.length;\n this.sourceBuffer.copyWithin(0, overflow);\n this.sourceBufferLength = this.sourceBuffer.length - inputChannel.length;\n }\n this.sourceBuffer.set(inputChannel, this.sourceBufferLength);\n this.sourceBufferLength += inputChannel.length;\n\n // 2. 计算可降采样样本数\n const availableOutputSamples = Math.floor(this.sourceBufferLength / this.downsampleRatio);\n if (availableOutputSamples === 0) return true;\n\n // 3. 线性插值降采样\n const downsampled = new Float32Array(availableOutputSamples);\n for (let i = 0; i < availableOutputSamples; i++) {\n const srcIndex = i * this.downsampleRatio;\n const srcIndexInt = Math.floor(srcIndex);\n const fraction = srcIndex - srcIndexInt;\n const val0 = this.sourceBuffer[srcIndexInt];\n const val1 = srcIndexInt + 1 < this.sourceBufferLength ? this.sourceBuffer[srcIndexInt + 1] : val0;\n downsampled[i] = val0 + (val1 - val0) * fraction;\n }\n\n // 4. Float32 → Int16 PCM\n const pcmData = this.floatTo16BitPCM(downsampled);\n\n // 5. 写入 PCM 缓冲区(空间不足时直接覆盖最旧,缓冲区足够大基本不会触发)\n if (this.pcmBufferIndex + pcmData.length > this.pcmBuffer.length) {\n // 简单策略:从头覆盖(丢弃最旧数据)\n this.pcmBufferIndex = 0;\n }\n this.pcmBuffer.set(pcmData, this.pcmBufferIndex);\n this.pcmBufferIndex += pcmData.length;\n\n // 6. 发送所有完整的 chunk(关键:复制到新数组再转移)\n while (this.pcmBufferIndex >= this.chunkSize) {\n // 方法1:推荐,使用 slice(隐式复制到新 ArrayBuffer)\n const chunk = this.pcmBuffer.slice(0, this.chunkSize);\n\n // 方法2:等价写法\n // const chunk = new Int16Array(this.pcmBuffer.subarray(0, this.chunkSize));\n\n this.port.postMessage(chunk, [chunk.buffer]); // 转移新缓冲区,安全!\n\n // 移动剩余数据到开头\n this.pcmBuffer.copyWithin(0, this.chunkSize, this.pcmBufferIndex);\n this.pcmBufferIndex -= this.chunkSize;\n }\n\n // 7. 清理已消费的源数据\n const consumedSrc = Math.floor(availableOutputSamples * this.downsampleRatio + 0.5); // 四舍五入更准\n if (consumedSrc > 0) {\n this.sourceBuffer.copyWithin(0, consumedSrc, this.sourceBufferLength);\n this.sourceBufferLength -= consumedSrc;\n }\n\n return true;\n }\n\n floatTo16BitPCM(input) {\n const output = new Int16Array(input.length);\n for (let i = 0; i < input.length; i++) {\n const s = Math.max(-1, Math.min(1, input[i]));\n output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;\n }\n return output;\n }\n}\n\nregisterProcessor('audio-stream-resampler-processor', AudioStreamResamplerProcessor);\n`;\n\n/**\n * 浏览器端实时音频流重采样器。\n * 基于 AudioWorklet 将麦克风/媒体流转换为 16 kHz、16-bit、单声道 PCM,\n * 并通过回调逐块输出,可选保存完整 PCM 用于后续合并。\n */\nexport class AudioStreamResampler {\n /**\n * @param {object} config\n * @param {function(Int16Array)} config.onData - 收到一个 chunk PCM 数据的回调\n * @param {function(string, string)} [config.onStateChange] - 状态变化回调\n * @param {object} [config.processorOptions] - 传递给 AudioWorklet 的选项\n * @param {boolean} [config.saveFullPcm=false] - 是否在内部保存所有 PCM 用于 stop 时合并(长时间录音建议关闭)\n */\n constructor(config) {\n this.onData = config.onData || (() => {});\n this.onStateChange = config.onStateChange || (() => {});\n this.processorOptions = config.processorOptions || {};\n this.saveFullPcm = config.saveFullPcm ?? false;\n\n this.audioContext = null;\n this.workletNode = null;\n this.source = null;\n this.workletUrl = null;\n this.fullPcmData = this.saveFullPcm ? [] : null;\n\n this.isInitialized = false;\n this.isProcessing = false;\n }\n\n /**\n * 初始化 AudioContext 并加载 AudioWorklet。\n * 完成后状态变为 `\"ready\"`。\n */\n async init() {\n this.onStateChange(\"initializing\", \"正在初始化音频环境...\");\n try {\n this.audioContext = new (window.AudioContext || window.webkitAudioContext)();\n\n const blob = new Blob([AudioStreamResamplerProcessorCode], { type: \"application/javascript\" });\n this.workletUrl = URL.createObjectURL(blob);\n await this.audioContext.audioWorklet.addModule(this.workletUrl);\n\n this.workletNode = new AudioWorkletNode(this.audioContext, \"audio-stream-resampler-processor\", {\n processorOptions: this.processorOptions\n });\n\n this.workletNode.port.onmessage = (event) => {\n const chunk = event.data; // Int16Array\n this.onData(chunk);\n if (this.saveFullPcm) {\n this.fullPcmData.push(chunk);\n }\n };\n\n this.isInitialized = true;\n this.onStateChange(\"ready\", \"音频环境已就绪\");\n } catch (err) {\n console.error(\"AudioStreamResampler init error:\", err);\n this.onStateChange(\"error\", `初始化失败: ${err.message}`);\n }\n }\n\n /**\n * 绑定媒体流,开始实时处理。\n * @param {MediaStream} stream - 通过 getUserMedia 或其他方式获得的流\n */\n setMediaStream(stream) {\n if (!this.isInitialized) {\n console.error(\"请先调用 init()\");\n return;\n }\n\n if (this.source) {\n this.source.disconnect();\n }\n\n this.source = this.audioContext.createMediaStreamSource(stream);\n this.source.connect(this.workletNode);\n // workletNode 默认连接到 destination,可选断开以静音\n // this.workletNode.connect(this.audioContext.destination);\n\n this.isProcessing = true;\n this.onStateChange(\"processing\", \"正在处理音频流...\");\n }\n\n /**\n * 停止处理并释放资源。\n * @param {(fullPcm: Int16Array) => void} [callback] - 若构造时 `saveFullPcm=true`,会把合并后的完整 PCM 通过此回调传出\n */\n stop(callback) {\n if (!this.isProcessing) return;\n\n this.onStateChange(\"stopping\", \"正在停止...\");\n\n if (this.source) {\n this.source.disconnect();\n this.source = null;\n }\n\n this._cleanup();\n\n this.onStateChange(\"stopped\", \"已停止\");\n\n if (this.saveFullPcm && callback && typeof callback === \"function\") {\n const totalLength = this.fullPcmData.reduce((sum, chunk) => sum + chunk.length, 0);\n const combined = new Int16Array(totalLength);\n let offset = 0;\n for (const chunk of this.fullPcmData) {\n combined.set(chunk, offset);\n offset += chunk.length;\n }\n callback(combined);\n }\n }\n\n _cleanup() {\n if (this.workletNode) {\n this.workletNode.disconnect();\n this.workletNode.port.close();\n this.workletNode = null;\n }\n if (this.audioContext && this.audioContext.state !== \"closed\") {\n this.audioContext.close();\n this.audioContext = null;\n }\n if (this.workletUrl) {\n URL.revokeObjectURL(this.workletUrl);\n this.workletUrl = null;\n }\n\n this.isProcessing = false;\n this.isInitialized = false;\n if (this.fullPcmData) {\n this.fullPcmData.length = 0;\n }\n }\n}\n\n/**\n * 将 16-bit 单声道 PCM 数据封装成标准 WAV Blob。\n *\n * @param {Int16Array} pcmData - PCM 采样数据\n * @param {number} [sampleRate=16000] - 采样率,默认 16 kHz\n * @returns {Blob} audio/wav Blob\n */\nexport function pcmToWavBlob(pcmData, sampleRate = 16000) {\n const length = pcmData.length;\n const buffer = new ArrayBuffer(44 + length * 2);\n const view = new DataView(buffer);\n const writeString = (offset, string) => {\n for (let i = 0; i < string.length; i++) {\n view.setUint8(offset + i, string.charCodeAt(i));\n }\n };\n writeString(0, \"RIFF\");\n view.setUint32(4, 36 + length * 2, true);\n writeString(8, \"WAVE\");\n writeString(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, 1, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * 2, true);\n view.setUint16(32, 2, true);\n view.setUint16(34, 16, true);\n writeString(36, \"data\");\n view.setUint32(40, length * 2, true);\n let offset = 44;\n for (let i = 0; i < length; i++) {\n view.setInt16(offset, pcmData[i], true);\n offset += 2;\n }\n return new Blob([buffer], { type: \"audio/wav\" });\n}\n"],"names":[],"mappings":"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"}