web-audio-recorder-ts 1.0.6 → 1.0.7
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/README.md +9 -1
- package/dist/encoders/OggVorbisEncoder.d.ts +0 -5
- package/dist/index.cjs.js +262 -288
- package/dist/index.esm.js +262 -287
- package/dist/index.umd.js +262 -288
- package/dist/utils/encoderLoader.d.ts +8 -8
- package/package.json +2 -2
package/dist/index.umd.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WebAudioRecorder = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
-
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
7
|
/**
|
|
9
8
|
* Tipos e interfaces principais para WebAudioRecorder
|
|
10
9
|
*/
|
|
@@ -91,6 +90,27 @@
|
|
|
91
90
|
}
|
|
92
91
|
// Criar source node a partir do stream
|
|
93
92
|
this.sourceNode = this.audioContext.createMediaStreamSource(stream);
|
|
93
|
+
// Detectar número real de canais do stream
|
|
94
|
+
let detectedChannels = this.numChannels;
|
|
95
|
+
const audioTracks = stream.getAudioTracks();
|
|
96
|
+
if (audioTracks.length > 0) {
|
|
97
|
+
// Tentar obter do getSettings (navegadores modernos)
|
|
98
|
+
const settings = audioTracks[0].getSettings();
|
|
99
|
+
if (settings.channelCount) {
|
|
100
|
+
detectedChannels = settings.channelCount;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Fallback: assumir que a maioria dos microfones é mono (1) ou usar padrão (2)
|
|
104
|
+
// Mas como não podemos ter certeza sem processar, vamos confiar na configuração
|
|
105
|
+
// ou no padrão do AudioContext
|
|
106
|
+
console.log('Channel count not available in track settings, using default/configured:', this.numChannels);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Atualizar numChannels se necessário
|
|
110
|
+
if (detectedChannels !== this.numChannels) {
|
|
111
|
+
console.log(`Detected ${detectedChannels} channel(s) in stream, adjusting from ${this.numChannels} to ${detectedChannels}`);
|
|
112
|
+
this.numChannels = detectedChannels;
|
|
113
|
+
}
|
|
94
114
|
// Criar script processor para capturar dados de áudio
|
|
95
115
|
this.scriptProcessor = this.audioContext.createScriptProcessor(this.bufferSize, this.numChannels, this.numChannels);
|
|
96
116
|
// Conectar os nós
|
|
@@ -110,6 +130,7 @@
|
|
|
110
130
|
const channelData = inputBuffer.getChannelData(channel);
|
|
111
131
|
buffers.push(new Float32Array(channelData));
|
|
112
132
|
}
|
|
133
|
+
// Garantir que temos exatamente o número de canais esperado
|
|
113
134
|
// Se o número de canais do buffer não corresponde ao esperado, ajustar
|
|
114
135
|
if (actualChannels !== this.numChannels) {
|
|
115
136
|
if (actualChannels === 1 && this.numChannels === 2) {
|
|
@@ -120,17 +141,49 @@
|
|
|
120
141
|
// Estéreo -> Mono: usar apenas o primeiro canal
|
|
121
142
|
buffers.splice(1);
|
|
122
143
|
}
|
|
123
|
-
else {
|
|
124
|
-
//
|
|
144
|
+
else if (actualChannels < this.numChannels) {
|
|
145
|
+
// Menos canais do que esperado: duplicar o último canal
|
|
125
146
|
while (buffers.length < this.numChannels && buffers.length > 0) {
|
|
126
|
-
// Duplicar o último canal se necessário
|
|
127
147
|
buffers.push(new Float32Array(buffers[buffers.length - 1]));
|
|
128
148
|
}
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
}
|
|
150
|
+
else if (actualChannels > this.numChannels) {
|
|
151
|
+
// Mais canais do que esperado: usar apenas os primeiros
|
|
152
|
+
buffers.splice(this.numChannels);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Validação final CRÍTICA: garantir que temos exatamente o número correto de canais
|
|
156
|
+
// O encoder Emscripten é muito sensível e aborta se o número de canais não corresponder
|
|
157
|
+
if (buffers.length !== this.numChannels) {
|
|
158
|
+
console.warn(`Channel mismatch detected: Expected ${this.numChannels} channels, ` +
|
|
159
|
+
`got ${buffers.length} after adjustment. ` +
|
|
160
|
+
`Actual input channels: ${actualChannels}. ` +
|
|
161
|
+
`Fixing by ${buffers.length < this.numChannels ? 'duplicating' : 'removing'} channels.`);
|
|
162
|
+
// Tentar corrigir: duplicar ou remover canais conforme necessário
|
|
163
|
+
while (buffers.length < this.numChannels) {
|
|
164
|
+
if (buffers.length > 0) {
|
|
165
|
+
// Duplicar o primeiro canal (ou último se houver mais de um)
|
|
166
|
+
const sourceChannel = buffers[buffers.length - 1];
|
|
167
|
+
buffers.push(new Float32Array(sourceChannel));
|
|
131
168
|
}
|
|
169
|
+
else {
|
|
170
|
+
// Se não há buffers, criar um buffer vazio (não deveria acontecer)
|
|
171
|
+
console.error('No buffers available! Creating empty buffer.');
|
|
172
|
+
buffers.push(new Float32Array(inputBuffer.length));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Remover canais extras se houver mais do que o esperado
|
|
176
|
+
if (buffers.length > this.numChannels) {
|
|
177
|
+
buffers.splice(this.numChannels);
|
|
132
178
|
}
|
|
133
179
|
}
|
|
180
|
+
// Validação final absoluta: se ainda não temos o número correto, não codificar
|
|
181
|
+
if (buffers.length !== this.numChannels) {
|
|
182
|
+
console.error(`CRITICAL: Failed to fix channel mismatch. ` +
|
|
183
|
+
`Expected ${this.numChannels}, got ${buffers.length}. ` +
|
|
184
|
+
`Skipping this buffer to prevent encoder abort().`);
|
|
185
|
+
return; // Não codificar este buffer para evitar abort()
|
|
186
|
+
}
|
|
134
187
|
// Codificar os dados
|
|
135
188
|
if (this.encoder && buffers.length > 0) {
|
|
136
189
|
this.encoder.encode(buffers);
|
|
@@ -424,233 +477,107 @@
|
|
|
424
477
|
}
|
|
425
478
|
|
|
426
479
|
/**
|
|
427
|
-
* Utility functions for
|
|
480
|
+
* Utility functions for loading encoder files
|
|
481
|
+
*
|
|
482
|
+
* STANDARD: Files should be placed in `public/encoders/`
|
|
428
483
|
*/
|
|
429
484
|
/**
|
|
430
|
-
* Get the base URL for encoder files
|
|
431
|
-
*
|
|
485
|
+
* Get the default base URL for encoder files
|
|
486
|
+
* Defaults to '/encoders' (relative to domain root)
|
|
432
487
|
*/
|
|
433
488
|
function getEncoderBaseUrl() {
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
try {
|
|
437
|
-
// @ts-ignore - import.meta may not exist in all environments
|
|
438
|
-
if (typeof ({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.umd.js', document.baseURI).href)) }) !== 'undefined' && (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.umd.js', document.baseURI).href))) {
|
|
439
|
-
try {
|
|
440
|
-
// @ts-ignore
|
|
441
|
-
const url = new URL((typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.umd.js', document.baseURI).href)));
|
|
442
|
-
// If running from node_modules, construct path to lib
|
|
443
|
-
if (url.pathname.includes('node_modules')) {
|
|
444
|
-
const packagePath = url.pathname.split('node_modules/')[1];
|
|
445
|
-
const packageName = packagePath.split('/')[0];
|
|
446
|
-
// Return relative path from package root
|
|
447
|
-
return `/node_modules/${packageName}/lib`;
|
|
448
|
-
}
|
|
449
|
-
// If running from dist, go up to lib
|
|
450
|
-
if (url.pathname.includes('/dist/')) {
|
|
451
|
-
return url.pathname.replace('/dist/', '/lib/').replace(/\/[^/]+$/, '');
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
catch (e) {
|
|
455
|
-
// Fall through to other methods
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
catch (e) {
|
|
460
|
-
// import.meta not available, continue to other methods
|
|
461
|
-
}
|
|
462
|
-
// Try to detect from __dirname (CJS/Node.js)
|
|
463
|
-
if (typeof __dirname !== 'undefined') {
|
|
464
|
-
try {
|
|
465
|
-
// If in node_modules, construct path
|
|
466
|
-
if (__dirname.includes('node_modules')) {
|
|
467
|
-
const parts = __dirname.split('node_modules/');
|
|
468
|
-
if (parts.length > 1) {
|
|
469
|
-
const packageName = parts[1].split('/')[0];
|
|
470
|
-
// Retornar caminho absoluto se possível
|
|
471
|
-
if (typeof window !== 'undefined') {
|
|
472
|
-
return `${window.location.origin}/node_modules/${packageName}/lib`;
|
|
473
|
-
}
|
|
474
|
-
return `/node_modules/${packageName}/lib`;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// If in dist, go to lib
|
|
478
|
-
if (__dirname.includes('dist')) {
|
|
479
|
-
const libPath = __dirname.replace('dist', 'lib');
|
|
480
|
-
if (typeof window !== 'undefined') {
|
|
481
|
-
return `${window.location.origin}${libPath}`;
|
|
482
|
-
}
|
|
483
|
-
return libPath;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
catch (e) {
|
|
487
|
-
// Fall through
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
// Try to detect from document.currentScript or all scripts (browser)
|
|
491
|
-
if (typeof document !== 'undefined') {
|
|
492
|
-
// Try currentScript first
|
|
493
|
-
let script = document.currentScript;
|
|
494
|
-
// If no currentScript, try to find script with web-audio-recorder in src
|
|
495
|
-
if (!script || !script.src) {
|
|
496
|
-
const scripts = document.querySelectorAll('script[src]');
|
|
497
|
-
for (const s of Array.from(scripts)) {
|
|
498
|
-
const src = s.src;
|
|
499
|
-
if (src.includes('web-audio-recorder')) {
|
|
500
|
-
script = s;
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
if (script && script.src) {
|
|
506
|
-
try {
|
|
507
|
-
const url = new URL(script.src);
|
|
508
|
-
// If from node_modules
|
|
509
|
-
if (url.pathname.includes('node_modules')) {
|
|
510
|
-
const packagePath = url.pathname.split('node_modules/')[1];
|
|
511
|
-
const packageName = packagePath.split('/')[0];
|
|
512
|
-
// Retornar caminho absoluto para node_modules
|
|
513
|
-
return `${url.origin}/node_modules/${packageName}/lib`;
|
|
514
|
-
}
|
|
515
|
-
// If from dist, go to lib
|
|
516
|
-
if (url.pathname.includes('/dist/')) {
|
|
517
|
-
const libPath = url.pathname.replace('/dist/', '/lib/').replace(/\/[^/]+$/, '');
|
|
518
|
-
return `${url.origin}${libPath}`;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
catch (e) {
|
|
522
|
-
// Fall through
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
// Default fallback: try node_modules first, then root
|
|
527
|
-
if (typeof window !== 'undefined') {
|
|
528
|
-
// Priorizar node_modules
|
|
529
|
-
return '/node_modules/web-audio-recorder-ts/lib';
|
|
530
|
-
}
|
|
531
|
-
return '/lib';
|
|
489
|
+
// Simples e direto: padrão é a pasta /encoders na raiz
|
|
490
|
+
return '/encoders';
|
|
532
491
|
}
|
|
533
492
|
/**
|
|
534
493
|
* Get the full URL for an encoder script file
|
|
535
|
-
* @param filename - Name of the encoder file (e.g., 'OggVorbisEncoder.min.js')
|
|
536
|
-
* @param customBaseUrl - Optional custom base URL (overrides auto-detection)
|
|
537
494
|
*/
|
|
538
495
|
function getEncoderScriptUrl(filename, customBaseUrl) {
|
|
539
496
|
const baseUrl = customBaseUrl || getEncoderBaseUrl();
|
|
540
|
-
//
|
|
497
|
+
// Garantir barra no final
|
|
541
498
|
const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
542
|
-
//
|
|
543
|
-
if (normalizedBase.startsWith('http
|
|
499
|
+
// Se baseUrl começa com http/https ou /, retorna como está
|
|
500
|
+
if (normalizedBase.startsWith('http') || normalizedBase.startsWith('/')) {
|
|
544
501
|
return `${normalizedBase}${filename}`;
|
|
545
502
|
}
|
|
546
|
-
//
|
|
547
|
-
if (normalizedBase.startsWith('/')) {
|
|
548
|
-
return `${normalizedBase}${filename}`;
|
|
549
|
-
}
|
|
550
|
-
// Otherwise, make it relative to current location
|
|
503
|
+
// Caso contrário, relativo à origem atual
|
|
551
504
|
if (typeof window !== 'undefined') {
|
|
552
|
-
|
|
553
|
-
return `${base}/${normalizedBase}${filename}`;
|
|
505
|
+
return `${window.location.origin}/${normalizedBase}${filename}`;
|
|
554
506
|
}
|
|
555
|
-
return
|
|
507
|
+
return `/${normalizedBase}${filename}`;
|
|
556
508
|
}
|
|
557
509
|
/**
|
|
558
510
|
* Configure encoder memory initializer paths
|
|
559
|
-
*
|
|
511
|
+
* Critical for OGG/MP3 Emscripten modules
|
|
560
512
|
*/
|
|
561
513
|
function configureEncoderPaths(baseUrl) {
|
|
562
514
|
if (typeof window === 'undefined') {
|
|
563
515
|
return;
|
|
564
516
|
}
|
|
565
|
-
|
|
517
|
+
// Se nenhuma URL for passada, usar o padrão /encoders
|
|
518
|
+
let url = baseUrl || getEncoderBaseUrl();
|
|
519
|
+
// Converter para URL absoluta para evitar problemas com Emscripten
|
|
520
|
+
try {
|
|
521
|
+
if (!url.startsWith('http')) {
|
|
522
|
+
// Usar a URL atual como base
|
|
523
|
+
// Se url for '/encoders', vira 'http://localhost:3000/encoders'
|
|
524
|
+
url = new URL(url, window.location.href).href;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch (e) {
|
|
528
|
+
console.warn('Failed to resolve absolute URL for encoder path:', e);
|
|
529
|
+
}
|
|
530
|
+
// Garantir barra no final
|
|
566
531
|
const normalizedUrl = url.endsWith('/') ? url : `${url}/`;
|
|
567
|
-
|
|
532
|
+
console.log(`[web-audio-recorder-ts] Configuring encoder memory path to: ${normalizedUrl}`);
|
|
533
|
+
// Configurar globais para o Emscripten
|
|
534
|
+
// OGG
|
|
568
535
|
window.OggVorbisEncoderConfig = {
|
|
536
|
+
// Importante: Emscripten usa isso para achar o arquivo .mem
|
|
569
537
|
memoryInitializerPrefixURL: normalizedUrl
|
|
570
538
|
};
|
|
571
|
-
//
|
|
539
|
+
// MP3
|
|
572
540
|
window.Mp3LameEncoderConfig = {
|
|
573
541
|
memoryInitializerPrefixURL: normalizedUrl
|
|
574
542
|
};
|
|
575
543
|
}
|
|
576
544
|
/**
|
|
577
|
-
*
|
|
578
|
-
*
|
|
545
|
+
* Find and load encoder script
|
|
546
|
+
* Simplified: ONLY looks in standard locations or provided path
|
|
579
547
|
*/
|
|
580
548
|
async function findEncoderPath(filename) {
|
|
581
|
-
//
|
|
582
|
-
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
583
|
-
const currentPath = typeof window !== 'undefined' ? window.location.pathname : '';
|
|
584
|
-
// Priorizar caminhos do node_modules primeiro
|
|
549
|
+
// Caminhos padrão para tentar
|
|
585
550
|
const possiblePaths = [
|
|
586
|
-
//
|
|
587
|
-
`/
|
|
588
|
-
|
|
589
|
-
// Caminhos relativos do node_modules
|
|
590
|
-
`./node_modules/web-audio-recorder-ts/lib/${filename}`,
|
|
591
|
-
`../node_modules/web-audio-recorder-ts/lib/${filename}`,
|
|
592
|
-
`../../node_modules/web-audio-recorder-ts/lib/${filename}`,
|
|
593
|
-
`../../../node_modules/web-audio-recorder-ts/lib/${filename}`,
|
|
594
|
-
// Com base no caminho atual
|
|
595
|
-
`${currentPath.replace(/\/[^/]*$/, '')}/node_modules/web-audio-recorder-ts/lib/${filename}`,
|
|
596
|
-
// From dist (se os arquivos foram copiados para dist/lib)
|
|
597
|
-
`/node_modules/web-audio-recorder-ts/dist/lib/${filename}`,
|
|
598
|
-
`${currentOrigin}/node_modules/web-audio-recorder-ts/dist/lib/${filename}`,
|
|
599
|
-
`./node_modules/web-audio-recorder-ts/dist/lib/${filename}`,
|
|
600
|
-
// Auto-detected path (pode apontar para node_modules)
|
|
601
|
-
getEncoderScriptUrl(filename),
|
|
602
|
-
// Para desenvolvimento/demo: try public folder (Vite serves public/ at root)
|
|
551
|
+
// 1. Caminho padrão: /encoders/ (na raiz do servidor)
|
|
552
|
+
`/encoders/${filename}`,
|
|
553
|
+
// 2. Caminho na raiz (fallback)
|
|
603
554
|
`/${filename}`,
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
`../lib/${filename}`,
|
|
610
|
-
// CDN or absolute paths (if configured)
|
|
611
|
-
filename.startsWith('http') ? filename : null
|
|
612
|
-
].filter((path) => path !== null);
|
|
613
|
-
console.log(`[web-audio-recorder-ts] Searching for ${filename} in ${possiblePaths.length} possible paths...`);
|
|
614
|
-
// Try each path
|
|
615
|
-
for (let i = 0; i < possiblePaths.length; i++) {
|
|
616
|
-
const path = possiblePaths[i];
|
|
555
|
+
// 3. Fallback relativo simples
|
|
556
|
+
`./encoders/${filename}`
|
|
557
|
+
];
|
|
558
|
+
console.log(`[web-audio-recorder-ts] Looking for ${filename} mainly in /encoders/...`);
|
|
559
|
+
for (const path of possiblePaths) {
|
|
617
560
|
try {
|
|
561
|
+
// Resolver URL absoluta para teste
|
|
618
562
|
const testUrl = path.startsWith('http')
|
|
619
563
|
? path
|
|
620
564
|
: new URL(path, typeof window !== 'undefined' ? window.location.href : 'file://').href;
|
|
621
|
-
console.log(`[web-audio-recorder-ts] Trying path ${i + 1}/${possiblePaths.length}: ${path} -> ${testUrl}`);
|
|
622
|
-
// Usar GET para verificar se é JavaScript válido (não HTML)
|
|
623
565
|
const response = await fetch(testUrl, { method: 'GET', cache: 'no-cache' });
|
|
624
566
|
if (response.ok) {
|
|
625
|
-
// Verificar se
|
|
567
|
+
// Verificar se é JS e não HTML (404)
|
|
626
568
|
const text = await response.text();
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (trimmedText.startsWith('<')) {
|
|
630
|
-
console.warn(`[web-audio-recorder-ts] ❌ Path ${path} returned HTML instead of JavaScript (likely 404), skipping...`);
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
// Se parece JavaScript, retornar este caminho
|
|
634
|
-
if (trimmedText.includes('function') || trimmedText.includes('var') || trimmedText.includes('const') || trimmedText.includes('let') || trimmedText.length > 100) {
|
|
635
|
-
console.log(`[web-audio-recorder-ts] ✅ Found encoder file at: ${path}`);
|
|
636
|
-
return path;
|
|
637
|
-
}
|
|
638
|
-
else {
|
|
639
|
-
console.warn(`[web-audio-recorder-ts] ⚠️ Path ${path} returned content but doesn't look like JavaScript`);
|
|
569
|
+
if (text.trim().startsWith('<')) {
|
|
570
|
+
continue; // É HTML (erro)
|
|
640
571
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
console.warn(`[web-audio-recorder-ts] ❌ Path ${path} returned status ${response.status} ${response.statusText}`);
|
|
572
|
+
console.log(`[web-audio-recorder-ts] ✅ Found encoder at: ${path}`);
|
|
573
|
+
return path;
|
|
644
574
|
}
|
|
645
575
|
}
|
|
646
576
|
catch (e) {
|
|
647
|
-
console.warn(`[web-audio-recorder-ts] ❌ Error testing path ${path}:`, e);
|
|
648
|
-
// Continue to next path
|
|
649
577
|
continue;
|
|
650
578
|
}
|
|
651
579
|
}
|
|
652
|
-
console.error(`[web-audio-recorder-ts] ❌ Could not find ${filename}
|
|
653
|
-
console.error(`[web-audio-recorder-ts] Tried paths:`, possiblePaths);
|
|
580
|
+
console.error(`[web-audio-recorder-ts] ❌ Could not find ${filename}. Please ensure files are in 'public/encoders/' folder.`);
|
|
654
581
|
return null;
|
|
655
582
|
}
|
|
656
583
|
|
|
@@ -686,136 +613,155 @@
|
|
|
686
613
|
// Validar e limitar qualidade (-0.1 a 1.0 para Vorbis)
|
|
687
614
|
const rawQuality = options.quality ?? 0.5;
|
|
688
615
|
if (!Number.isFinite(rawQuality)) {
|
|
689
|
-
console.warn(`Invalid quality value: ${rawQuality}. Using default 0.5`);
|
|
690
616
|
this.quality = 0.5;
|
|
691
617
|
}
|
|
692
618
|
else {
|
|
693
|
-
// Clamp quality to valid range
|
|
694
619
|
this.quality = Math.max(-0.1, Math.min(1.0, rawQuality));
|
|
695
|
-
if (rawQuality !== this.quality) {
|
|
696
|
-
console.warn(`Quality value ${rawQuality} clamped to valid range: ${this.quality}`);
|
|
697
|
-
}
|
|
698
620
|
}
|
|
699
621
|
// Verificar se OggVorbisEncoder está disponível
|
|
700
622
|
if (typeof OggVorbisEncoder === 'undefined') {
|
|
701
|
-
throw new Error('OggVorbisEncoder is not loaded.
|
|
623
|
+
throw new Error('OggVorbisEncoder is not loaded.');
|
|
702
624
|
}
|
|
703
625
|
try {
|
|
704
|
-
// Criar instância do encoder
|
|
705
626
|
this.encoder = new OggVorbisEncoder(sampleRate, numChannels, this.quality);
|
|
706
627
|
}
|
|
707
628
|
catch (error) {
|
|
708
|
-
|
|
709
|
-
throw new Error(`Failed to initialize OGG encoder: ${errorMsg}. ` +
|
|
710
|
-
`Parameters: sampleRate=${sampleRate}, numChannels=${numChannels}, quality=${this.quality}`);
|
|
629
|
+
throw new Error(`Failed to initialize OGG encoder: ${String(error)}`);
|
|
711
630
|
}
|
|
712
631
|
}
|
|
713
632
|
/**
|
|
714
633
|
* Codifica buffers de áudio
|
|
715
|
-
*
|
|
716
|
-
* @param buffers - Array de buffers Float32Array, um por canal
|
|
717
634
|
*/
|
|
718
635
|
encode(buffers) {
|
|
719
636
|
if (!this.encoder) {
|
|
720
637
|
throw new Error('Encoder is not initialized');
|
|
721
638
|
}
|
|
722
|
-
|
|
723
|
-
|
|
639
|
+
// Validação básica de entrada
|
|
640
|
+
if (!buffers || buffers.length === 0) {
|
|
641
|
+
return;
|
|
724
642
|
}
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
// Validar que há dados para processar
|
|
734
|
-
if (expectedLength === 0) {
|
|
735
|
-
// Buffer vazio, não há nada para codificar
|
|
736
|
-
return;
|
|
643
|
+
// Preparar buffers para o encoder
|
|
644
|
+
// O encoder espera exatamente this.numChannels arrays
|
|
645
|
+
const finalBuffers = [];
|
|
646
|
+
// 1. Resolver mismatch de canais
|
|
647
|
+
if (buffers.length === this.numChannels) {
|
|
648
|
+
// Caso ideal: número de canais bate
|
|
649
|
+
for (let i = 0; i < this.numChannels; i++) {
|
|
650
|
+
finalBuffers.push(buffers[i]);
|
|
737
651
|
}
|
|
738
652
|
}
|
|
653
|
+
else if (buffers.length === 1 && this.numChannels === 2) {
|
|
654
|
+
// Mono -> Stereo (Duplicar)
|
|
655
|
+
finalBuffers.push(buffers[0]);
|
|
656
|
+
finalBuffers.push(buffers[0]);
|
|
657
|
+
}
|
|
658
|
+
else if (buffers.length >= 2 && this.numChannels === 1) {
|
|
659
|
+
// Stereo -> Mono (Pegar apenas o primeiro canal - downmixing simples)
|
|
660
|
+
finalBuffers.push(buffers[0]);
|
|
661
|
+
}
|
|
739
662
|
else {
|
|
740
|
-
//
|
|
741
|
-
|
|
663
|
+
// Fallback genérico: preencher com o que tem ou silêncio
|
|
664
|
+
for (let i = 0; i < this.numChannels; i++) {
|
|
665
|
+
if (i < buffers.length) {
|
|
666
|
+
finalBuffers.push(buffers[i]);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
// Duplicar o último disponível
|
|
670
|
+
finalBuffers.push(buffers[buffers.length - 1]);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
742
673
|
}
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
674
|
+
// 2. Sanitizar dados (Safe Clamping)
|
|
675
|
+
// Importante: valores exatamente 1.0 ou -1.0 podem causar crash em algumas versões do Vorbis encoder
|
|
676
|
+
// Usamos um clamping levemente conservador para garantir estabilidade
|
|
677
|
+
const SAFE_MAX = 0.9999;
|
|
678
|
+
const SAFE_MIN = -0.9999;
|
|
679
|
+
const safeBuffers = finalBuffers.map(buffer => {
|
|
680
|
+
// Criar nova cópia para não alterar o original e garantir propriedade
|
|
681
|
+
const copy = new Float32Array(buffer.length);
|
|
747
682
|
for (let i = 0; i < buffer.length; i++) {
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
hasInvalidValues = true;
|
|
752
|
-
// Substituir valores inválidos por 0
|
|
753
|
-
safeBuffer[i] = 0;
|
|
683
|
+
const val = buffer[i];
|
|
684
|
+
if (!Number.isFinite(val)) {
|
|
685
|
+
copy[i] = 0; // Remove NaN/Infinity
|
|
754
686
|
}
|
|
755
687
|
else {
|
|
756
|
-
// Clamp
|
|
757
|
-
|
|
688
|
+
// Clamp conservador
|
|
689
|
+
if (val > SAFE_MAX)
|
|
690
|
+
copy[i] = SAFE_MAX;
|
|
691
|
+
else if (val < SAFE_MIN)
|
|
692
|
+
copy[i] = SAFE_MIN;
|
|
693
|
+
else
|
|
694
|
+
copy[i] = val;
|
|
758
695
|
}
|
|
759
696
|
}
|
|
760
|
-
|
|
761
|
-
console.warn(`OGG Encoder: Found invalid values (NaN/Infinity) in channel ${channelIndex}. ` +
|
|
762
|
-
`Replaced with 0. Buffer length: ${buffer.length}`);
|
|
763
|
-
}
|
|
764
|
-
return safeBuffer;
|
|
697
|
+
return copy;
|
|
765
698
|
});
|
|
766
699
|
try {
|
|
767
700
|
this.encoder.encode(safeBuffers);
|
|
768
|
-
// Contar buffers processados para garantir que há dados antes de finalizar
|
|
769
701
|
this.bufferCount++;
|
|
770
702
|
this.totalSamples += safeBuffers[0].length;
|
|
771
703
|
}
|
|
772
704
|
catch (error) {
|
|
773
|
-
|
|
774
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
775
|
-
throw new Error(`OGG encoding error: ${errorMsg}. ` +
|
|
776
|
-
`Buffers: ${buffers.length} channels, lengths: ${buffers.map(b => b.length).join(', ')}, ` +
|
|
777
|
-
`Total buffers processed: ${this.bufferCount}, Total samples: ${this.totalSamples}`);
|
|
705
|
+
throw new Error(`OGG encoding error: ${String(error)}`);
|
|
778
706
|
}
|
|
779
707
|
}
|
|
780
708
|
/**
|
|
781
709
|
* Finaliza o encoding e retorna o Blob OGG
|
|
782
|
-
*
|
|
783
|
-
* @param mimeType - Tipo MIME (padrão: 'audio/ogg')
|
|
784
|
-
* @returns Blob contendo o arquivo OGG
|
|
785
710
|
*/
|
|
786
711
|
finish(mimeType = 'audio/ogg') {
|
|
787
712
|
if (!this.encoder) {
|
|
788
713
|
throw new Error('Encoder is not initialized');
|
|
789
714
|
}
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
//
|
|
715
|
+
// Requisito mínimo de samples (0.5s)
|
|
716
|
+
const MIN_SAMPLES = this.sampleRate * 0.5;
|
|
717
|
+
if (this.totalSamples < MIN_SAMPLES) {
|
|
718
|
+
// Se não atingiu o mínimo, gera silêncio para completar e salvar o arquivo
|
|
719
|
+
// Isso é melhor que lançar erro ou crashar
|
|
720
|
+
const missingSamples = Math.ceil(MIN_SAMPLES - this.totalSamples);
|
|
721
|
+
if (missingSamples > 0 && missingSamples < this.sampleRate * 10) { // Limite de 10s de silêncio
|
|
722
|
+
console.warn(`OGG Encoder: Padding with ${missingSamples} samples of silence to reach minimum duration.`);
|
|
723
|
+
const silence = new Float32Array(missingSamples); // Preenchido com zeros por padrão
|
|
724
|
+
const silenceBuffers = Array(this.numChannels).fill(silence);
|
|
725
|
+
try {
|
|
726
|
+
this.encoder.encode(silenceBuffers);
|
|
727
|
+
}
|
|
728
|
+
catch (e) {
|
|
729
|
+
console.error("Failed to pad silence:", e);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
else if (this.bufferCount === 0) {
|
|
733
|
+
throw new Error('No audio data recorded.');
|
|
734
|
+
}
|
|
794
735
|
}
|
|
795
736
|
try {
|
|
737
|
+
// Tentar finalizar
|
|
796
738
|
const blob = this.encoder.finish(mimeType);
|
|
797
|
-
// Validar que o blob não está vazio
|
|
798
|
-
if (blob.size === 0) {
|
|
799
|
-
console.warn('OGG Encoder: finish() returned empty blob. This may indicate insufficient audio data was encoded.');
|
|
800
|
-
}
|
|
801
739
|
return blob;
|
|
802
740
|
}
|
|
803
741
|
catch (error) {
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
`
|
|
742
|
+
const msg = String(error);
|
|
743
|
+
// Se for abort(3), geralmente é erro fatal de memória ou interno do WASM
|
|
744
|
+
if (msg.includes('abort')) {
|
|
745
|
+
throw new Error(`OGG Critical Error: The encoder crashed (${msg}). ` +
|
|
746
|
+
`This usually happens due to memory issues or invalid audio data. ` +
|
|
747
|
+
`Stats: ${this.totalSamples} samples, ${this.bufferCount} buffers.`);
|
|
748
|
+
}
|
|
749
|
+
throw new Error(`OGG finish() error: ${msg}`);
|
|
808
750
|
}
|
|
809
751
|
}
|
|
810
752
|
/**
|
|
811
753
|
* Cancela o encoding
|
|
812
754
|
*/
|
|
813
755
|
cancel() {
|
|
814
|
-
|
|
815
|
-
this.encoder
|
|
816
|
-
|
|
756
|
+
try {
|
|
757
|
+
if (this.encoder) {
|
|
758
|
+
this.encoder.cancel();
|
|
759
|
+
}
|
|
817
760
|
}
|
|
818
|
-
|
|
761
|
+
catch (e) {
|
|
762
|
+
// Ignorar erros no cancelamento
|
|
763
|
+
}
|
|
764
|
+
this.encoder = null;
|
|
819
765
|
this.bufferCount = 0;
|
|
820
766
|
this.totalSamples = 0;
|
|
821
767
|
}
|
|
@@ -827,28 +773,28 @@
|
|
|
827
773
|
* @returns Promise que resolve quando o script é carregado
|
|
828
774
|
*/
|
|
829
775
|
async function loadOggVorbisEncoder(scriptUrl) {
|
|
830
|
-
// Se não
|
|
776
|
+
// Se URL não fornecida, tentar encontrar no padrão /encoders
|
|
831
777
|
if (!scriptUrl) {
|
|
832
|
-
// Configurar paths dos arquivos .mem
|
|
833
|
-
configureEncoderPaths();
|
|
834
|
-
// Tentar encontrar o arquivo automaticamente
|
|
835
778
|
const foundPath = await findEncoderPath('OggVorbisEncoder.min.js');
|
|
836
779
|
if (!foundPath) {
|
|
837
|
-
|
|
838
|
-
'Please
|
|
839
|
-
'1. Provide the path manually: await loadOggVorbisEncoder("/path/to/OggVorbisEncoder.min.js")\n' +
|
|
840
|
-
'2. Copy files to public/: cp node_modules/web-audio-recorder-ts/lib/*.js public/\n' +
|
|
841
|
-
'3. Configure server to serve node_modules (see NUXT_USAGE.md)\n' +
|
|
842
|
-
'4. Check browser console for detailed path information';
|
|
843
|
-
console.error(errorMsg);
|
|
844
|
-
throw new Error(errorMsg);
|
|
780
|
+
throw new Error('Could not find OggVorbisEncoder.min.js in public/encoders/ folder.\n' +
|
|
781
|
+
'Please copy lib/*.js files to your public/encoders/ directory.');
|
|
845
782
|
}
|
|
846
783
|
scriptUrl = foundPath;
|
|
847
784
|
}
|
|
785
|
+
// Extrair diretório base da URL para configurar carregamento do .mem
|
|
786
|
+
let baseUrl = '';
|
|
787
|
+
const lastSlash = scriptUrl.lastIndexOf('/');
|
|
788
|
+
if (lastSlash !== -1) {
|
|
789
|
+
baseUrl = scriptUrl.substring(0, lastSlash);
|
|
790
|
+
}
|
|
848
791
|
else {
|
|
849
|
-
// Se
|
|
850
|
-
|
|
792
|
+
// Se não tem barra, assumimos que está na raiz ou relativo simples
|
|
793
|
+
// Mas se foi retornado por findEncoderPath como 'OggVorbis...', então deve ser ''
|
|
794
|
+
baseUrl = 'encoders'; // Melhor chute para fallback
|
|
851
795
|
}
|
|
796
|
+
// Configurar encoder (isso define OggVorbisEncoderConfig global)
|
|
797
|
+
configureEncoderPaths(baseUrl);
|
|
852
798
|
return loadOggVorbisEncoderInternal(scriptUrl);
|
|
853
799
|
}
|
|
854
800
|
/**
|
|
@@ -1019,15 +965,45 @@
|
|
|
1019
965
|
if (!this.encoder) {
|
|
1020
966
|
throw new Error('Encoder is not initialized');
|
|
1021
967
|
}
|
|
968
|
+
// Converter canais se necessário ANTES de validar
|
|
969
|
+
// Isso permite que streams mono funcionem com encoders estéreo e vice-versa
|
|
970
|
+
let processedBuffers = buffers;
|
|
1022
971
|
if (buffers.length !== this.numChannels) {
|
|
1023
|
-
|
|
972
|
+
// Ajustar número de canais para corresponder ao esperado pelo encoder
|
|
973
|
+
if (buffers.length === 1 && this.numChannels === 2) {
|
|
974
|
+
// Mono -> Estéreo: duplicar o canal
|
|
975
|
+
processedBuffers = [
|
|
976
|
+
new Float32Array(buffers[0]),
|
|
977
|
+
new Float32Array(buffers[0])
|
|
978
|
+
];
|
|
979
|
+
}
|
|
980
|
+
else if (buffers.length === 2 && this.numChannels === 1) {
|
|
981
|
+
// Estéreo -> Mono: usar apenas o primeiro canal
|
|
982
|
+
processedBuffers = [new Float32Array(buffers[0])];
|
|
983
|
+
}
|
|
984
|
+
else if (buffers.length < this.numChannels) {
|
|
985
|
+
// Menos canais: duplicar o último canal
|
|
986
|
+
processedBuffers = [...buffers];
|
|
987
|
+
while (processedBuffers.length < this.numChannels && processedBuffers.length > 0) {
|
|
988
|
+
processedBuffers.push(new Float32Array(processedBuffers[processedBuffers.length - 1]));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
else if (buffers.length > this.numChannels) {
|
|
992
|
+
// Mais canais: usar apenas os primeiros
|
|
993
|
+
processedBuffers = buffers.slice(0, this.numChannels);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// Validação final
|
|
997
|
+
if (processedBuffers.length !== this.numChannels) {
|
|
998
|
+
throw new Error(`Failed to adjust channels: Expected ${this.numChannels} channels, ` +
|
|
999
|
+
`got ${processedBuffers.length} after conversion from ${buffers.length} channels`);
|
|
1024
1000
|
}
|
|
1025
1001
|
// Validar que todos os buffers têm o mesmo tamanho
|
|
1026
|
-
if (
|
|
1027
|
-
const expectedLength =
|
|
1028
|
-
for (let i = 1; i <
|
|
1029
|
-
if (
|
|
1030
|
-
throw new Error(`Channel ${i} has length ${
|
|
1002
|
+
if (processedBuffers.length > 0) {
|
|
1003
|
+
const expectedLength = processedBuffers[0].length;
|
|
1004
|
+
for (let i = 1; i < processedBuffers.length; i++) {
|
|
1005
|
+
if (processedBuffers[i].length !== expectedLength) {
|
|
1006
|
+
throw new Error(`Channel ${i} has length ${processedBuffers[i].length}, expected ${expectedLength}`);
|
|
1031
1007
|
}
|
|
1032
1008
|
}
|
|
1033
1009
|
// Validar que há dados para processar
|
|
@@ -1041,7 +1017,7 @@
|
|
|
1041
1017
|
return;
|
|
1042
1018
|
}
|
|
1043
1019
|
// Criar cópias dos buffers e validar valores (NaN, Infinity)
|
|
1044
|
-
const safeBuffers =
|
|
1020
|
+
const safeBuffers = processedBuffers.map((buffer, channelIndex) => {
|
|
1045
1021
|
const safeBuffer = new Float32Array(buffer.length);
|
|
1046
1022
|
let hasInvalidValues = false;
|
|
1047
1023
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -1127,28 +1103,26 @@
|
|
|
1127
1103
|
* @returns Promise que resolve quando o script é carregado
|
|
1128
1104
|
*/
|
|
1129
1105
|
async function loadMp3LameEncoder(scriptUrl) {
|
|
1130
|
-
// Se não
|
|
1106
|
+
// Se URL não fornecida, tentar encontrar no padrão /encoders
|
|
1131
1107
|
if (!scriptUrl) {
|
|
1132
|
-
// Configurar paths dos arquivos .mem
|
|
1133
|
-
configureEncoderPaths();
|
|
1134
|
-
// Tentar encontrar o arquivo automaticamente
|
|
1135
1108
|
const foundPath = await findEncoderPath('Mp3LameEncoder.min.js');
|
|
1136
1109
|
if (!foundPath) {
|
|
1137
|
-
|
|
1138
|
-
'Please
|
|
1139
|
-
'1. Provide the path manually: await loadMp3LameEncoder("/path/to/Mp3LameEncoder.min.js")\n' +
|
|
1140
|
-
'2. Copy files to public/: cp node_modules/web-audio-recorder-ts/lib/*.js public/\n' +
|
|
1141
|
-
'3. Configure server to serve node_modules (see NUXT_USAGE.md)\n' +
|
|
1142
|
-
'4. Check browser console for detailed path information';
|
|
1143
|
-
console.error(errorMsg);
|
|
1144
|
-
throw new Error(errorMsg);
|
|
1110
|
+
throw new Error('Could not find Mp3LameEncoder.min.js in public/encoders/ folder.\n' +
|
|
1111
|
+
'Please copy lib/*.js files to your public/encoders/ directory.');
|
|
1145
1112
|
}
|
|
1146
1113
|
scriptUrl = foundPath;
|
|
1147
1114
|
}
|
|
1115
|
+
// Extrair diretório base da URL para configurar carregamento do .mem
|
|
1116
|
+
let baseUrl = '';
|
|
1117
|
+
const lastSlash = scriptUrl.lastIndexOf('/');
|
|
1118
|
+
if (lastSlash !== -1) {
|
|
1119
|
+
baseUrl = scriptUrl.substring(0, lastSlash);
|
|
1120
|
+
}
|
|
1148
1121
|
else {
|
|
1149
|
-
|
|
1150
|
-
configureEncoderPaths();
|
|
1122
|
+
baseUrl = 'encoders';
|
|
1151
1123
|
}
|
|
1124
|
+
// Configurar encoder (isso define Mp3LameEncoderConfig global)
|
|
1125
|
+
configureEncoderPaths(baseUrl);
|
|
1152
1126
|
return loadMp3LameEncoderInternal(scriptUrl);
|
|
1153
1127
|
}
|
|
1154
1128
|
/**
|