web-audio-recorder-ts 1.0.0
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.
- package/LICENSE +62 -0
- package/README.md +261 -0
- package/dist/core/WebAudioRecorder.d.ts +85 -0
- package/dist/core/WebAudioRecorder.d.ts.map +1 -0
- package/dist/core/types.d.ts +109 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/encoders/Mp3LameEncoder.d.ts +48 -0
- package/dist/encoders/Mp3LameEncoder.d.ts.map +1 -0
- package/dist/encoders/OggVorbisEncoder.d.ts +48 -0
- package/dist/encoders/OggVorbisEncoder.d.ts.map +1 -0
- package/dist/encoders/WavAudioEncoder.d.ts +47 -0
- package/dist/encoders/WavAudioEncoder.d.ts.map +1 -0
- package/dist/index.cjs.js +770 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +760 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.umd.js +776 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/lib/Mp3LameEncoder.min.js +18 -0
- package/dist/lib/Mp3LameEncoder.min.js.mem +0 -0
- package/dist/lib/OggVorbisEncoder.min.js +17 -0
- package/dist/lib/OggVorbisEncoder.min.js.mem +0 -0
- package/dist/lib/WavAudioEncoder.min.js +1 -0
- package/dist/recorders/WebAudioRecorderMp3.d.ts +29 -0
- package/dist/recorders/WebAudioRecorderMp3.d.ts.map +1 -0
- package/dist/recorders/WebAudioRecorderOgg.d.ts +29 -0
- package/dist/recorders/WebAudioRecorderOgg.d.ts.map +1 -0
- package/dist/recorders/WebAudioRecorderWav.d.ts +20 -0
- package/dist/recorders/WebAudioRecorderWav.d.ts.map +1 -0
- package/lib/Mp3LameEncoder.min.js +18 -0
- package/lib/Mp3LameEncoder.min.js.mem +0 -0
- package/lib/OggVorbisEncoder.min.js +17 -0
- package/lib/OggVorbisEncoder.min.js.mem +0 -0
- package/lib/WavAudioEncoder.min.js +1 -0
- package/package.json +62 -0
- package/types/mp3-lame-encoder.d.ts +28 -0
- package/types/ogg-vorbis-encoder.d.ts +28 -0
- package/types/wav-audio-encoder.d.ts +17 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tipos e interfaces principais para WebAudioRecorder
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Formatos de áudio suportados
|
|
8
|
+
*/
|
|
9
|
+
exports.AudioFormat = void 0;
|
|
10
|
+
(function (AudioFormat) {
|
|
11
|
+
AudioFormat["WAV"] = "wav";
|
|
12
|
+
AudioFormat["OGG"] = "ogg";
|
|
13
|
+
AudioFormat["MP3"] = "mp3";
|
|
14
|
+
})(exports.AudioFormat || (exports.AudioFormat = {}));
|
|
15
|
+
/**
|
|
16
|
+
* Status do recorder
|
|
17
|
+
*/
|
|
18
|
+
exports.RecorderStatus = void 0;
|
|
19
|
+
(function (RecorderStatus) {
|
|
20
|
+
RecorderStatus["INACTIVE"] = "inactive";
|
|
21
|
+
RecorderStatus["RECORDING"] = "recording";
|
|
22
|
+
RecorderStatus["PAUSED"] = "paused";
|
|
23
|
+
RecorderStatus["PROCESSING"] = "processing";
|
|
24
|
+
RecorderStatus["COMPLETE"] = "complete";
|
|
25
|
+
RecorderStatus["ERROR"] = "error";
|
|
26
|
+
})(exports.RecorderStatus || (exports.RecorderStatus = {}));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Classe base WebAudioRecorder para gravação de áudio
|
|
30
|
+
*
|
|
31
|
+
* @module core/WebAudioRecorder
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Classe principal para gravação de áudio usando Web Audio API
|
|
35
|
+
*/
|
|
36
|
+
class WebAudioRecorder {
|
|
37
|
+
/**
|
|
38
|
+
* Cria uma instância do WebAudioRecorder
|
|
39
|
+
*
|
|
40
|
+
* @param audioContext - Contexto de áudio Web Audio API
|
|
41
|
+
* @param encoder - Encoder de áudio a ser usado
|
|
42
|
+
* @param options - Opções de configuração do recorder
|
|
43
|
+
*/
|
|
44
|
+
constructor(audioContext, encoder, options = {}) {
|
|
45
|
+
this.audioContext = null;
|
|
46
|
+
this.sourceNode = null;
|
|
47
|
+
this.scriptProcessor = null;
|
|
48
|
+
this.encoder = null;
|
|
49
|
+
this.stream = null;
|
|
50
|
+
this.status = exports.RecorderStatus.INACTIVE;
|
|
51
|
+
this.sampleRate = 44100;
|
|
52
|
+
this.numChannels = 2;
|
|
53
|
+
this.bufferSize = 4096;
|
|
54
|
+
this.startTime = 0;
|
|
55
|
+
this.onDataAvailableCallback = null;
|
|
56
|
+
this.onCompleteCallback = null;
|
|
57
|
+
this.onErrorCallback = null;
|
|
58
|
+
this.audioContext = audioContext;
|
|
59
|
+
this.encoder = encoder;
|
|
60
|
+
this.sampleRate = options.sampleRate || this.audioContext.sampleRate;
|
|
61
|
+
this.numChannels = options.numChannels || 2;
|
|
62
|
+
this.bufferSize = options.bufferSize || 4096;
|
|
63
|
+
this.onDataAvailableCallback = options.onDataAvailable || null;
|
|
64
|
+
this.onCompleteCallback = options.onComplete || null;
|
|
65
|
+
this.onErrorCallback = options.onError || null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Inicia a gravação de áudio
|
|
69
|
+
*
|
|
70
|
+
* @param stream - Stream de mídia a ser gravado
|
|
71
|
+
* @returns Promise que resolve quando a gravação inicia
|
|
72
|
+
*/
|
|
73
|
+
async start(stream) {
|
|
74
|
+
if (this.status === exports.RecorderStatus.RECORDING) {
|
|
75
|
+
throw new Error('Recording is already in progress');
|
|
76
|
+
}
|
|
77
|
+
if (this.status === exports.RecorderStatus.PROCESSING) {
|
|
78
|
+
throw new Error('Previous recording is still being processed');
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
this.stream = stream;
|
|
82
|
+
this.status = exports.RecorderStatus.RECORDING;
|
|
83
|
+
this.startTime = Date.now();
|
|
84
|
+
if (!this.audioContext) {
|
|
85
|
+
throw new Error('AudioContext is not initialized');
|
|
86
|
+
}
|
|
87
|
+
// Criar source node a partir do stream
|
|
88
|
+
this.sourceNode = this.audioContext.createMediaStreamSource(stream);
|
|
89
|
+
// Criar script processor para capturar dados de áudio
|
|
90
|
+
this.scriptProcessor = this.audioContext.createScriptProcessor(this.bufferSize, this.numChannels, this.numChannels);
|
|
91
|
+
// Conectar os nós
|
|
92
|
+
this.sourceNode.connect(this.scriptProcessor);
|
|
93
|
+
this.scriptProcessor.connect(this.audioContext.destination);
|
|
94
|
+
// Configurar callback para processar dados de áudio
|
|
95
|
+
this.scriptProcessor.onaudioprocess = (event) => {
|
|
96
|
+
if (this.status !== exports.RecorderStatus.RECORDING) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const inputBuffer = event.inputBuffer;
|
|
101
|
+
const buffers = [];
|
|
102
|
+
// Extrair dados de cada canal
|
|
103
|
+
for (let channel = 0; channel < this.numChannels; channel++) {
|
|
104
|
+
const channelData = inputBuffer.getChannelData(channel);
|
|
105
|
+
buffers.push(new Float32Array(channelData));
|
|
106
|
+
}
|
|
107
|
+
// Codificar os dados
|
|
108
|
+
if (this.encoder) {
|
|
109
|
+
this.encoder.encode(buffers);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.handleError(error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
this.status = exports.RecorderStatus.ERROR;
|
|
119
|
+
this.handleError(error);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Para a gravação e finaliza o arquivo de áudio
|
|
125
|
+
*
|
|
126
|
+
* @param mimeType - Tipo MIME do arquivo (padrão: baseado no encoder)
|
|
127
|
+
* @returns Promise que resolve com o Blob do áudio gravado
|
|
128
|
+
*/
|
|
129
|
+
async stop(mimeType) {
|
|
130
|
+
if (this.status !== exports.RecorderStatus.RECORDING) {
|
|
131
|
+
throw new Error('No recording in progress');
|
|
132
|
+
}
|
|
133
|
+
this.status = exports.RecorderStatus.PROCESSING;
|
|
134
|
+
try {
|
|
135
|
+
// Desconectar nós
|
|
136
|
+
if (this.scriptProcessor) {
|
|
137
|
+
this.scriptProcessor.disconnect();
|
|
138
|
+
this.scriptProcessor.onaudioprocess = null;
|
|
139
|
+
this.scriptProcessor = null;
|
|
140
|
+
}
|
|
141
|
+
if (this.sourceNode) {
|
|
142
|
+
this.sourceNode.disconnect();
|
|
143
|
+
this.sourceNode = null;
|
|
144
|
+
}
|
|
145
|
+
// Finalizar encoding
|
|
146
|
+
if (!this.encoder) {
|
|
147
|
+
throw new Error('Encoder is not initialized');
|
|
148
|
+
}
|
|
149
|
+
const blob = this.encoder.finish(mimeType);
|
|
150
|
+
const url = URL.createObjectURL(blob);
|
|
151
|
+
const timecode = Date.now() - this.startTime;
|
|
152
|
+
// Criar evento de conclusão
|
|
153
|
+
const completeEvent = {
|
|
154
|
+
blob,
|
|
155
|
+
url
|
|
156
|
+
};
|
|
157
|
+
this.status = exports.RecorderStatus.COMPLETE;
|
|
158
|
+
// Chamar callback de conclusão
|
|
159
|
+
if (this.onCompleteCallback) {
|
|
160
|
+
this.onCompleteCallback(completeEvent);
|
|
161
|
+
}
|
|
162
|
+
// Criar evento de dados disponíveis
|
|
163
|
+
const dataEvent = {
|
|
164
|
+
data: blob,
|
|
165
|
+
timecode
|
|
166
|
+
};
|
|
167
|
+
if (this.onDataAvailableCallback) {
|
|
168
|
+
this.onDataAvailableCallback(dataEvent);
|
|
169
|
+
}
|
|
170
|
+
return blob;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
this.status = exports.RecorderStatus.ERROR;
|
|
174
|
+
this.handleError(error);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Cancela a gravação atual
|
|
180
|
+
*/
|
|
181
|
+
cancel() {
|
|
182
|
+
if (this.status === exports.RecorderStatus.INACTIVE || this.status === exports.RecorderStatus.COMPLETE) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Desconectar nós
|
|
186
|
+
if (this.scriptProcessor) {
|
|
187
|
+
this.scriptProcessor.disconnect();
|
|
188
|
+
this.scriptProcessor.onaudioprocess = null;
|
|
189
|
+
this.scriptProcessor = null;
|
|
190
|
+
}
|
|
191
|
+
if (this.sourceNode) {
|
|
192
|
+
this.sourceNode.disconnect();
|
|
193
|
+
this.sourceNode = null;
|
|
194
|
+
}
|
|
195
|
+
// Cancelar encoding
|
|
196
|
+
if (this.encoder) {
|
|
197
|
+
this.encoder.cancel();
|
|
198
|
+
}
|
|
199
|
+
// Limpar stream
|
|
200
|
+
if (this.stream) {
|
|
201
|
+
this.stream.getTracks().forEach(track => track.stop());
|
|
202
|
+
this.stream = null;
|
|
203
|
+
}
|
|
204
|
+
this.status = exports.RecorderStatus.INACTIVE;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Obtém o status atual do recorder
|
|
208
|
+
*
|
|
209
|
+
* @returns Status atual
|
|
210
|
+
*/
|
|
211
|
+
getStatus() {
|
|
212
|
+
return this.status;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Define callback para quando dados estão disponíveis
|
|
216
|
+
*
|
|
217
|
+
* @param callback - Função callback
|
|
218
|
+
*/
|
|
219
|
+
setOnDataAvailable(callback) {
|
|
220
|
+
this.onDataAvailableCallback = callback;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Define callback para quando gravação é completada
|
|
224
|
+
*
|
|
225
|
+
* @param callback - Função callback
|
|
226
|
+
*/
|
|
227
|
+
setOnComplete(callback) {
|
|
228
|
+
this.onCompleteCallback = callback;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Define callback para quando ocorre erro
|
|
232
|
+
*
|
|
233
|
+
* @param callback - Função callback
|
|
234
|
+
*/
|
|
235
|
+
setOnError(callback) {
|
|
236
|
+
this.onErrorCallback = callback;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Trata erros e chama callback de erro
|
|
240
|
+
*
|
|
241
|
+
* @param error - Erro ocorrido
|
|
242
|
+
*/
|
|
243
|
+
handleError(error) {
|
|
244
|
+
const errorEvent = {
|
|
245
|
+
message: error.message,
|
|
246
|
+
error
|
|
247
|
+
};
|
|
248
|
+
if (this.onErrorCallback) {
|
|
249
|
+
this.onErrorCallback(errorEvent);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Limpa recursos e reseta o recorder
|
|
254
|
+
*/
|
|
255
|
+
cleanup() {
|
|
256
|
+
this.cancel();
|
|
257
|
+
this.encoder = null;
|
|
258
|
+
this.audioContext = null;
|
|
259
|
+
this.status = exports.RecorderStatus.INACTIVE;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Encoder WAV para áudio
|
|
265
|
+
*
|
|
266
|
+
* @module encoders/WavAudioEncoder
|
|
267
|
+
*/
|
|
268
|
+
/**
|
|
269
|
+
* Encoder WAV simples que cria arquivos WAV a partir de buffers de áudio
|
|
270
|
+
*/
|
|
271
|
+
class WavAudioEncoder {
|
|
272
|
+
/**
|
|
273
|
+
* Cria uma instância do encoder WAV
|
|
274
|
+
*
|
|
275
|
+
* @param sampleRate - Taxa de amostragem em Hz
|
|
276
|
+
* @param numChannels - Número de canais (1 = mono, 2 = estéreo)
|
|
277
|
+
*/
|
|
278
|
+
constructor(sampleRate, numChannels) {
|
|
279
|
+
this.buffers = [];
|
|
280
|
+
this.sampleRate = sampleRate;
|
|
281
|
+
this.numChannels = numChannels;
|
|
282
|
+
this.buffers = [];
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Codifica buffers de áudio
|
|
286
|
+
*
|
|
287
|
+
* @param buffers - Array de buffers Float32Array, um por canal
|
|
288
|
+
*/
|
|
289
|
+
encode(buffers) {
|
|
290
|
+
if (buffers.length !== this.numChannels) {
|
|
291
|
+
throw new Error(`Expected ${this.numChannels} channels, got ${buffers.length}`);
|
|
292
|
+
}
|
|
293
|
+
// Armazenar buffers para processamento posterior
|
|
294
|
+
this.buffers.push(buffers.map(buffer => new Float32Array(buffer)));
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Finaliza o encoding e retorna o Blob WAV
|
|
298
|
+
*
|
|
299
|
+
* @param mimeType - Tipo MIME (padrão: 'audio/wav')
|
|
300
|
+
* @returns Blob contendo o arquivo WAV
|
|
301
|
+
*/
|
|
302
|
+
finish(mimeType = 'audio/wav') {
|
|
303
|
+
if (this.buffers.length === 0) {
|
|
304
|
+
throw new Error('No audio data to encode');
|
|
305
|
+
}
|
|
306
|
+
// Calcular tamanho total somando todos os frames de todos os buffers
|
|
307
|
+
let totalFrames = 0;
|
|
308
|
+
for (let i = 0; i < this.buffers.length; i++) {
|
|
309
|
+
if (this.buffers[i].length > 0 && this.buffers[i][0].length > 0) {
|
|
310
|
+
totalFrames += this.buffers[i][0].length;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (totalFrames === 0) {
|
|
314
|
+
throw new Error('No valid audio data to encode');
|
|
315
|
+
}
|
|
316
|
+
// Calcular tamanho dos dados (16-bit samples = 2 bytes por sample)
|
|
317
|
+
const dataSize = totalFrames * this.numChannels * 2;
|
|
318
|
+
const fileSize = 44 + dataSize; // 44 bytes de cabeçalho + dados
|
|
319
|
+
// Criar buffer para o arquivo WAV
|
|
320
|
+
const buffer = new ArrayBuffer(fileSize);
|
|
321
|
+
const view = new DataView(buffer);
|
|
322
|
+
// Escrever cabeçalho WAV
|
|
323
|
+
// RIFF header
|
|
324
|
+
this.writeString(view, 0, 'RIFF');
|
|
325
|
+
view.setUint32(4, fileSize - 8, true); // Tamanho do arquivo - 8 bytes do header RIFF
|
|
326
|
+
this.writeString(view, 8, 'WAVE');
|
|
327
|
+
// fmt chunk
|
|
328
|
+
this.writeString(view, 12, 'fmt ');
|
|
329
|
+
view.setUint32(16, 16, true); // fmt chunk size
|
|
330
|
+
view.setUint16(20, 1, true); // audio format (1 = PCM)
|
|
331
|
+
view.setUint16(22, this.numChannels, true);
|
|
332
|
+
view.setUint32(24, this.sampleRate, true);
|
|
333
|
+
view.setUint32(28, this.sampleRate * this.numChannels * 2, true); // byte rate
|
|
334
|
+
view.setUint16(32, this.numChannels * 2, true); // block align
|
|
335
|
+
view.setUint16(34, 16, true); // bits per sample
|
|
336
|
+
// data chunk
|
|
337
|
+
this.writeString(view, 36, 'data');
|
|
338
|
+
view.setUint32(40, dataSize, true);
|
|
339
|
+
// Escrever dados de áudio (intercalados para estéreo)
|
|
340
|
+
let offset = 44;
|
|
341
|
+
for (let i = 0; i < this.buffers.length; i++) {
|
|
342
|
+
const frameBuffers = this.buffers[i];
|
|
343
|
+
// Verificar se há dados válidos
|
|
344
|
+
if (frameBuffers.length === 0 || frameBuffers[0].length === 0) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const frameLength = frameBuffers[0].length;
|
|
348
|
+
// Verificar se todos os canais têm o mesmo tamanho
|
|
349
|
+
for (let channel = 0; channel < this.numChannels; channel++) {
|
|
350
|
+
if (frameBuffers[channel].length !== frameLength) {
|
|
351
|
+
throw new Error(`Channel ${channel} has different length in frame ${i}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
for (let j = 0; j < frameLength; j++) {
|
|
355
|
+
for (let channel = 0; channel < this.numChannels; channel++) {
|
|
356
|
+
// Verificar se o offset está dentro dos limites
|
|
357
|
+
if (offset + 2 > buffer.byteLength) {
|
|
358
|
+
throw new Error(`Offset ${offset + 2} exceeds buffer size ${buffer.byteLength}`);
|
|
359
|
+
}
|
|
360
|
+
// Converter float32 (-1.0 a 1.0) para int16 (-32768 a 32767)
|
|
361
|
+
const sample = Math.max(-1, Math.min(1, frameBuffers[channel][j]));
|
|
362
|
+
const int16Sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
|
|
363
|
+
view.setInt16(offset, int16Sample, true);
|
|
364
|
+
offset += 2;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Verificar se escrevemos todos os dados
|
|
369
|
+
if (offset !== buffer.byteLength) {
|
|
370
|
+
console.warn(`Warning: Expected to write ${buffer.byteLength} bytes, but wrote ${offset} bytes`);
|
|
371
|
+
}
|
|
372
|
+
// Limpar buffers
|
|
373
|
+
this.buffers = [];
|
|
374
|
+
return new Blob([buffer], { type: mimeType });
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Cancela o encoding e limpa os buffers
|
|
378
|
+
*/
|
|
379
|
+
cancel() {
|
|
380
|
+
this.buffers = [];
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Escreve string no DataView
|
|
384
|
+
*
|
|
385
|
+
* @param view - DataView
|
|
386
|
+
* @param offset - Offset inicial
|
|
387
|
+
* @param string - String a escrever
|
|
388
|
+
*/
|
|
389
|
+
writeString(view, offset, string) {
|
|
390
|
+
for (let i = 0; i < string.length; i++) {
|
|
391
|
+
view.setUint8(offset + i, string.charCodeAt(i));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Wrapper TypeScript para OggVorbisEncoder (Emscripten)
|
|
398
|
+
*
|
|
399
|
+
* @module encoders/OggVorbisEncoder
|
|
400
|
+
*/
|
|
401
|
+
/**
|
|
402
|
+
* Wrapper para o encoder OGG Vorbis compilado via Emscripten
|
|
403
|
+
*/
|
|
404
|
+
class OggVorbisEncoderWrapper {
|
|
405
|
+
/**
|
|
406
|
+
* Cria uma instância do encoder OGG Vorbis
|
|
407
|
+
*
|
|
408
|
+
* @param sampleRate - Taxa de amostragem em Hz
|
|
409
|
+
* @param numChannels - Número de canais
|
|
410
|
+
* @param options - Opções do encoder OGG
|
|
411
|
+
*/
|
|
412
|
+
constructor(sampleRate, numChannels, options = {}) {
|
|
413
|
+
this.encoder = null;
|
|
414
|
+
this.sampleRate = sampleRate;
|
|
415
|
+
this.numChannels = numChannels;
|
|
416
|
+
this.quality = options.quality ?? 0.5;
|
|
417
|
+
// Verificar se OggVorbisEncoder está disponível
|
|
418
|
+
if (typeof OggVorbisEncoder === 'undefined') {
|
|
419
|
+
throw new Error('OggVorbisEncoder is not loaded. Make sure to load OggVorbisEncoder.min.js before using this encoder.');
|
|
420
|
+
}
|
|
421
|
+
// Criar instância do encoder
|
|
422
|
+
this.encoder = new OggVorbisEncoder(sampleRate, numChannels, this.quality);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Codifica buffers de áudio
|
|
426
|
+
*
|
|
427
|
+
* @param buffers - Array de buffers Float32Array, um por canal
|
|
428
|
+
*/
|
|
429
|
+
encode(buffers) {
|
|
430
|
+
if (!this.encoder) {
|
|
431
|
+
throw new Error('Encoder is not initialized');
|
|
432
|
+
}
|
|
433
|
+
if (buffers.length !== this.numChannels) {
|
|
434
|
+
throw new Error(`Expected ${this.numChannels} channels, got ${buffers.length}`);
|
|
435
|
+
}
|
|
436
|
+
this.encoder.encode(buffers);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Finaliza o encoding e retorna o Blob OGG
|
|
440
|
+
*
|
|
441
|
+
* @param mimeType - Tipo MIME (padrão: 'audio/ogg')
|
|
442
|
+
* @returns Blob contendo o arquivo OGG
|
|
443
|
+
*/
|
|
444
|
+
finish(mimeType = 'audio/ogg') {
|
|
445
|
+
if (!this.encoder) {
|
|
446
|
+
throw new Error('Encoder is not initialized');
|
|
447
|
+
}
|
|
448
|
+
return this.encoder.finish(mimeType);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Cancela o encoding
|
|
452
|
+
*/
|
|
453
|
+
cancel() {
|
|
454
|
+
if (this.encoder) {
|
|
455
|
+
this.encoder.cancel();
|
|
456
|
+
this.encoder = null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Função helper para carregar o script OggVorbisEncoder
|
|
462
|
+
*
|
|
463
|
+
* @param scriptUrl - URL do script OggVorbisEncoder.min.js
|
|
464
|
+
* @returns Promise que resolve quando o script é carregado
|
|
465
|
+
*/
|
|
466
|
+
function loadOggVorbisEncoder(scriptUrl) {
|
|
467
|
+
return new Promise((resolve, reject) => {
|
|
468
|
+
// Verificar se já está carregado
|
|
469
|
+
if (typeof window.OggVorbisEncoder !== 'undefined') {
|
|
470
|
+
resolve();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// Verificar se o script já está sendo carregado
|
|
474
|
+
const existingScript = document.querySelector(`script[src="${scriptUrl}"]`);
|
|
475
|
+
if (existingScript) {
|
|
476
|
+
// Se já está carregado, verificar imediatamente
|
|
477
|
+
if (typeof window.OggVorbisEncoder !== 'undefined') {
|
|
478
|
+
resolve();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Aguardar o script existente carregar
|
|
482
|
+
existingScript.addEventListener('load', () => {
|
|
483
|
+
setTimeout(() => {
|
|
484
|
+
if (typeof window.OggVorbisEncoder !== 'undefined') {
|
|
485
|
+
resolve();
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
reject(new Error('OggVorbisEncoder failed to load'));
|
|
489
|
+
}
|
|
490
|
+
}, 100);
|
|
491
|
+
});
|
|
492
|
+
existingScript.addEventListener('error', () => {
|
|
493
|
+
reject(new Error('Failed to load OggVorbisEncoder script'));
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Primeiro, verificar se o arquivo existe fazendo uma requisição HEAD
|
|
498
|
+
fetch(scriptUrl, { method: 'HEAD', cache: 'no-cache' })
|
|
499
|
+
.then(response => {
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
throw new Error(`File not found: ${scriptUrl} (${response.status})`);
|
|
502
|
+
}
|
|
503
|
+
// Criar e carregar novo script
|
|
504
|
+
const script = document.createElement('script');
|
|
505
|
+
script.src = scriptUrl;
|
|
506
|
+
script.async = false; // Carregar de forma síncrona para garantir ordem
|
|
507
|
+
script.type = 'text/javascript';
|
|
508
|
+
script.onload = () => {
|
|
509
|
+
// Aguardar um pouco para garantir que o objeto global foi criado
|
|
510
|
+
setTimeout(() => {
|
|
511
|
+
if (typeof window.OggVorbisEncoder !== 'undefined') {
|
|
512
|
+
resolve();
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
reject(new Error('OggVorbisEncoder object not found after script load. The script may not have exported the global correctly.'));
|
|
516
|
+
}
|
|
517
|
+
}, 200);
|
|
518
|
+
};
|
|
519
|
+
script.onerror = (event) => {
|
|
520
|
+
const error = new Error(`Failed to load OggVorbisEncoder script from ${scriptUrl}. Check browser console for CORS or network errors.`);
|
|
521
|
+
console.error('Script load error:', event);
|
|
522
|
+
console.error('Script URL:', scriptUrl);
|
|
523
|
+
reject(error);
|
|
524
|
+
};
|
|
525
|
+
document.head.appendChild(script);
|
|
526
|
+
})
|
|
527
|
+
.catch(error => {
|
|
528
|
+
reject(new Error(`Cannot access OggVorbisEncoder script at ${scriptUrl}: ${error.message}`));
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Wrapper TypeScript para Mp3LameEncoder (Emscripten)
|
|
535
|
+
*
|
|
536
|
+
* @module encoders/Mp3LameEncoder
|
|
537
|
+
*/
|
|
538
|
+
/**
|
|
539
|
+
* Wrapper para o encoder MP3 LAME compilado via Emscripten
|
|
540
|
+
*/
|
|
541
|
+
class Mp3LameEncoderWrapper {
|
|
542
|
+
/**
|
|
543
|
+
* Cria uma instância do encoder MP3 LAME
|
|
544
|
+
*
|
|
545
|
+
* @param sampleRate - Taxa de amostragem em Hz
|
|
546
|
+
* @param numChannels - Número de canais
|
|
547
|
+
* @param options - Opções do encoder MP3
|
|
548
|
+
*/
|
|
549
|
+
constructor(sampleRate, numChannels, options = {}) {
|
|
550
|
+
this.encoder = null;
|
|
551
|
+
this.sampleRate = sampleRate;
|
|
552
|
+
this.numChannels = numChannels;
|
|
553
|
+
this.bitrate = options.bitrate ?? 128;
|
|
554
|
+
// Verificar se Mp3LameEncoder está disponível
|
|
555
|
+
if (typeof Mp3LameEncoder === 'undefined') {
|
|
556
|
+
throw new Error('Mp3LameEncoder is not loaded. Make sure to load Mp3LameEncoder.min.js before using this encoder.');
|
|
557
|
+
}
|
|
558
|
+
// Criar instância do encoder
|
|
559
|
+
this.encoder = new Mp3LameEncoder(sampleRate, numChannels, this.bitrate);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Codifica buffers de áudio
|
|
563
|
+
*
|
|
564
|
+
* @param buffers - Array de buffers Float32Array, um por canal
|
|
565
|
+
*/
|
|
566
|
+
encode(buffers) {
|
|
567
|
+
if (!this.encoder) {
|
|
568
|
+
throw new Error('Encoder is not initialized');
|
|
569
|
+
}
|
|
570
|
+
if (buffers.length !== this.numChannels) {
|
|
571
|
+
throw new Error(`Expected ${this.numChannels} channels, got ${buffers.length}`);
|
|
572
|
+
}
|
|
573
|
+
this.encoder.encode(buffers);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Finaliza o encoding e retorna o Blob MP3
|
|
577
|
+
*
|
|
578
|
+
* @param mimeType - Tipo MIME (padrão: 'audio/mpeg')
|
|
579
|
+
* @returns Blob contendo o arquivo MP3
|
|
580
|
+
*/
|
|
581
|
+
finish(mimeType = 'audio/mpeg') {
|
|
582
|
+
if (!this.encoder) {
|
|
583
|
+
throw new Error('Encoder is not initialized');
|
|
584
|
+
}
|
|
585
|
+
return this.encoder.finish(mimeType);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Cancela o encoding
|
|
589
|
+
*/
|
|
590
|
+
cancel() {
|
|
591
|
+
if (this.encoder) {
|
|
592
|
+
this.encoder.cancel();
|
|
593
|
+
this.encoder = null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Função helper para carregar o script Mp3LameEncoder
|
|
599
|
+
*
|
|
600
|
+
* @param scriptUrl - URL do script Mp3LameEncoder.min.js
|
|
601
|
+
* @returns Promise que resolve quando o script é carregado
|
|
602
|
+
*/
|
|
603
|
+
function loadMp3LameEncoder(scriptUrl) {
|
|
604
|
+
return new Promise((resolve, reject) => {
|
|
605
|
+
// Verificar se já está carregado
|
|
606
|
+
if (typeof window.Mp3LameEncoder !== 'undefined') {
|
|
607
|
+
resolve();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
// Verificar se o script já está sendo carregado
|
|
611
|
+
const existingScript = document.querySelector(`script[src="${scriptUrl}"]`);
|
|
612
|
+
if (existingScript) {
|
|
613
|
+
// Se já está carregado, verificar imediatamente
|
|
614
|
+
if (typeof window.Mp3LameEncoder !== 'undefined') {
|
|
615
|
+
resolve();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
// Aguardar o script existente carregar
|
|
619
|
+
existingScript.addEventListener('load', () => {
|
|
620
|
+
setTimeout(() => {
|
|
621
|
+
if (typeof window.Mp3LameEncoder !== 'undefined') {
|
|
622
|
+
resolve();
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
reject(new Error('Mp3LameEncoder failed to load'));
|
|
626
|
+
}
|
|
627
|
+
}, 100);
|
|
628
|
+
});
|
|
629
|
+
existingScript.addEventListener('error', () => {
|
|
630
|
+
reject(new Error('Failed to load Mp3LameEncoder script'));
|
|
631
|
+
});
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// Primeiro, verificar se o arquivo existe fazendo uma requisição HEAD
|
|
635
|
+
fetch(scriptUrl, { method: 'HEAD', cache: 'no-cache' })
|
|
636
|
+
.then(response => {
|
|
637
|
+
if (!response.ok) {
|
|
638
|
+
throw new Error(`File not found: ${scriptUrl} (${response.status})`);
|
|
639
|
+
}
|
|
640
|
+
// Criar e carregar novo script
|
|
641
|
+
const script = document.createElement('script');
|
|
642
|
+
script.src = scriptUrl;
|
|
643
|
+
script.async = false; // Carregar de forma síncrona para garantir ordem
|
|
644
|
+
script.type = 'text/javascript';
|
|
645
|
+
script.onload = () => {
|
|
646
|
+
// Aguardar um pouco para garantir que o objeto global foi criado
|
|
647
|
+
setTimeout(() => {
|
|
648
|
+
if (typeof window.Mp3LameEncoder !== 'undefined') {
|
|
649
|
+
resolve();
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
reject(new Error('Mp3LameEncoder object not found after script load. The script may not have exported the global correctly.'));
|
|
653
|
+
}
|
|
654
|
+
}, 200);
|
|
655
|
+
};
|
|
656
|
+
script.onerror = (event) => {
|
|
657
|
+
const error = new Error(`Failed to load Mp3LameEncoder script from ${scriptUrl}. Check browser console for CORS or network errors.`);
|
|
658
|
+
console.error('Script load error:', event);
|
|
659
|
+
console.error('Script URL:', scriptUrl);
|
|
660
|
+
reject(error);
|
|
661
|
+
};
|
|
662
|
+
document.head.appendChild(script);
|
|
663
|
+
})
|
|
664
|
+
.catch(error => {
|
|
665
|
+
reject(new Error(`Cannot access Mp3LameEncoder script at ${scriptUrl}: ${error.message}`));
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Recorder WAV usando WebAudioRecorder
|
|
672
|
+
*
|
|
673
|
+
* @module recorders/WebAudioRecorderWav
|
|
674
|
+
*/
|
|
675
|
+
/**
|
|
676
|
+
* Classe para gravação de áudio em formato WAV
|
|
677
|
+
*/
|
|
678
|
+
class WebAudioRecorderWav extends WebAudioRecorder {
|
|
679
|
+
/**
|
|
680
|
+
* Cria uma instância do recorder WAV
|
|
681
|
+
*
|
|
682
|
+
* @param audioContext - Contexto de áudio Web Audio API
|
|
683
|
+
* @param options - Opções de configuração do recorder
|
|
684
|
+
*/
|
|
685
|
+
constructor(audioContext, options = {}) {
|
|
686
|
+
const sampleRate = options.sampleRate || audioContext.sampleRate;
|
|
687
|
+
const numChannels = options.numChannels || 2;
|
|
688
|
+
const encoder = new WavAudioEncoder(sampleRate, numChannels);
|
|
689
|
+
super(audioContext, encoder, options);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Recorder OGG Vorbis usando WebAudioRecorder
|
|
695
|
+
*
|
|
696
|
+
* @module recorders/WebAudioRecorderOgg
|
|
697
|
+
*/
|
|
698
|
+
/**
|
|
699
|
+
* Classe para gravação de áudio em formato OGG Vorbis
|
|
700
|
+
*/
|
|
701
|
+
class WebAudioRecorderOgg extends WebAudioRecorder {
|
|
702
|
+
/**
|
|
703
|
+
* Cria uma instância do recorder OGG Vorbis
|
|
704
|
+
*
|
|
705
|
+
* @param audioContext - Contexto de áudio Web Audio API
|
|
706
|
+
* @param options - Opções de configuração do recorder
|
|
707
|
+
* @param oggOptions - Opções específicas do encoder OGG
|
|
708
|
+
*/
|
|
709
|
+
constructor(audioContext, options = {}, oggOptions = {}) {
|
|
710
|
+
const sampleRate = options.sampleRate || audioContext.sampleRate;
|
|
711
|
+
const numChannels = options.numChannels || 2;
|
|
712
|
+
const encoder = new OggVorbisEncoderWrapper(sampleRate, numChannels, oggOptions);
|
|
713
|
+
super(audioContext, encoder, options);
|
|
714
|
+
this.oggOptions = oggOptions;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Carrega o script OggVorbisEncoder antes de usar
|
|
718
|
+
*
|
|
719
|
+
* @param scriptUrl - URL do script OggVorbisEncoder.min.js
|
|
720
|
+
* @returns Promise que resolve quando o script é carregado
|
|
721
|
+
*/
|
|
722
|
+
static async loadEncoder(scriptUrl) {
|
|
723
|
+
return loadOggVorbisEncoder(scriptUrl);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Recorder MP3 usando WebAudioRecorder
|
|
729
|
+
*
|
|
730
|
+
* @module recorders/WebAudioRecorderMp3
|
|
731
|
+
*/
|
|
732
|
+
/**
|
|
733
|
+
* Classe para gravação de áudio em formato MP3
|
|
734
|
+
*/
|
|
735
|
+
class WebAudioRecorderMp3 extends WebAudioRecorder {
|
|
736
|
+
/**
|
|
737
|
+
* Cria uma instância do recorder MP3
|
|
738
|
+
*
|
|
739
|
+
* @param audioContext - Contexto de áudio Web Audio API
|
|
740
|
+
* @param options - Opções de configuração do recorder
|
|
741
|
+
* @param mp3Options - Opções específicas do encoder MP3
|
|
742
|
+
*/
|
|
743
|
+
constructor(audioContext, options = {}, mp3Options = {}) {
|
|
744
|
+
const sampleRate = options.sampleRate || audioContext.sampleRate;
|
|
745
|
+
const numChannels = options.numChannels || 2;
|
|
746
|
+
const encoder = new Mp3LameEncoderWrapper(sampleRate, numChannels, mp3Options);
|
|
747
|
+
super(audioContext, encoder, options);
|
|
748
|
+
this.mp3Options = mp3Options;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Carrega o script Mp3LameEncoder antes de usar
|
|
752
|
+
*
|
|
753
|
+
* @param scriptUrl - URL do script Mp3LameEncoder.min.js
|
|
754
|
+
* @returns Promise que resolve quando o script é carregado
|
|
755
|
+
*/
|
|
756
|
+
static async loadEncoder(scriptUrl) {
|
|
757
|
+
return loadMp3LameEncoder(scriptUrl);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
exports.Mp3LameEncoderWrapper = Mp3LameEncoderWrapper;
|
|
762
|
+
exports.OggVorbisEncoderWrapper = OggVorbisEncoderWrapper;
|
|
763
|
+
exports.WavAudioEncoder = WavAudioEncoder;
|
|
764
|
+
exports.WebAudioRecorder = WebAudioRecorder;
|
|
765
|
+
exports.WebAudioRecorderMp3 = WebAudioRecorderMp3;
|
|
766
|
+
exports.WebAudioRecorderOgg = WebAudioRecorderOgg;
|
|
767
|
+
exports.WebAudioRecorderWav = WebAudioRecorderWav;
|
|
768
|
+
exports.loadMp3LameEncoder = loadMp3LameEncoder;
|
|
769
|
+
exports.loadOggVorbisEncoder = loadOggVorbisEncoder;
|
|
770
|
+
//# sourceMappingURL=index.cjs.js.map
|