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/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
@@ -99,14 +119,69 @@ class WebAudioRecorder {
99
119
  }
100
120
  try {
101
121
  const inputBuffer = event.inputBuffer;
122
+ const actualChannels = inputBuffer.numberOfChannels;
102
123
  const buffers = [];
103
- // Extrair dados de cada canal
104
- for (let channel = 0; channel < this.numChannels; channel++) {
124
+ // Extrair dados de cada canal disponível
125
+ for (let channel = 0; channel < actualChannels; channel++) {
105
126
  const channelData = inputBuffer.getChannelData(channel);
106
127
  buffers.push(new Float32Array(channelData));
107
128
  }
129
+ // Garantir que temos exatamente o número de canais esperado
130
+ // Se o número de canais do buffer não corresponde ao esperado, ajustar
131
+ if (actualChannels !== this.numChannels) {
132
+ if (actualChannels === 1 && this.numChannels === 2) {
133
+ // Mono -> Estéreo: duplicar o canal
134
+ buffers.push(new Float32Array(buffers[0]));
135
+ }
136
+ else if (actualChannels === 2 && this.numChannels === 1) {
137
+ // Estéreo -> Mono: usar apenas o primeiro canal
138
+ buffers.splice(1);
139
+ }
140
+ else if (actualChannels < this.numChannels) {
141
+ // Menos canais do que esperado: duplicar o último canal
142
+ while (buffers.length < this.numChannels && buffers.length > 0) {
143
+ buffers.push(new Float32Array(buffers[buffers.length - 1]));
144
+ }
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));
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);
174
+ }
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
+ }
108
183
  // Codificar os dados
109
- if (this.encoder) {
184
+ if (this.encoder && buffers.length > 0) {
110
185
  this.encoder.encode(buffers);
111
186
  }
112
187
  }
@@ -398,233 +473,107 @@ class WavAudioEncoder {
398
473
  }
399
474
 
400
475
  /**
401
- * Utility functions for automatically loading encoder files from the npm package
476
+ * Utility functions for loading encoder files
477
+ *
478
+ * STANDARD: Files should be placed in `public/encoders/`
402
479
  */
403
480
  /**
404
- * Get the base URL for encoder files
405
- * Tries to detect the package location automatically
481
+ * Get the default base URL for encoder files
482
+ * Defaults to '/encoders' (relative to domain root)
406
483
  */
407
484
  function getEncoderBaseUrl() {
408
- // Try to detect from import.meta.url (ESM)
409
- // Use try-catch to safely check for import.meta
410
- try {
411
- // @ts-ignore - import.meta may not exist in all environments
412
- 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))) {
413
- try {
414
- // @ts-ignore
415
- 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)));
416
- // If running from node_modules, construct path to lib
417
- if (url.pathname.includes('node_modules')) {
418
- const packagePath = url.pathname.split('node_modules/')[1];
419
- const packageName = packagePath.split('/')[0];
420
- // Return relative path from package root
421
- return `/node_modules/${packageName}/lib`;
422
- }
423
- // If running from dist, go up to lib
424
- if (url.pathname.includes('/dist/')) {
425
- return url.pathname.replace('/dist/', '/lib/').replace(/\/[^/]+$/, '');
426
- }
427
- }
428
- catch (e) {
429
- // Fall through to other methods
430
- }
431
- }
432
- }
433
- catch (e) {
434
- // import.meta not available, continue to other methods
435
- }
436
- // Try to detect from __dirname (CJS/Node.js)
437
- if (typeof __dirname !== 'undefined') {
438
- try {
439
- // If in node_modules, construct path
440
- if (__dirname.includes('node_modules')) {
441
- const parts = __dirname.split('node_modules/');
442
- if (parts.length > 1) {
443
- const packageName = parts[1].split('/')[0];
444
- // Retornar caminho absoluto se possível
445
- if (typeof window !== 'undefined') {
446
- return `${window.location.origin}/node_modules/${packageName}/lib`;
447
- }
448
- return `/node_modules/${packageName}/lib`;
449
- }
450
- }
451
- // If in dist, go to lib
452
- if (__dirname.includes('dist')) {
453
- const libPath = __dirname.replace('dist', 'lib');
454
- if (typeof window !== 'undefined') {
455
- return `${window.location.origin}${libPath}`;
456
- }
457
- return libPath;
458
- }
459
- }
460
- catch (e) {
461
- // Fall through
462
- }
463
- }
464
- // Try to detect from document.currentScript or all scripts (browser)
465
- if (typeof document !== 'undefined') {
466
- // Try currentScript first
467
- let script = document.currentScript;
468
- // If no currentScript, try to find script with web-audio-recorder in src
469
- if (!script || !script.src) {
470
- const scripts = document.querySelectorAll('script[src]');
471
- for (const s of Array.from(scripts)) {
472
- const src = s.src;
473
- if (src.includes('web-audio-recorder')) {
474
- script = s;
475
- break;
476
- }
477
- }
478
- }
479
- if (script && script.src) {
480
- try {
481
- const url = new URL(script.src);
482
- // If from node_modules
483
- if (url.pathname.includes('node_modules')) {
484
- const packagePath = url.pathname.split('node_modules/')[1];
485
- const packageName = packagePath.split('/')[0];
486
- // Retornar caminho absoluto para node_modules
487
- return `${url.origin}/node_modules/${packageName}/lib`;
488
- }
489
- // If from dist, go to lib
490
- if (url.pathname.includes('/dist/')) {
491
- const libPath = url.pathname.replace('/dist/', '/lib/').replace(/\/[^/]+$/, '');
492
- return `${url.origin}${libPath}`;
493
- }
494
- }
495
- catch (e) {
496
- // Fall through
497
- }
498
- }
499
- }
500
- // Default fallback: try node_modules first, then root
501
- if (typeof window !== 'undefined') {
502
- // Priorizar node_modules
503
- return '/node_modules/web-audio-recorder-ts/lib';
504
- }
505
- return '/lib';
485
+ // Simples e direto: padrão é a pasta /encoders na raiz
486
+ return '/encoders';
506
487
  }
507
488
  /**
508
489
  * Get the full URL for an encoder script file
509
- * @param filename - Name of the encoder file (e.g., 'OggVorbisEncoder.min.js')
510
- * @param customBaseUrl - Optional custom base URL (overrides auto-detection)
511
490
  */
512
491
  function getEncoderScriptUrl(filename, customBaseUrl) {
513
492
  const baseUrl = customBaseUrl || getEncoderBaseUrl();
514
- // Ensure baseUrl ends with /
493
+ // Garantir barra no final
515
494
  const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
516
- // If baseUrl is absolute (starts with http:// or https://), return as is
517
- if (normalizedBase.startsWith('http://') || normalizedBase.startsWith('https://')) {
518
- return `${normalizedBase}${filename}`;
519
- }
520
- // If baseUrl starts with /, it's already absolute
521
- if (normalizedBase.startsWith('/')) {
495
+ // Se baseUrl começa com http/https ou /, retorna como está
496
+ if (normalizedBase.startsWith('http') || normalizedBase.startsWith('/')) {
522
497
  return `${normalizedBase}${filename}`;
523
498
  }
524
- // Otherwise, make it relative to current location
499
+ // Caso contrário, relativo à origem atual
525
500
  if (typeof window !== 'undefined') {
526
- const base = window.location.origin;
527
- return `${base}/${normalizedBase}${filename}`;
501
+ return `${window.location.origin}/${normalizedBase}${filename}`;
528
502
  }
529
- return `${normalizedBase}${filename}`;
503
+ return `/${normalizedBase}${filename}`;
530
504
  }
531
505
  /**
532
506
  * Configure encoder memory initializer paths
533
- * @param baseUrl - Base URL for .mem files (defaults to auto-detected path)
507
+ * Critical for OGG/MP3 Emscripten modules
534
508
  */
535
509
  function configureEncoderPaths(baseUrl) {
536
510
  if (typeof window === 'undefined') {
537
511
  return;
538
512
  }
539
- const url = baseUrl || getEncoderBaseUrl();
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
540
527
  const normalizedUrl = url.endsWith('/') ? url : `${url}/`;
541
- // Configure OGG encoder
528
+ console.log(`[web-audio-recorder-ts] Configuring encoder memory path to: ${normalizedUrl}`);
529
+ // Configurar globais para o Emscripten
530
+ // OGG
542
531
  window.OggVorbisEncoderConfig = {
532
+ // Importante: Emscripten usa isso para achar o arquivo .mem
543
533
  memoryInitializerPrefixURL: normalizedUrl
544
534
  };
545
- // Configure MP3 encoder
535
+ // MP3
546
536
  window.Mp3LameEncoderConfig = {
547
537
  memoryInitializerPrefixURL: normalizedUrl
548
538
  };
549
539
  }
550
540
  /**
551
- * Try multiple paths to find encoder files
552
- * Useful when auto-detection fails
541
+ * Find and load encoder script
542
+ * Simplified: ONLY looks in standard locations or provided path
553
543
  */
554
544
  async function findEncoderPath(filename) {
555
- // Obter base URL atual para construir caminhos relativos
556
- const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
557
- const currentPath = typeof window !== 'undefined' ? window.location.pathname : '';
558
- // Priorizar caminhos do node_modules primeiro
545
+ // Caminhos padrão para tentar
559
546
  const possiblePaths = [
560
- // Primeiro: tentar node_modules com caminhos absolutos (prioridade máxima)
561
- `/node_modules/web-audio-recorder-ts/lib/${filename}`,
562
- `${currentOrigin}/node_modules/web-audio-recorder-ts/lib/${filename}`,
563
- // Caminhos relativos do node_modules
564
- `./node_modules/web-audio-recorder-ts/lib/${filename}`,
565
- `../node_modules/web-audio-recorder-ts/lib/${filename}`,
566
- `../../node_modules/web-audio-recorder-ts/lib/${filename}`,
567
- `../../../node_modules/web-audio-recorder-ts/lib/${filename}`,
568
- // Com base no caminho atual
569
- `${currentPath.replace(/\/[^/]*$/, '')}/node_modules/web-audio-recorder-ts/lib/${filename}`,
570
- // From dist (se os arquivos foram copiados para dist/lib)
571
- `/node_modules/web-audio-recorder-ts/dist/lib/${filename}`,
572
- `${currentOrigin}/node_modules/web-audio-recorder-ts/dist/lib/${filename}`,
573
- `./node_modules/web-audio-recorder-ts/dist/lib/${filename}`,
574
- // Auto-detected path (pode apontar para node_modules)
575
- getEncoderScriptUrl(filename),
576
- // 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)
577
550
  `/${filename}`,
578
- `${currentOrigin}/${filename}`,
579
- // Direct lib paths (for development or custom setups)
580
- `/lib/${filename}`,
581
- `${currentOrigin}/lib/${filename}`,
582
- `./lib/${filename}`,
583
- `../lib/${filename}`,
584
- // CDN or absolute paths (if configured)
585
- filename.startsWith('http') ? filename : null
586
- ].filter((path) => path !== null);
587
- console.log(`[web-audio-recorder-ts] Searching for ${filename} in ${possiblePaths.length} possible paths...`);
588
- // Try each path
589
- for (let i = 0; i < possiblePaths.length; i++) {
590
- 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) {
591
556
  try {
557
+ // Resolver URL absoluta para teste
592
558
  const testUrl = path.startsWith('http')
593
559
  ? path
594
560
  : new URL(path, typeof window !== 'undefined' ? window.location.href : 'file://').href;
595
- console.log(`[web-audio-recorder-ts] Trying path ${i + 1}/${possiblePaths.length}: ${path} -> ${testUrl}`);
596
- // Usar GET para verificar se é JavaScript válido (não HTML)
597
561
  const response = await fetch(testUrl, { method: 'GET', cache: 'no-cache' });
598
562
  if (response.ok) {
599
- // Verificar se o conteúdo é JavaScript (não HTML)
563
+ // Verificar se é JS e não HTML (404)
600
564
  const text = await response.text();
601
- const trimmedText = text.trim();
602
- // Se começar com '<', é HTML (404, etc) - pular este caminho
603
- if (trimmedText.startsWith('<')) {
604
- console.warn(`[web-audio-recorder-ts] ❌ Path ${path} returned HTML instead of JavaScript (likely 404), skipping...`);
605
- continue;
565
+ if (text.trim().startsWith('<')) {
566
+ continue; // É HTML (erro)
606
567
  }
607
- // Se parece JavaScript, retornar este caminho
608
- if (trimmedText.includes('function') || trimmedText.includes('var') || trimmedText.includes('const') || trimmedText.includes('let') || trimmedText.length > 100) {
609
- console.log(`[web-audio-recorder-ts] ✅ Found encoder file at: ${path}`);
610
- return path;
611
- }
612
- else {
613
- console.warn(`[web-audio-recorder-ts] ⚠️ Path ${path} returned content but doesn't look like JavaScript`);
614
- }
615
- }
616
- else {
617
- 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;
618
570
  }
619
571
  }
620
572
  catch (e) {
621
- console.warn(`[web-audio-recorder-ts] ❌ Error testing path ${path}:`, e);
622
- // Continue to next path
623
573
  continue;
624
574
  }
625
575
  }
626
- console.error(`[web-audio-recorder-ts] ❌ Could not find ${filename} in any of the ${possiblePaths.length} paths tried`);
627
- 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.`);
628
577
  return null;
629
578
  }
630
579
 
@@ -660,136 +609,155 @@ class OggVorbisEncoderWrapper {
660
609
  // Validar e limitar qualidade (-0.1 a 1.0 para Vorbis)
661
610
  const rawQuality = options.quality ?? 0.5;
662
611
  if (!Number.isFinite(rawQuality)) {
663
- console.warn(`Invalid quality value: ${rawQuality}. Using default 0.5`);
664
612
  this.quality = 0.5;
665
613
  }
666
614
  else {
667
- // Clamp quality to valid range
668
615
  this.quality = Math.max(-0.1, Math.min(1.0, rawQuality));
669
- if (rawQuality !== this.quality) {
670
- console.warn(`Quality value ${rawQuality} clamped to valid range: ${this.quality}`);
671
- }
672
616
  }
673
617
  // Verificar se OggVorbisEncoder está disponível
674
618
  if (typeof OggVorbisEncoder === 'undefined') {
675
- throw new Error('OggVorbisEncoder is not loaded. Make sure to load OggVorbisEncoder.min.js before using this encoder.');
619
+ throw new Error('OggVorbisEncoder is not loaded.');
676
620
  }
677
621
  try {
678
- // Criar instância do encoder
679
622
  this.encoder = new OggVorbisEncoder(sampleRate, numChannels, this.quality);
680
623
  }
681
624
  catch (error) {
682
- const errorMsg = error instanceof Error ? error.message : String(error);
683
- throw new Error(`Failed to initialize OGG encoder: ${errorMsg}. ` +
684
- `Parameters: sampleRate=${sampleRate}, numChannels=${numChannels}, quality=${this.quality}`);
625
+ throw new Error(`Failed to initialize OGG encoder: ${String(error)}`);
685
626
  }
686
627
  }
687
628
  /**
688
629
  * Codifica buffers de áudio
689
- *
690
- * @param buffers - Array de buffers Float32Array, um por canal
691
630
  */
692
631
  encode(buffers) {
693
632
  if (!this.encoder) {
694
633
  throw new Error('Encoder is not initialized');
695
634
  }
696
- if (buffers.length !== this.numChannels) {
697
- throw new Error(`Expected ${this.numChannels} channels, got ${buffers.length}`);
635
+ // Validação básica de entrada
636
+ if (!buffers || buffers.length === 0) {
637
+ return;
698
638
  }
699
- // Validar que todos os buffers têm o mesmo tamanho
700
- if (buffers.length > 0) {
701
- const expectedLength = buffers[0].length;
702
- for (let i = 1; i < buffers.length; i++) {
703
- if (buffers[i].length !== expectedLength) {
704
- throw new Error(`Channel ${i} has length ${buffers[i].length}, expected ${expectedLength}`);
705
- }
706
- }
707
- // Validar que há dados para processar
708
- if (expectedLength === 0) {
709
- // Buffer vazio, não há nada para codificar
710
- 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]);
711
647
  }
712
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
+ }
713
658
  else {
714
- // Nenhum buffer fornecido
715
- return;
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
+ }
716
669
  }
717
- // Criar cópias dos buffers e validar valores (NaN, Infinity)
718
- const safeBuffers = buffers.map((buffer, channelIndex) => {
719
- const safeBuffer = new Float32Array(buffer.length);
720
- let hasInvalidValues = false;
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);
721
678
  for (let i = 0; i < buffer.length; i++) {
722
- const value = buffer[i];
723
- // Verificar NaN e Infinity
724
- if (!Number.isFinite(value)) {
725
- hasInvalidValues = true;
726
- // Substituir valores inválidos por 0
727
- safeBuffer[i] = 0;
679
+ const val = buffer[i];
680
+ if (!Number.isFinite(val)) {
681
+ copy[i] = 0; // Remove NaN/Infinity
728
682
  }
729
683
  else {
730
- // Clamp valores para o range válido de áudio (-1.0 a 1.0)
731
- safeBuffer[i] = Math.max(-1, Math.min(1.0, value));
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;
732
691
  }
733
692
  }
734
- if (hasInvalidValues) {
735
- console.warn(`OGG Encoder: Found invalid values (NaN/Infinity) in channel ${channelIndex}. ` +
736
- `Replaced with 0. Buffer length: ${buffer.length}`);
737
- }
738
- return safeBuffer;
693
+ return copy;
739
694
  });
740
695
  try {
741
696
  this.encoder.encode(safeBuffers);
742
- // Contar buffers processados para garantir que há dados antes de finalizar
743
697
  this.bufferCount++;
744
698
  this.totalSamples += safeBuffers[0].length;
745
699
  }
746
700
  catch (error) {
747
- // Melhorar mensagem de erro para incluir informações de debug
748
- const errorMsg = error instanceof Error ? error.message : String(error);
749
- throw new Error(`OGG encoding error: ${errorMsg}. ` +
750
- `Buffers: ${buffers.length} channels, lengths: ${buffers.map(b => b.length).join(', ')}, ` +
751
- `Total buffers processed: ${this.bufferCount}, Total samples: ${this.totalSamples}`);
701
+ throw new Error(`OGG encoding error: ${String(error)}`);
752
702
  }
753
703
  }
754
704
  /**
755
705
  * Finaliza o encoding e retorna o Blob OGG
756
- *
757
- * @param mimeType - Tipo MIME (padrão: 'audio/ogg')
758
- * @returns Blob contendo o arquivo OGG
759
706
  */
760
707
  finish(mimeType = 'audio/ogg') {
761
708
  if (!this.encoder) {
762
709
  throw new Error('Encoder is not initialized');
763
710
  }
764
- // Verificar se dados processados
765
- if (this.bufferCount === 0) {
766
- console.warn('OGG Encoder: finish() called but no buffers were encoded. This may cause issues with the Emscripten encoder.');
767
- // Ainda tentar finalizar, mas avisar
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
+ }
768
731
  }
769
732
  try {
733
+ // Tentar finalizar
770
734
  const blob = this.encoder.finish(mimeType);
771
- // Validar que o blob não está vazio
772
- if (blob.size === 0) {
773
- console.warn('OGG Encoder: finish() returned empty blob. This may indicate insufficient audio data was encoded.');
774
- }
775
735
  return blob;
776
736
  }
777
737
  catch (error) {
778
- const errorMsg = error instanceof Error ? error.message : String(error);
779
- throw new Error(`OGG finish() error: ${errorMsg}. ` +
780
- `Buffers processed: ${this.bufferCount}, Total samples: ${this.totalSamples}, ` +
781
- `Sample rate: ${this.sampleRate}, Channels: ${this.numChannels}, Quality: ${this.quality}`);
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}`);
782
746
  }
783
747
  }
784
748
  /**
785
749
  * Cancela o encoding
786
750
  */
787
751
  cancel() {
788
- if (this.encoder) {
789
- this.encoder.cancel();
790
- this.encoder = null;
752
+ try {
753
+ if (this.encoder) {
754
+ this.encoder.cancel();
755
+ }
791
756
  }
792
- // Reset contadores
757
+ catch (e) {
758
+ // Ignorar erros no cancelamento
759
+ }
760
+ this.encoder = null;
793
761
  this.bufferCount = 0;
794
762
  this.totalSamples = 0;
795
763
  }
@@ -801,28 +769,28 @@ class OggVorbisEncoderWrapper {
801
769
  * @returns Promise que resolve quando o script é carregado
802
770
  */
803
771
  async function loadOggVorbisEncoder(scriptUrl) {
804
- // Se não fornecido, tentar auto-detectar
772
+ // Se URL não fornecida, tentar encontrar no padrão /encoders
805
773
  if (!scriptUrl) {
806
- // Configurar paths dos arquivos .mem
807
- configureEncoderPaths();
808
- // Tentar encontrar o arquivo automaticamente
809
774
  const foundPath = await findEncoderPath('OggVorbisEncoder.min.js');
810
775
  if (!foundPath) {
811
- const errorMsg = 'Could not find OggVorbisEncoder.min.js automatically.\n\n' +
812
- 'Please try one of the following:\n' +
813
- '1. Provide the path manually: await loadOggVorbisEncoder("/path/to/OggVorbisEncoder.min.js")\n' +
814
- '2. Copy files to public/: cp node_modules/web-audio-recorder-ts/lib/*.js public/\n' +
815
- '3. Configure server to serve node_modules (see NUXT_USAGE.md)\n' +
816
- '4. Check browser console for detailed path information';
817
- console.error(errorMsg);
818
- 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.');
819
778
  }
820
779
  scriptUrl = foundPath;
821
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
+ }
822
787
  else {
823
- // Se fornecido, ainda configurar paths dos .mem
824
- configureEncoderPaths();
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
825
791
  }
792
+ // Configurar encoder (isso define OggVorbisEncoderConfig global)
793
+ configureEncoderPaths(baseUrl);
826
794
  return loadOggVorbisEncoderInternal(scriptUrl);
827
795
  }
828
796
  /**
@@ -993,15 +961,45 @@ class Mp3LameEncoderWrapper {
993
961
  if (!this.encoder) {
994
962
  throw new Error('Encoder is not initialized');
995
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;
996
967
  if (buffers.length !== this.numChannels) {
997
- throw new Error(`Expected ${this.numChannels} channels, got ${buffers.length}`);
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`);
998
996
  }
999
997
  // Validar que todos os buffers têm o mesmo tamanho
1000
- if (buffers.length > 0) {
1001
- const expectedLength = buffers[0].length;
1002
- for (let i = 1; i < buffers.length; i++) {
1003
- if (buffers[i].length !== expectedLength) {
1004
- throw new Error(`Channel ${i} has length ${buffers[i].length}, expected ${expectedLength}`);
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}`);
1005
1003
  }
1006
1004
  }
1007
1005
  // Validar que há dados para processar
@@ -1015,7 +1013,7 @@ class Mp3LameEncoderWrapper {
1015
1013
  return;
1016
1014
  }
1017
1015
  // Criar cópias dos buffers e validar valores (NaN, Infinity)
1018
- const safeBuffers = buffers.map((buffer, channelIndex) => {
1016
+ const safeBuffers = processedBuffers.map((buffer, channelIndex) => {
1019
1017
  const safeBuffer = new Float32Array(buffer.length);
1020
1018
  let hasInvalidValues = false;
1021
1019
  for (let i = 0; i < buffer.length; i++) {
@@ -1101,28 +1099,26 @@ class Mp3LameEncoderWrapper {
1101
1099
  * @returns Promise que resolve quando o script é carregado
1102
1100
  */
1103
1101
  async function loadMp3LameEncoder(scriptUrl) {
1104
- // Se não fornecido, tentar auto-detectar
1102
+ // Se URL não fornecida, tentar encontrar no padrão /encoders
1105
1103
  if (!scriptUrl) {
1106
- // Configurar paths dos arquivos .mem
1107
- configureEncoderPaths();
1108
- // Tentar encontrar o arquivo automaticamente
1109
1104
  const foundPath = await findEncoderPath('Mp3LameEncoder.min.js');
1110
1105
  if (!foundPath) {
1111
- const errorMsg = 'Could not find Mp3LameEncoder.min.js automatically.\n\n' +
1112
- 'Please try one of the following:\n' +
1113
- '1. Provide the path manually: await loadMp3LameEncoder("/path/to/Mp3LameEncoder.min.js")\n' +
1114
- '2. Copy files to public/: cp node_modules/web-audio-recorder-ts/lib/*.js public/\n' +
1115
- '3. Configure server to serve node_modules (see NUXT_USAGE.md)\n' +
1116
- '4. Check browser console for detailed path information';
1117
- console.error(errorMsg);
1118
- 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.');
1119
1108
  }
1120
1109
  scriptUrl = foundPath;
1121
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
+ }
1122
1117
  else {
1123
- // Se fornecido, ainda configurar paths dos .mem
1124
- configureEncoderPaths();
1118
+ baseUrl = 'encoders';
1125
1119
  }
1120
+ // Configurar encoder (isso define Mp3LameEncoderConfig global)
1121
+ configureEncoderPaths(baseUrl);
1126
1122
  return loadMp3LameEncoderInternal(scriptUrl);
1127
1123
  }
1128
1124
  /**