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