@idlebox/browser 0.0.44 → 0.0.46

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.
@@ -0,0 +1,281 @@
1
+ import {
2
+ closableToDisposable,
3
+ DeferredPromise,
4
+ definePublicConstant,
5
+ DuplicateDisposeAction,
6
+ Emitter,
7
+ EnhancedDisposable,
8
+ ExtendableTimer,
9
+ sleep,
10
+ type EventRegister,
11
+ } from '@idlebox/common';
12
+
13
+ /**
14
+ * TODO: need a logger
15
+ */
16
+ const logger = console;
17
+
18
+ interface IRecorder {
19
+ onDataAvailable: EventRegister<Uint8Array<ArrayBuffer>>;
20
+ onFinished: EventRegister<void>;
21
+ dispose(): void;
22
+ }
23
+
24
+ /**
25
+ * TODO: 新api要求使用webworker 这里模拟worker
26
+ */
27
+ export class RawPcmStreamNode extends EnhancedDisposable implements IRecorder {
28
+ protected override duplicateDispose = DuplicateDisposeAction.Allow;
29
+
30
+ private readonly _onDataAvailable = new Emitter<Uint8Array<ArrayBuffer>>();
31
+ public readonly onDataAvailable = this._onDataAvailable.event;
32
+
33
+ /**
34
+ * 由于AudioNode没有类似end的事件,只能用延迟模拟一个
35
+ */
36
+ private readonly _onFinished = new Emitter<void>();
37
+ public readonly onFinished = this._onFinished.event;
38
+
39
+ /**
40
+ * 被要求结束后,只有连续没有收到音频,才真正触发 onFinished
41
+ * 这和决定录音何时结束无关
42
+ */
43
+ private readonly willFinish: ExtendableTimer;
44
+
45
+ private _node?: ScriptProcessorNode;
46
+
47
+ constructor(
48
+ private readonly audioContext: AudioContext,
49
+ private readonly bitDepth = 16,
50
+ private readonly latency = 150,
51
+ ) {
52
+ super();
53
+
54
+ this.willFinish = new ExtendableTimer(latency * 2);
55
+ this.willFinish.onSchedule(() => {
56
+ logger.debug('连续没有收到音频,录制正确结束');
57
+ this._onFinished.fireNoError();
58
+ this.dispose();
59
+ });
60
+ this.onPostDispose(() => {
61
+ this._onFinished.dispose();
62
+ if (this._node) {
63
+ this._node.disconnect();
64
+ this._node = undefined;
65
+ }
66
+ this.willFinish.cancel();
67
+ this._onDataAvailable.dispose();
68
+ });
69
+ }
70
+
71
+ get bufferSize() {
72
+ return calculateBufferSize(this.latency, this.audioContext.sampleRate, 1, this.bitDepth);
73
+ }
74
+
75
+ private _getNode() {
76
+ if (!this._node) {
77
+ this._node = this.audioContext.createScriptProcessor(this.bufferSize, 1, 1);
78
+ logger.debug(`创建新的stream-node: buffer size = %s`, this.bufferSize);
79
+ }
80
+
81
+ this._node.onaudioprocess = (event) => {
82
+ this.willFinish.renew();
83
+ const rawPcmData = event.inputBuffer.getChannelData(0);
84
+ logger.debug('~ script process data %s frames', event.inputBuffer.length);
85
+ const buff = float32_sint16(rawPcmData);
86
+ this._onDataAvailable.fireNoError(new Uint8Array(buff.buffer));
87
+ };
88
+
89
+ // 实际无用,但没有目标就不产生事件
90
+ this._node.connect(this.audioContext.destination);
91
+
92
+ return this._node;
93
+ }
94
+
95
+ connectFrom(source: AudioNode) {
96
+ source.connect(this._getNode());
97
+ }
98
+
99
+ /**
100
+ * 外部不要调用
101
+ *
102
+ * 优雅结束
103
+ * @private
104
+ */
105
+ shutdown() {
106
+ logger.debug('即将结束录制过程');
107
+ this.willFinish.start();
108
+ }
109
+ }
110
+
111
+ export class RawPcmStreamRecorder extends EnhancedDisposable {
112
+ protected override duplicateDispose = DuplicateDisposeAction.Allow;
113
+
114
+ private readonly channels = 1;
115
+ private readonly dfd = new DeferredPromise<void>();
116
+
117
+ constructor(
118
+ public readonly bitDepth = 16,
119
+ public readonly sampleRate = 16000,
120
+ ) {
121
+ super();
122
+ }
123
+
124
+ get context(): AudioContext {
125
+ logger.debug('creating new audio context');
126
+ const context = new AudioContext({ sampleRate: this.sampleRate });
127
+ context.addEventListener('statechange', () => {
128
+ if (context.state === 'closed' && !this.hasDisposed) {
129
+ this.dispose();
130
+ }
131
+ });
132
+
133
+ definePublicConstant(this, 'context', context);
134
+ this._register(closableToDisposable(context));
135
+
136
+ this.onBeforeDispose(() => {
137
+ if (!this.dfd.settled) this.dfd.error(new Error('录制操作中断'));
138
+
139
+ this.started = undefined;
140
+ this.close_microphone(0);
141
+ });
142
+
143
+ return context;
144
+ }
145
+
146
+ private started?: Promise<RawPcmStreamNode>;
147
+
148
+ /**
149
+ * 开始录音(可以重复调用,返回相同)
150
+ * stop之后不能重新start
151
+ */
152
+ startRecording(latency: number = 150): Promise<IRecorder> {
153
+ if (this.disposed || this.hasClosed) {
154
+ throw new Error('recorder is already finished');
155
+ }
156
+
157
+ if (!this.started) {
158
+ this.started = this._startRecording(latency);
159
+ }
160
+ return this.started;
161
+ }
162
+
163
+ private _microphone?: MediaStream;
164
+ private _analyze?: AnalyserNode;
165
+ private _recorder?: RawPcmStreamNode;
166
+ async _startRecording(latency: number) {
167
+ logger.debug('启动录音,缓冲 %sms', latency);
168
+ const microphone = await navigator.mediaDevices
169
+ .getUserMedia({
170
+ video: false,
171
+ audio: {
172
+ sampleRate: this.sampleRate,
173
+ channelCount: this.channels,
174
+ sampleSize: this.bitDepth,
175
+ echoCancellation: true,
176
+ noiseSuppression: true,
177
+ autoGainControl: true,
178
+ },
179
+ })
180
+ .catch((e: any) => {
181
+ this.dfd.error(e);
182
+ throw e;
183
+ });
184
+ const streamNode = this.context.createMediaStreamSource(microphone);
185
+
186
+ for (const item of microphone.getAudioTracks()) {
187
+ item.addEventListener('ended', () => {
188
+ logger.warn('麦克风由于外部原因停止');
189
+ this.close_microphone(0);
190
+ });
191
+ }
192
+
193
+ this._microphone = microphone;
194
+
195
+ const recorder = new RawPcmStreamNode(this.context, this.bitDepth, latency);
196
+ this._recorder = recorder;
197
+ recorder.connectFrom(streamNode);
198
+
199
+ recorder.onFinished(() => {
200
+ this.dfd.complete();
201
+ });
202
+
203
+ const analyze = this.context.createAnalyser();
204
+ this._analyze = analyze;
205
+ streamNode.connect(analyze);
206
+
207
+ return recorder;
208
+ }
209
+
210
+ private caclculateVolume() {
211
+ if (!this._analyze) return 0;
212
+ const bufferLength = this._analyze.frequencyBinCount;
213
+ const dataArray = new Uint8Array(bufferLength);
214
+
215
+ this._analyze.getByteFrequencyData(dataArray);
216
+
217
+ let sum = 0;
218
+ for (const amplitude of dataArray) {
219
+ sum += amplitude * amplitude;
220
+ }
221
+
222
+ return Math.sqrt(sum / dataArray.length);
223
+ }
224
+
225
+ getPromise() {
226
+ return this.dfd.p;
227
+ }
228
+
229
+ private hasClosed = false;
230
+ private close_microphone(delay: number) {
231
+ if (this.hasClosed) return;
232
+ this.hasClosed = true;
233
+
234
+ logger.debug('程序主动关闭麦克风 | 延迟 %sms', delay);
235
+ sleep(delay).then(() => {
236
+ if (this._microphone) {
237
+ // 其实只有一个
238
+ for (const item of this._microphone.getAudioTracks()) {
239
+ item.stop();
240
+ this._microphone.removeTrack(item);
241
+ }
242
+ }
243
+
244
+ if (this._recorder) this._recorder.shutdown();
245
+ });
246
+ }
247
+
248
+ async stopRecording() {
249
+ const volume: number = this.caclculateVolume();
250
+
251
+ // 不知道是否合适
252
+ const delay = (1000 * volume) / 60;
253
+
254
+ this.close_microphone(delay);
255
+
256
+ return new Promise<void>((resolve, reject) => {
257
+ if (!this._recorder) return resolve();
258
+
259
+ this._recorder.onBeforeDispose(resolve);
260
+ this._recorder.onBeforeDispose(() => reject(new Error('录制操作非正常中断')));
261
+ });
262
+ }
263
+ }
264
+
265
+ function calculateBufferSize(milliseconds: number, sampleRate: number, channels: number, bits: number): number {
266
+ const rawSize = Math.floor((milliseconds / 1000) * sampleRate * channels * (bits / 8));
267
+ // 返回大于rawSize的最接近的2的幂
268
+ const near = 2 ** Math.ceil(Math.log2(rawSize));
269
+ return Math.max(256, Math.min(16384, near));
270
+ }
271
+
272
+ function float32_sint16(input: Float32Array) {
273
+ // 将Float32Array转换为Int16Array
274
+ const output = new Int16Array(input.length);
275
+ for (let i = 0; i < input.length; i++) {
276
+ let s = input[i];
277
+ s = Math.max(-1, Math.min(1, s));
278
+ output[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
279
+ }
280
+ return output;
281
+ }
@@ -1,3 +0,0 @@
1
- export { WrappedWebConsole } from "./debug/logger.js";
2
- export { TimeoutStorage } from "./storage/timeoutStorage.js";
3
- //# sourceMappingURL=autoindex.generated.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"autoindex.generated.d.ts","sourceRoot":"","sources":["../src/autoindex.generated.ts"],"names":[],"mappings":"AAMC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC"}
@@ -1,10 +0,0 @@
1
- // DO NOT EDIT THIS FILE
2
- // @ts-ignore
3
- /* eslint-disable */
4
- /* debug/logger.ts */
5
- // Identifiers
6
- export { WrappedWebConsole } from "./debug/logger.js";
7
- /* storage/timeoutStorage.ts */
8
- // Identifiers
9
- export { TimeoutStorage } from "./storage/timeoutStorage.js";
10
- //# sourceMappingURL=autoindex.generated.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"autoindex.generated.js","sourceRoot":"","sources":["../src/autoindex.generated.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,aAAa;AACb,oBAAoB;AAEpB,qBAAqB;AACpB,cAAc;AACd,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACvD,+BAA+B;AAC9B,cAAc;AACd,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"timeoutStorage.d.ts","sourceRoot":"","sources":["../../src/storage/timeoutStorage.ts"],"names":[],"mappings":"AAAA,qBAAa,cAAc,CAAC,CAAC;IAM3B,OAAO,CAAC,QAAQ,CAAC,OAAO;IALzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAGlC,GAAG,EAAE,MAAM,EACM,OAAO,GAAE,OAAsB;IAMjD,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAU7C,MAAM;IAKN,SAAS;IAWT,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;IAC1C,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,GAAG,SAAS;CAuB/B"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"timeoutStorage.js","sourceRoot":"","sources":["../../src/storage/timeoutStorage.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,cAAc;IAMR;IALD,QAAQ,CAAS;IACjB,SAAS,CAAS;IAEnC,YACC,GAAW,EACM,UAAmB,YAAY;QAA/B,YAAO,GAAP,OAAO,CAAwB;QAEhD,IAAI,CAAC,QAAQ,GAAG,GAAG,GAAG,SAAS,CAAC;QAChC,IAAI,CAAC,SAAS,GAAG,GAAG,GAAG,UAAU,CAAC;IACnC,CAAC;IAED,IAAI,CAAC,IAAiB,EAAE,MAAqB;QAC5C,IAAI,MAAM,YAAY,IAAI,EAAE,CAAC;YAC5B,MAAM,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;QAC/B,CAAC;QAED,2DAA2D;QAC3D,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1D,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM;QACL,yCAAyC;QACzC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,SAAS;QACR,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;YACpC,0CAA0C;YAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO,IAAI,CAAC;QACb,CAAC;QAED,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAKD,IAAI,CAAC,UAAwB;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,EAAE,CAAC;YACX,OAAO,UAAU,CAAC;QACnB,CAAC;QAED,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;YACpC,0CAA0C;YAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO,UAAU,CAAC;QACnB,CAAC;QAED,IAAI,CAAC;YACJ,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,EAAE,EAAE,CAAC;YACb,qDAAqD;YACrD,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO,UAAU,CAAC;QACnB,CAAC;IACF,CAAC;CACD"}
@@ -1,10 +0,0 @@
1
- // DO NOT EDIT THIS FILE
2
- // @ts-ignore
3
- /* eslint-disable */
4
-
5
- /* debug/logger.ts */
6
- // Identifiers
7
- export { WrappedWebConsole } from "./debug/logger.js";
8
- /* storage/timeoutStorage.ts */
9
- // Identifiers
10
- export { TimeoutStorage } from "./storage/timeoutStorage.js";