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