web-audio-recorder-ts 1.0.5 → 1.0.7

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