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