sfxmix 1.4.2 → 1.4.6
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/index.js +52 -36
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -232,14 +232,18 @@ class SfxMix {
|
|
|
232
232
|
}
|
|
233
233
|
} else {
|
|
234
234
|
// Save as MP3 or original format (no conversion needed)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
235
|
+
if (this.isTempFile(this.currentFile)) {
|
|
236
|
+
// Temp file: move it (rename) to the output path
|
|
237
|
+
try {
|
|
238
|
+
fs.renameSync(this.currentFile, absoluteOutput);
|
|
239
|
+
} catch (renameErr) {
|
|
240
|
+
// Cross-device rename fails: copy + delete
|
|
241
|
+
fs.copyFileSync(this.currentFile, absoluteOutput);
|
|
241
242
|
this.safeDeleteFile(this.currentFile);
|
|
242
243
|
}
|
|
244
|
+
} else {
|
|
245
|
+
// Original user file: always copy, never move
|
|
246
|
+
fs.copyFileSync(this.currentFile, absoluteOutput);
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
|
|
@@ -481,35 +485,52 @@ class SfxMix {
|
|
|
481
485
|
return new Promise(async (resolve, reject) => {
|
|
482
486
|
try {
|
|
483
487
|
// Default options for silenceremove filter
|
|
484
|
-
// startDuration:
|
|
485
|
-
// startThreshold: noise tolerance for start (in dB, e.g., -50dB)
|
|
486
|
-
// stopDuration:
|
|
487
|
-
//
|
|
488
|
+
// startDuration: min duration of non-silence to stop trimming at start (in seconds). 0 = stop at first non-silent sample.
|
|
489
|
+
// startThreshold: noise tolerance for start (in dB, e.g., -50dB). Higher values (e.g. -20) are more aggressive.
|
|
490
|
+
// stopDuration: min duration of non-silence to stop trimming at end (in seconds).
|
|
491
|
+
// Default 0.05s (50ms) to skip short noise artifacts/blips at the end that would prevent proper trimming.
|
|
492
|
+
// stopThreshold: noise tolerance for end (in dB, e.g., -50dB). Higher values (e.g. -20) are more aggressive.
|
|
488
493
|
// paddingStart: milliseconds of silence to add at the start (after removing silence)
|
|
489
494
|
// paddingEnd: milliseconds of silence to add at the end (after removing silence)
|
|
490
495
|
|
|
491
496
|
const startDuration = options.startDuration !== undefined ? options.startDuration : 0;
|
|
492
|
-
const startThreshold = options.startThreshold !== undefined ? options.startThreshold : -
|
|
493
|
-
const stopDuration = options.stopDuration !== undefined ? options.stopDuration : 0;
|
|
494
|
-
const stopThreshold = options.stopThreshold !== undefined ? options.stopThreshold : -
|
|
497
|
+
const startThreshold = options.startThreshold !== undefined ? options.startThreshold : -50;
|
|
498
|
+
const stopDuration = options.stopDuration !== undefined ? options.stopDuration : 0.05;
|
|
499
|
+
const stopThreshold = options.stopThreshold !== undefined ? options.stopThreshold : -50;
|
|
495
500
|
const paddingStart = options.paddingStart || 0; // in milliseconds
|
|
496
501
|
const paddingEnd = options.paddingEnd || 0; // in milliseconds
|
|
497
502
|
|
|
503
|
+
let bitrateKbps;
|
|
504
|
+
let audioInfo = null;
|
|
505
|
+
|
|
506
|
+
// If bitrate is null, auto-detect from input file
|
|
507
|
+
if (this.bitrate === null) {
|
|
508
|
+
try {
|
|
509
|
+
audioInfo = await this.getAudioInfo(inputFile);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.warn('Could not get audio info for trim, using 128k default:', err.message);
|
|
512
|
+
}
|
|
513
|
+
bitrateKbps = audioInfo ? Math.floor(audioInfo.bitrate / 1000) + 'k' : '128k';
|
|
514
|
+
} else {
|
|
515
|
+
// Use configured default bitrate
|
|
516
|
+
bitrateKbps = Math.floor(this.bitrate / 1000) + 'k';
|
|
517
|
+
}
|
|
518
|
+
|
|
498
519
|
// Build filter chain
|
|
499
520
|
// Strategy: Remove silence from start, then reverse audio, remove silence from new start (old end), then reverse back
|
|
500
521
|
// This ensures we ONLY remove silence from the beginning and end, preserving intermediate silences
|
|
501
522
|
const filters = [];
|
|
502
523
|
|
|
503
|
-
//
|
|
524
|
+
// Remove silence from the start
|
|
504
525
|
filters.push(`silenceremove=start_periods=1:start_duration=${startDuration}:start_threshold=${startThreshold}dB:detection=peak`);
|
|
505
526
|
|
|
506
|
-
//
|
|
527
|
+
// Reverse the audio
|
|
507
528
|
filters.push('areverse');
|
|
508
529
|
|
|
509
|
-
//
|
|
530
|
+
// Remove silence from what is now the start (but was the end)
|
|
510
531
|
filters.push(`silenceremove=start_periods=1:start_duration=${stopDuration}:start_threshold=${stopThreshold}dB:detection=peak`);
|
|
511
532
|
|
|
512
|
-
//
|
|
533
|
+
// Reverse back to original direction
|
|
513
534
|
filters.push('areverse');
|
|
514
535
|
|
|
515
536
|
// Add padding at start if specified
|
|
@@ -523,24 +544,13 @@ class SfxMix {
|
|
|
523
544
|
filters.push(`apad=pad_dur=${paddingSec}`);
|
|
524
545
|
}
|
|
525
546
|
|
|
526
|
-
|
|
547
|
+
// CRITICAL: Resample at the end to fix "inadequate AVFrame plane padding" error
|
|
548
|
+
// This regenerates the audio frames with proper padding for the encoder
|
|
549
|
+
filters.push('aresample=async=1:min_hard_comp=0.100000:first_pts=0');
|
|
527
550
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// If bitrate is null, auto-detect from input file
|
|
531
|
-
if (this.bitrate === null) {
|
|
532
|
-
let audioInfo = null;
|
|
533
|
-
try {
|
|
534
|
-
audioInfo = await this.getAudioInfo(inputFile);
|
|
535
|
-
} catch (err) {
|
|
536
|
-
console.warn('Could not get audio info for trim, using 128k default:', err.message);
|
|
537
|
-
}
|
|
538
|
-
bitrateKbps = audioInfo ? Math.floor(audioInfo.bitrate / 1000) + 'k' : '128k';
|
|
539
|
-
} else {
|
|
540
|
-
// Use configured default bitrate
|
|
541
|
-
bitrateKbps = Math.floor(this.bitrate / 1000) + 'k';
|
|
542
|
-
}
|
|
551
|
+
const filterChain = filters.join(',');
|
|
543
552
|
|
|
553
|
+
// Apply filters directly on input file
|
|
544
554
|
const command = ffmpeg()
|
|
545
555
|
.input(inputFile)
|
|
546
556
|
.audioFilters(filterChain)
|
|
@@ -551,7 +561,13 @@ class SfxMix {
|
|
|
551
561
|
|
|
552
562
|
command
|
|
553
563
|
.on('end', () => resolve())
|
|
554
|
-
.on('error', (err) =>
|
|
564
|
+
.on('error', (err, stdout, stderr) => {
|
|
565
|
+
console.error('FFmpeg trim error:', err.message);
|
|
566
|
+
if (stderr) {
|
|
567
|
+
console.error('FFmpeg stderr:', stderr);
|
|
568
|
+
}
|
|
569
|
+
reject(err);
|
|
570
|
+
})
|
|
555
571
|
.run();
|
|
556
572
|
} catch (err) {
|
|
557
573
|
reject(err);
|
|
@@ -563,10 +579,10 @@ class SfxMix {
|
|
|
563
579
|
switch (filterName) {
|
|
564
580
|
case 'normalize':
|
|
565
581
|
// Normalize audio using loudnorm filter (EBU R128)
|
|
566
|
-
// i: integrated loudness target in LUFS (default: -
|
|
582
|
+
// i: integrated loudness target in LUFS (default: -18)
|
|
567
583
|
// lra: loudness range target in LU (default: 11)
|
|
568
584
|
// tp: true peak in dBTP (configurable, typically -3dB or -0.1dB)
|
|
569
|
-
const i = options.i !== undefined ? options.i : -
|
|
585
|
+
const i = options.i !== undefined ? options.i : -18; // Target loudness
|
|
570
586
|
const lra = options.lra !== undefined ? options.lra : 11; // Loudness range
|
|
571
587
|
const tp = options.tp !== undefined ? options.tp : -3; // True peak
|
|
572
588
|
return `loudnorm=I=${i}:LRA=${lra}:TP=${tp}`;
|