Spaces:
Running
Running
Update src/lib/audio-recorder.ts
Browse files- src/lib/audio-recorder.ts +104 -398
src/lib/audio-recorder.ts
CHANGED
@@ -1,417 +1,123 @@
|
|
1 |
-
|
2 |
-
* Copyright 2024 Google LLC
|
3 |
-
*
|
4 |
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
5 |
-
* you may not use this file except in compliance with the License.
|
6 |
-
* You may obtain a copy of the License at
|
7 |
-
*
|
8 |
-
* http://www.apache.org/licenses/LICENSE-2.0
|
9 |
-
*
|
10 |
-
* Unless required by applicable law or agreed to in writing, software
|
11 |
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
12 |
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 |
-
* See the License for the specific language governing permissions and
|
14 |
-
* limitations under the License.
|
15 |
-
*/
|
16 |
|
17 |
-
import
|
18 |
-
import
|
19 |
-
import
|
20 |
-
import VolMeterWorket from "./worklets/vol-meter";
|
21 |
-
|
22 |
-
import { createWorketFromSrc } from "./audioworklet-registry";
|
23 |
-
import EventEmitter from "eventemitter3";
|
24 |
-
|
25 |
-
function arrayBufferToBase64(buffer: ArrayBuffer) {
|
26 |
-
var binary = "";
|
27 |
-
var bytes = new Uint8Array(buffer);
|
28 |
-
var len = bytes.byteLength;
|
29 |
-
for (var i = 0; i < len; i++) {
|
30 |
-
binary += String.fromCharCode(bytes[i]);
|
31 |
-
}
|
32 |
-
return window.btoa(binary);
|
33 |
-
}
|
34 |
-
|
35 |
-
// Add Safari-specific audio context creation
|
36 |
-
async function createSafariAudioContext(sampleRate: number): Promise<AudioContext> {
|
37 |
-
console.log('Creating Safari audio context with options:', { sampleRate });
|
38 |
-
|
39 |
-
// Safari requires webkit prefix
|
40 |
-
const AudioContextClass = (window as any).webkitAudioContext || window.AudioContext;
|
41 |
-
console.log('Using AudioContext class:', AudioContextClass.name);
|
42 |
-
|
43 |
-
const ctx = new AudioContextClass({
|
44 |
-
sampleRate,
|
45 |
-
latencyHint: 'interactive'
|
46 |
-
});
|
47 |
-
|
48 |
-
console.log('Safari AudioContext initial state:', {
|
49 |
-
state: ctx.state,
|
50 |
-
sampleRate: ctx.sampleRate,
|
51 |
-
baseLatency: ctx.baseLatency,
|
52 |
-
destination: ctx.destination,
|
53 |
-
});
|
54 |
-
|
55 |
-
// Safari requires user interaction to start audio context
|
56 |
-
if (ctx.state === 'suspended') {
|
57 |
-
console.log('Attempting to resume suspended Safari audio context...');
|
58 |
-
try {
|
59 |
-
await ctx.resume();
|
60 |
-
console.log('Successfully resumed Safari audio context:', ctx.state);
|
61 |
-
} catch (err) {
|
62 |
-
console.error('Failed to resume Safari audio context:', err);
|
63 |
-
throw err;
|
64 |
-
}
|
65 |
-
}
|
66 |
-
|
67 |
-
return ctx;
|
68 |
-
}
|
69 |
|
70 |
export class AudioRecorder extends EventEmitter {
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
// Add browser detection
|
81 |
-
isSafari: boolean;
|
82 |
-
isIOS: boolean;
|
83 |
-
|
84 |
-
constructor(public sampleRate = 16000) {
|
85 |
super();
|
86 |
-
this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
87 |
-
this.isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
|
88 |
-
console.log('AudioRecorder initialized:', {
|
89 |
-
isSafari: this.isSafari,
|
90 |
-
isIOS: this.isIOS,
|
91 |
-
sampleRate: this.sampleRate,
|
92 |
-
userAgent: navigator.userAgent,
|
93 |
-
webAudioSupport: !!(window.AudioContext || (window as any).webkitAudioContext),
|
94 |
-
mediaDevicesSupport: !!navigator.mediaDevices
|
95 |
-
});
|
96 |
}
|
97 |
|
98 |
-
async
|
99 |
-
if (!
|
100 |
-
|
101 |
-
mediaDevices: !!navigator.mediaDevices,
|
102 |
-
getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
|
103 |
-
});
|
104 |
-
throw new Error("Could not request user media");
|
105 |
}
|
106 |
-
|
107 |
-
console.log('Starting AudioRecorder with full environment info:', {
|
108 |
-
userAgent: navigator.userAgent,
|
109 |
-
platform: navigator.platform,
|
110 |
-
vendor: navigator.vendor,
|
111 |
-
audioWorkletSupport: !!(window.AudioWorklet),
|
112 |
-
sampleRate: this.sampleRate,
|
113 |
-
existingAudioContext: !!this.audioContext,
|
114 |
-
existingStream: !!this.stream,
|
115 |
-
isSafari: this.isSafari
|
116 |
-
});
|
117 |
-
|
118 |
-
this.starting = new Promise(async (resolve, reject) => {
|
119 |
try {
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
try {
|
138 |
-
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
139 |
-
const track = this.stream.getAudioTracks()[0];
|
140 |
-
console.log('Safari audio permissions granted:', {
|
141 |
-
track: track.label,
|
142 |
-
settings: track.getSettings(),
|
143 |
-
constraints: track.getConstraints(),
|
144 |
-
enabled: track.enabled,
|
145 |
-
muted: track.muted,
|
146 |
-
readyState: track.readyState
|
147 |
-
});
|
148 |
-
} catch (err) {
|
149 |
-
console.error('Failed to get Safari audio permissions:', err);
|
150 |
-
throw err;
|
151 |
-
}
|
152 |
-
|
153 |
-
// 2. Create and initialize audio context
|
154 |
-
try {
|
155 |
-
this.audioContext = await createSafariAudioContext(this.sampleRate);
|
156 |
-
console.log('Safari audio context ready:', {
|
157 |
-
state: this.audioContext.state,
|
158 |
-
currentTime: this.audioContext.currentTime
|
159 |
-
});
|
160 |
-
} catch (err) {
|
161 |
-
console.error('Failed to initialize Safari audio context:', err);
|
162 |
-
throw err;
|
163 |
-
}
|
164 |
-
|
165 |
-
// 3. Create and connect audio source
|
166 |
-
try {
|
167 |
-
console.log('Creating Safari audio source...');
|
168 |
-
this.source = this.audioContext.createMediaStreamSource(this.stream);
|
169 |
-
console.log('Safari audio source created successfully:', {
|
170 |
-
numberOfInputs: this.source.numberOfInputs,
|
171 |
-
numberOfOutputs: this.source.numberOfOutputs,
|
172 |
-
channelCount: this.source.channelCount
|
173 |
-
});
|
174 |
-
} catch (err) {
|
175 |
-
console.error('Failed to create Safari audio source:', err);
|
176 |
-
throw err;
|
177 |
-
}
|
178 |
-
|
179 |
-
// 4. Load and create worklet
|
180 |
-
try {
|
181 |
-
const workletName = "audio-recorder-worklet";
|
182 |
-
console.log('Loading Safari audio worklet...');
|
183 |
-
const src = createWorketFromSrc(workletName, SafariAudioRecordingWorklet);
|
184 |
-
await this.audioContext.audioWorklet.addModule(src);
|
185 |
-
console.log('Safari audio worklet module loaded');
|
186 |
-
|
187 |
-
this.recordingWorklet = new AudioWorkletNode(
|
188 |
-
this.audioContext,
|
189 |
-
workletName,
|
190 |
-
{
|
191 |
-
numberOfInputs: 1,
|
192 |
-
numberOfOutputs: 1,
|
193 |
-
channelCount: 1,
|
194 |
-
processorOptions: {
|
195 |
-
sampleRate: this.sampleRate
|
196 |
-
}
|
197 |
-
}
|
198 |
-
);
|
199 |
-
|
200 |
-
// Add detailed error handlers
|
201 |
-
this.recordingWorklet.onprocessorerror = (event) => {
|
202 |
-
console.error('Safari AudioWorklet processor error:', event);
|
203 |
-
};
|
204 |
-
|
205 |
-
this.recordingWorklet.port.onmessageerror = (event) => {
|
206 |
-
console.error('Safari AudioWorklet message error:', event);
|
207 |
-
};
|
208 |
-
|
209 |
-
// Add data handler with detailed logging
|
210 |
-
this.recordingWorklet.port.onmessage = (ev: MessageEvent) => {
|
211 |
-
const data = ev.data.data;
|
212 |
-
console.log('Safari AudioWorklet message received:', {
|
213 |
-
eventType: ev.data.event,
|
214 |
-
hasData: !!data,
|
215 |
-
dataType: data ? typeof data : null,
|
216 |
-
timestamp: Date.now()
|
217 |
-
});
|
218 |
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
this.emit("data", arrayBufferString);
|
226 |
-
} else {
|
227 |
-
console.warn('Invalid Safari audio chunk received:', ev.data);
|
228 |
-
}
|
229 |
-
};
|
230 |
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
}
|
236 |
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
|
|
|
|
246 |
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
noiseSuppression: true,
|
256 |
-
autoGainControl: true,
|
257 |
-
sampleRate: this.sampleRate
|
258 |
}
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
console.log('Chrome audio permissions granted:', {
|
266 |
-
track: track.label,
|
267 |
-
settings: track.getSettings()
|
268 |
-
});
|
269 |
-
} catch (err) {
|
270 |
-
console.error('Failed to get Chrome audio permissions:', err);
|
271 |
-
throw err;
|
272 |
-
}
|
273 |
-
|
274 |
-
// Create audio context after getting stream for Chrome
|
275 |
-
try {
|
276 |
-
console.log('Creating Chrome audio context...');
|
277 |
-
this.audioContext = await audioContext({ sampleRate: this.sampleRate });
|
278 |
-
console.log('Chrome audio context created:', {
|
279 |
-
state: this.audioContext.state,
|
280 |
-
sampleRate: this.audioContext.sampleRate
|
281 |
-
});
|
282 |
-
} catch (err) {
|
283 |
-
console.error('Failed to create Chrome audio context:', err);
|
284 |
-
throw err;
|
285 |
-
}
|
286 |
-
|
287 |
-
// Create media stream source
|
288 |
-
try {
|
289 |
-
console.log('Creating Chrome audio source...');
|
290 |
-
this.source = this.audioContext.createMediaStreamSource(this.stream);
|
291 |
-
console.log('Chrome audio source created');
|
292 |
-
} catch (err) {
|
293 |
-
console.error('Failed to create Chrome audio source:', err);
|
294 |
-
throw err;
|
295 |
-
}
|
296 |
-
|
297 |
-
// Load and create standard worklet
|
298 |
-
try {
|
299 |
-
const workletName = "audio-recorder-worklet";
|
300 |
-
console.log('Loading Chrome audio worklet...');
|
301 |
-
const src = createWorketFromSrc(workletName, AudioRecordingWorklet);
|
302 |
-
await this.audioContext.audioWorklet.addModule(src);
|
303 |
-
console.log('Chrome audio worklet loaded');
|
304 |
-
|
305 |
-
this.recordingWorklet = new AudioWorkletNode(
|
306 |
-
this.audioContext,
|
307 |
-
workletName,
|
308 |
-
{
|
309 |
-
numberOfInputs: 1,
|
310 |
-
numberOfOutputs: 1,
|
311 |
-
channelCount: 1,
|
312 |
-
processorOptions: {
|
313 |
-
sampleRate: this.sampleRate
|
314 |
-
}
|
315 |
-
}
|
316 |
-
);
|
317 |
-
|
318 |
-
// Add error handlers
|
319 |
-
this.recordingWorklet.onprocessorerror = (event) => {
|
320 |
-
console.error('Chrome AudioWorklet processor error:', event);
|
321 |
-
};
|
322 |
-
|
323 |
-
this.recordingWorklet.port.onmessageerror = (event) => {
|
324 |
-
console.error('Chrome AudioWorklet message error:', event);
|
325 |
-
};
|
326 |
-
|
327 |
-
// Add data handler
|
328 |
-
this.recordingWorklet.port.onmessage = async (ev: MessageEvent) => {
|
329 |
-
const arrayBuffer = ev.data.data?.int16arrayBuffer;
|
330 |
-
if (arrayBuffer) {
|
331 |
-
const arrayBufferString = arrayBufferToBase64(arrayBuffer);
|
332 |
-
this.emit("data", arrayBufferString);
|
333 |
-
} else {
|
334 |
-
console.warn('Invalid Chrome audio chunk received:', ev.data);
|
335 |
-
}
|
336 |
-
};
|
337 |
-
|
338 |
-
console.log('Chrome AudioWorkletNode created');
|
339 |
-
} catch (err) {
|
340 |
-
console.error('Failed to setup Chrome audio worklet:', err);
|
341 |
-
throw err;
|
342 |
-
}
|
343 |
-
|
344 |
-
// Connect nodes
|
345 |
-
try {
|
346 |
-
console.log('Connecting Chrome audio nodes...');
|
347 |
-
this.source.connect(this.recordingWorklet);
|
348 |
-
console.log('Chrome audio nodes connected');
|
349 |
-
|
350 |
-
// Set up VU meter
|
351 |
-
const vuWorkletName = "vu-meter";
|
352 |
-
await this.audioContext.audioWorklet.addModule(
|
353 |
-
createWorketFromSrc(vuWorkletName, VolMeterWorket),
|
354 |
-
);
|
355 |
-
this.vuWorklet = new AudioWorkletNode(this.audioContext, vuWorkletName);
|
356 |
-
this.vuWorklet.port.onmessage = (ev: MessageEvent) => {
|
357 |
-
this.emit("volume", ev.data.volume);
|
358 |
-
};
|
359 |
-
this.source.connect(this.vuWorklet);
|
360 |
-
console.log('Chrome VU meter connected');
|
361 |
-
} catch (err) {
|
362 |
-
console.error('Failed to connect Chrome audio nodes:', err);
|
363 |
-
throw err;
|
364 |
-
}
|
365 |
-
}
|
366 |
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
} catch (error) {
|
372 |
-
console.error('Failed to start recording:', error);
|
373 |
-
this.stop();
|
374 |
-
reject(error);
|
375 |
-
this.starting = null;
|
376 |
-
}
|
377 |
-
});
|
378 |
-
return this.starting;
|
379 |
}
|
380 |
|
381 |
-
stop() {
|
382 |
-
|
383 |
-
// its plausible that stop would be called before start completes
|
384 |
-
// such as if the websocket immediately hangs up
|
385 |
-
const handleStop = () => {
|
386 |
-
try {
|
387 |
-
if (this.source) {
|
388 |
-
console.log('Disconnecting audio source...');
|
389 |
-
this.source.disconnect();
|
390 |
-
}
|
391 |
-
if (this.stream) {
|
392 |
-
console.log('Stopping media stream tracks...');
|
393 |
-
this.stream.getTracks().forEach(track => {
|
394 |
-
track.stop();
|
395 |
-
console.log('Stopped track:', track.label);
|
396 |
-
});
|
397 |
-
}
|
398 |
-
if (this.audioContext && this.isSafari) {
|
399 |
-
console.log('Closing Safari audio context...');
|
400 |
-
this.audioContext.close();
|
401 |
-
}
|
402 |
-
this.stream = undefined;
|
403 |
-
this.recordingWorklet = undefined;
|
404 |
-
this.vuWorklet = undefined;
|
405 |
-
console.log('Audio recorder stopped successfully');
|
406 |
-
} catch (err) {
|
407 |
-
console.error('Error while stopping audio recorder:', err);
|
408 |
-
}
|
409 |
-
};
|
410 |
-
if (this.starting) {
|
411 |
-
console.log('Stop called while starting - waiting for start to complete...');
|
412 |
-
this.starting.then(handleStop);
|
413 |
return;
|
414 |
}
|
415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
416 |
}
|
417 |
-
}
|
|
|
1 |
+
// src/lib/audio-recorder.ts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
+
import EventEmitter from 'eventemitter3';
|
4 |
+
import { audioContext } from './utils';
|
5 |
+
import VolMeterWorket from './worklets/vol-meter';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
export class AudioRecorder extends EventEmitter {
|
8 |
+
public recording = false;
|
9 |
+
private mediaStream: MediaStream | null = null;
|
10 |
+
private audioContext: AudioContext | null = null;
|
11 |
+
private processor: ScriptProcessorNode | null = null;
|
12 |
+
private source: MediaStreamAudioSourceNode | null = null;
|
13 |
+
private volMeter: AudioWorkletNode | null = null;
|
14 |
+
|
15 |
+
constructor() {
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
super();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
}
|
18 |
|
19 |
+
private async init() {
|
20 |
+
if (!this.audioContext) {
|
21 |
+
this.audioContext = await audioContext({ id: 'audio-in' });
|
|
|
|
|
|
|
|
|
22 |
}
|
23 |
+
if (!this.mediaStream) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
try {
|
25 |
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
26 |
+
audio: {
|
27 |
+
sampleRate: 16000,
|
28 |
+
channelCount: 1,
|
29 |
+
echoCancellation: true,
|
30 |
+
noiseSuppression: true,
|
31 |
+
},
|
32 |
+
video: false,
|
33 |
+
});
|
34 |
+
} catch (err) {
|
35 |
+
console.error('Error getting media stream:', err);
|
36 |
+
this.emit('error', err);
|
37 |
+
return;
|
38 |
+
}
|
39 |
+
}
|
40 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
+
public async start() {
|
43 |
+
if (this.recording) {
|
44 |
+
return;
|
45 |
+
}
|
46 |
+
|
47 |
+
await this.init();
|
|
|
|
|
|
|
|
|
|
|
48 |
|
49 |
+
if (!this.audioContext || !this.mediaStream) {
|
50 |
+
console.error("AudioContext or MediaStream not available.");
|
51 |
+
return;
|
52 |
+
}
|
|
|
53 |
|
54 |
+
this.recording = true;
|
55 |
+
this.source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
56 |
+
this.processor = this.audioContext.createScriptProcessor(1024, 1, 1);
|
57 |
+
|
58 |
+
this.processor.onaudioprocess = (e) => {
|
59 |
+
if (!this.recording) return;
|
60 |
+
const inputData = e.inputBuffer.getChannelData(0);
|
61 |
+
const pcm16 = this.floatTo16BitPCM(inputData);
|
62 |
+
const base64 = this.pcm16ToBase64(pcm16);
|
63 |
+
this.emit('data', base64);
|
64 |
+
};
|
65 |
|
66 |
+
// --- بخش کلیدی: اضافه کردن Volume Meter ---
|
67 |
+
try {
|
68 |
+
await this.audioContext.audioWorklet.addModule(VolMeterWorket);
|
69 |
+
this.volMeter = new AudioWorkletNode(this.audioContext, 'vumeter');
|
70 |
+
this.volMeter.port.onmessage = (event) => {
|
71 |
+
if(event.data.volume) {
|
72 |
+
// ارسال رویداد جدید به همراه حجم صدا
|
73 |
+
this.emit('volume', event.data.volume);
|
|
|
|
|
|
|
74 |
}
|
75 |
+
};
|
76 |
+
this.source.connect(this.volMeter);
|
77 |
+
} catch(err) {
|
78 |
+
console.error("Error adding AudioWorklet module", err);
|
79 |
+
}
|
80 |
+
// ------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
82 |
+
this.source.connect(this.processor);
|
83 |
+
this.processor.connect(this.audioContext.destination);
|
84 |
+
|
85 |
+
this.emit('start');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
}
|
87 |
|
88 |
+
public stop() {
|
89 |
+
if (!this.recording) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
return;
|
91 |
}
|
92 |
+
this.recording = false;
|
93 |
+
this.emit('stop');
|
94 |
+
|
95 |
+
this.source?.disconnect();
|
96 |
+
this.processor?.disconnect();
|
97 |
+
this.volMeter?.disconnect();
|
98 |
+
|
99 |
+
this.source = null;
|
100 |
+
this.processor = null;
|
101 |
+
this.volMeter = null;
|
102 |
+
}
|
103 |
+
|
104 |
+
// توابع کمکی برای تبدیل فرمت صدا
|
105 |
+
private floatTo16BitPCM(input: Float32Array): Int16Array {
|
106 |
+
const output = new Int16Array(input.length);
|
107 |
+
for (let i = 0; i < input.length; i++) {
|
108 |
+
const s = Math.max(-1, Math.min(1, input[i]));
|
109 |
+
output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
110 |
+
}
|
111 |
+
return output;
|
112 |
+
}
|
113 |
+
|
114 |
+
private pcm16ToBase64(pcm16: Int16Array): string {
|
115 |
+
const buffer = pcm16.buffer;
|
116 |
+
const bytes = new Uint8Array(buffer);
|
117 |
+
let binary = '';
|
118 |
+
for (let i = 0; i < bytes.byteLength; i++) {
|
119 |
+
binary += String.fromCharCode(bytes[i]);
|
120 |
+
}
|
121 |
+
return btoa(binary);
|
122 |
}
|
123 |
+
}
|