sfxmix 1.2.8 → 1.3.2
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/README.md +1 -1
- package/demo/demo_8.js +33 -0
- package/demo/package.json +12 -0
- package/demo/part1.mp3 +0 -0
- package/index.js +181 -23
- package/package.json +2 -1
- /package/demo/{demo_1.mjs → demo_1.js} +0 -0
- /package/demo/{demo_2.mjs → demo_2.js} +0 -0
- /package/demo/{demo_3.mjs → demo_3.js} +0 -0
- /package/demo/{demo_4.mjs → demo_4.js} +0 -0
- /package/demo/{demo_5.mjs → demo_5.js} +0 -0
- /package/demo/{demo_6.mjs → demo_6.js} +0 -0
- /package/demo/{demo_7.mjs → demo_7.js} +0 -0
package/README.md
CHANGED
|
@@ -240,7 +240,7 @@ sfx.save('output.mp3');
|
|
|
240
240
|
Normalizes audio loudness to a specified target using the EBU R128 standard.
|
|
241
241
|
|
|
242
242
|
- **Options:**
|
|
243
|
-
- `tp` (number): Maximum true peak level in dBTP (default: `-
|
|
243
|
+
- `tp` (number): Maximum true peak level in dBTP (default: `-1.5`).
|
|
244
244
|
- `i` (number): Target integrated loudness in LUFS (default: `-20`).
|
|
245
245
|
- `lra` (number): Loudness range in LU (default: `11`).
|
|
246
246
|
|
package/demo/demo_8.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import SfxMix from '../index.js';
|
|
2
|
+
|
|
3
|
+
// Example 1: Trim silence without padding
|
|
4
|
+
console.log('Example 1: Trimming without padding...');
|
|
5
|
+
const sfx1 = new SfxMix();
|
|
6
|
+
await sfx1
|
|
7
|
+
.add('part1.mp3')
|
|
8
|
+
.trim()
|
|
9
|
+
.save('part1_trimmed.mp3')
|
|
10
|
+
.then(() => {
|
|
11
|
+
console.log('✓ Successfully exported: vo_intro_text_3_trimmed.mp3');
|
|
12
|
+
})
|
|
13
|
+
.catch((error) => {
|
|
14
|
+
console.error('Error during audio processing:', error);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Example 2: Trim silence with padding (200ms at start, 300ms at end)
|
|
18
|
+
console.log('\nExample 2: Trimming with padding...');
|
|
19
|
+
const sfx2 = new SfxMix();
|
|
20
|
+
await sfx2
|
|
21
|
+
.add('part1.mp3')
|
|
22
|
+
.trim({
|
|
23
|
+
paddingStart: 100, // 100ms of silence at the start
|
|
24
|
+
paddingEnd: 100 // 100ms of silence at the end
|
|
25
|
+
})
|
|
26
|
+
.save('part1_trimmed_padded.mp3')
|
|
27
|
+
.then(() => {
|
|
28
|
+
console.log('✓ Successfully exported: vo_intro_text_3_trimmed_padded.mp3');
|
|
29
|
+
})
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
console.error('Error during audio processing:', error);
|
|
32
|
+
});
|
|
33
|
+
|
package/demo/part1.mp3
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -9,6 +9,9 @@ class SfxMix {
|
|
|
9
9
|
this.actions = [];
|
|
10
10
|
this.currentFile = null;
|
|
11
11
|
this.TMP_DIR = path.resolve(config.tmpDir || path.join(__dirname, 'tmp'));
|
|
12
|
+
|
|
13
|
+
// Default bitrate: null = auto-detect, or specify a value (e.g., 64000, 128000, 192000)
|
|
14
|
+
this.bitrate = config.bitrate !== undefined ? config.bitrate : 64000;
|
|
12
15
|
|
|
13
16
|
// Ensure the temporary directory exists and is writable
|
|
14
17
|
try {
|
|
@@ -48,6 +51,11 @@ class SfxMix {
|
|
|
48
51
|
return this;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
trim(options = {}) {
|
|
55
|
+
this.actions.push({ type: 'trim', options });
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
|
|
52
60
|
|
|
53
61
|
convertAudio(inputFile, outputFile, outputOptions = {}) {
|
|
@@ -127,7 +135,16 @@ class SfxMix {
|
|
|
127
135
|
this.currentFile = tempFile;
|
|
128
136
|
} else if (action.type === 'silence') {
|
|
129
137
|
const tempSilenceFile = path.join(this.TMP_DIR, `temp_silence_${uuidv4()}.mp3`);
|
|
130
|
-
|
|
138
|
+
// Get audio info from current file to match channels and sample rate
|
|
139
|
+
let audioInfo = null;
|
|
140
|
+
if (this.currentFile != null) {
|
|
141
|
+
try {
|
|
142
|
+
audioInfo = await this.getAudioInfo(this.currentFile);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.warn('Could not get audio info, using defaults:', err.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await this.generateSilence(action.duration, tempSilenceFile, audioInfo);
|
|
131
148
|
if (this.currentFile == null) {
|
|
132
149
|
this.currentFile = tempSilenceFile;
|
|
133
150
|
} else {
|
|
@@ -149,6 +166,16 @@ class SfxMix {
|
|
|
149
166
|
fs.unlinkSync(this.currentFile);
|
|
150
167
|
}
|
|
151
168
|
this.currentFile = tempFile;
|
|
169
|
+
} else if (action.type === 'trim') {
|
|
170
|
+
if (this.currentFile == null) {
|
|
171
|
+
throw new Error('No audio to trim. Add audio before trimming.');
|
|
172
|
+
}
|
|
173
|
+
const tempFile = path.join(this.TMP_DIR, `temp_trim_${uuidv4()}.mp3`);
|
|
174
|
+
await this.applyTrim(this.currentFile, action.options, tempFile);
|
|
175
|
+
if (this.isTempFile(this.currentFile)) {
|
|
176
|
+
fs.unlinkSync(this.currentFile);
|
|
177
|
+
}
|
|
178
|
+
this.currentFile = tempFile;
|
|
152
179
|
}
|
|
153
180
|
}
|
|
154
181
|
|
|
@@ -212,7 +239,8 @@ class SfxMix {
|
|
|
212
239
|
filename.includes('temp_concat_') ||
|
|
213
240
|
filename.includes('temp_mix_') ||
|
|
214
241
|
filename.includes('temp_silence_') ||
|
|
215
|
-
filename.includes('temp_filter_')
|
|
242
|
+
filename.includes('temp_filter_') ||
|
|
243
|
+
filename.includes('temp_trim_')
|
|
216
244
|
);
|
|
217
245
|
}
|
|
218
246
|
|
|
@@ -274,11 +302,38 @@ class SfxMix {
|
|
|
274
302
|
});
|
|
275
303
|
}
|
|
276
304
|
|
|
277
|
-
|
|
305
|
+
getAudioInfo(inputFile) {
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
ffmpeg.ffprobe(inputFile, (err, metadata) => {
|
|
308
|
+
if (err) {
|
|
309
|
+
reject(err);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const audioStream = metadata.streams.find(s => s.codec_type === 'audio');
|
|
314
|
+
if (!audioStream) {
|
|
315
|
+
reject(new Error('No audio stream found'));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
resolve({
|
|
320
|
+
channels: audioStream.channels || 2,
|
|
321
|
+
sampleRate: audioStream.sample_rate || 44100,
|
|
322
|
+
bitrate: metadata.format.bit_rate || 128000
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
generateSilence(durationMs, outputFile, audioInfo = null) {
|
|
278
329
|
return new Promise((resolve, reject) => {
|
|
279
330
|
const durationSec = durationMs / 1000;
|
|
280
|
-
const sampleRate = 44100;
|
|
281
|
-
const numChannels = 2;
|
|
331
|
+
const sampleRate = audioInfo?.sampleRate || 44100;
|
|
332
|
+
const numChannels = audioInfo?.channels || 2;
|
|
333
|
+
|
|
334
|
+
// Use audioInfo bitrate if available, otherwise use bitrate or 128000 as fallback
|
|
335
|
+
const bitrate = audioInfo?.bitrate || this.bitrate || 128000;
|
|
336
|
+
|
|
282
337
|
const bytesPerSample = 2; // 16-bit audio
|
|
283
338
|
const bytesPerSecond = sampleRate * numChannels * bytesPerSample;
|
|
284
339
|
let totalBytes = Math.floor(durationSec * bytesPerSecond);
|
|
@@ -295,12 +350,15 @@ class SfxMix {
|
|
|
295
350
|
}
|
|
296
351
|
});
|
|
297
352
|
|
|
353
|
+
const bitrateKbps = Math.floor(bitrate / 1000) + 'k';
|
|
354
|
+
|
|
298
355
|
ffmpeg()
|
|
299
356
|
.input(silenceStream)
|
|
300
357
|
.inputFormat('s16le')
|
|
301
358
|
.audioChannels(numChannels)
|
|
302
359
|
.audioFrequency(sampleRate)
|
|
303
360
|
.audioCodec('libmp3lame')
|
|
361
|
+
.audioBitrate(bitrateKbps)
|
|
304
362
|
.format('mp3')
|
|
305
363
|
.output(outputFile)
|
|
306
364
|
.on('end', () => {
|
|
@@ -316,32 +374,132 @@ class SfxMix {
|
|
|
316
374
|
}
|
|
317
375
|
|
|
318
376
|
applyFilter(inputFile, filterName, options, outputFile) {
|
|
319
|
-
return new Promise((resolve, reject) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
377
|
+
return new Promise(async (resolve, reject) => {
|
|
378
|
+
try {
|
|
379
|
+
const filterChain = this.getFilterChain(filterName, options);
|
|
380
|
+
if (!filterChain) {
|
|
381
|
+
return reject(new Error(`Unknown filter: ${filterName}`));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let bitrateKbps;
|
|
385
|
+
|
|
386
|
+
// If bitrate is null, auto-detect from input file
|
|
387
|
+
if (this.bitrate === null) {
|
|
388
|
+
let audioInfo = null;
|
|
389
|
+
try {
|
|
390
|
+
audioInfo = await this.getAudioInfo(inputFile);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.warn('Could not get audio info for filter, using 128k default:', err.message);
|
|
393
|
+
}
|
|
394
|
+
bitrateKbps = audioInfo ? Math.floor(audioInfo.bitrate / 1000) + 'k' : '128k';
|
|
395
|
+
} else {
|
|
396
|
+
// Use configured default bitrate
|
|
397
|
+
bitrateKbps = Math.floor(this.bitrate / 1000) + 'k';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const command = ffmpeg()
|
|
401
|
+
.input(inputFile)
|
|
402
|
+
.audioFilters(filterChain)
|
|
403
|
+
.audioCodec('libmp3lame')
|
|
404
|
+
.audioBitrate(bitrateKbps)
|
|
405
|
+
.format('mp3')
|
|
406
|
+
.output(outputFile);
|
|
407
|
+
|
|
408
|
+
command
|
|
409
|
+
.on('end', () => resolve())
|
|
410
|
+
.on('error', (err) => reject(err))
|
|
411
|
+
.run();
|
|
412
|
+
} catch (err) {
|
|
413
|
+
reject(err);
|
|
323
414
|
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
324
417
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
418
|
+
applyTrim(inputFile, options, outputFile) {
|
|
419
|
+
return new Promise(async (resolve, reject) => {
|
|
420
|
+
try {
|
|
421
|
+
// Default options for silenceremove filter
|
|
422
|
+
// start_periods: number of silence periods at start to remove (1 = remove silence from start)
|
|
423
|
+
// start_duration: minimum duration of silence to detect at start (in seconds)
|
|
424
|
+
// start_threshold: noise tolerance for start (in dB, e.g., -50dB)
|
|
425
|
+
// stop_periods: number of silence periods at end to remove (-1 = remove all silence from end)
|
|
426
|
+
// stop_duration: minimum duration of silence to detect at end (in seconds)
|
|
427
|
+
// stop_threshold: noise tolerance for end (in dB)
|
|
428
|
+
// paddingStart: milliseconds of silence to add at the start (after removing silence)
|
|
429
|
+
// paddingEnd: milliseconds of silence to add at the end (after removing silence)
|
|
430
|
+
|
|
431
|
+
const startPeriods = options.startPeriods !== undefined ? options.startPeriods : 1;
|
|
432
|
+
const startDuration = options.startDuration !== undefined ? options.startDuration : 0;
|
|
433
|
+
const startThreshold = options.startThreshold !== undefined ? options.startThreshold : -50;
|
|
434
|
+
const stopPeriods = options.stopPeriods !== undefined ? options.stopPeriods : -1;
|
|
435
|
+
const stopDuration = options.stopDuration !== undefined ? options.stopDuration : 0;
|
|
436
|
+
const stopThreshold = options.stopThreshold !== undefined ? options.stopThreshold : -50;
|
|
437
|
+
const paddingStart = options.paddingStart || 0; // in milliseconds
|
|
438
|
+
const paddingEnd = options.paddingEnd || 0; // in milliseconds
|
|
439
|
+
|
|
440
|
+
// Build filter chain
|
|
441
|
+
const filters = [];
|
|
442
|
+
|
|
443
|
+
// Add silenceremove filter
|
|
444
|
+
filters.push(`silenceremove=start_periods=${startPeriods}:start_duration=${startDuration}:start_threshold=${startThreshold}dB:stop_periods=${stopPeriods}:stop_duration=${stopDuration}:stop_threshold=${stopThreshold}dB`);
|
|
445
|
+
|
|
446
|
+
// Add padding at start if specified
|
|
447
|
+
if (paddingStart > 0) {
|
|
448
|
+
filters.push(`adelay=${paddingStart}|${paddingStart}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Add padding at end if specified
|
|
452
|
+
if (paddingEnd > 0) {
|
|
453
|
+
const paddingSec = paddingEnd / 1000;
|
|
454
|
+
filters.push(`apad=pad_dur=${paddingSec}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const filterChain = filters.join(',');
|
|
458
|
+
|
|
459
|
+
let bitrateKbps;
|
|
460
|
+
|
|
461
|
+
// If bitrate is null, auto-detect from input file
|
|
462
|
+
if (this.bitrate === null) {
|
|
463
|
+
let audioInfo = null;
|
|
464
|
+
try {
|
|
465
|
+
audioInfo = await this.getAudioInfo(inputFile);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
console.warn('Could not get audio info for trim, using 128k default:', err.message);
|
|
468
|
+
}
|
|
469
|
+
bitrateKbps = audioInfo ? Math.floor(audioInfo.bitrate / 1000) + 'k' : '128k';
|
|
470
|
+
} else {
|
|
471
|
+
// Use configured default bitrate
|
|
472
|
+
bitrateKbps = Math.floor(this.bitrate / 1000) + 'k';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const command = ffmpeg()
|
|
476
|
+
.input(inputFile)
|
|
477
|
+
.audioFilters(filterChain)
|
|
478
|
+
.audioCodec('libmp3lame')
|
|
479
|
+
.audioBitrate(bitrateKbps)
|
|
480
|
+
.format('mp3')
|
|
481
|
+
.output(outputFile);
|
|
482
|
+
|
|
483
|
+
command
|
|
484
|
+
.on('end', () => resolve())
|
|
485
|
+
.on('error', (err) => reject(err))
|
|
486
|
+
.run();
|
|
487
|
+
} catch (err) {
|
|
488
|
+
reject(err);
|
|
489
|
+
}
|
|
334
490
|
});
|
|
335
491
|
}
|
|
336
492
|
|
|
337
493
|
getFilterChain(filterName, options) {
|
|
338
494
|
switch (filterName) {
|
|
339
495
|
case 'normalize':
|
|
340
|
-
// Normalize audio using
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
496
|
+
// Normalize audio using dynaudnorm filter which is more robust
|
|
497
|
+
// tp parameter is converted to target peak (p parameter, range 0-1)
|
|
498
|
+
// Default tp of -1.5 dB corresponds to approximately 0.95 peak
|
|
499
|
+
const tp = options.tp || -1.5;
|
|
500
|
+
const targetPeak = Math.pow(10, tp / 20); // Convert dB to linear scale
|
|
501
|
+
const p = Math.min(0.99, Math.max(0.5, targetPeak));
|
|
502
|
+
return `dynaudnorm=p=${p.toFixed(2)}:m=100:s=10`;
|
|
345
503
|
|
|
346
504
|
case 'telephone':
|
|
347
505
|
// Telephone effect with parameters
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfxmix",
|
|
3
3
|
"description": "🎧 SfxMix - Powerful and easy-to-use module for processing audio.",
|
|
4
|
-
"version": "1.2
|
|
4
|
+
"version": "1.3.2",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"phaser",
|
|
30
30
|
"compressor",
|
|
31
31
|
"flanger",
|
|
32
|
+
"trim",
|
|
32
33
|
"reverb",
|
|
33
34
|
"highpass",
|
|
34
35
|
"lowpass",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|