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