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