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