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 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: `-3`).
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
+
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "demo",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "demo_1.js",
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ }
12
+ }
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
- await this.generateSilence(action.duration, tempSilenceFile);
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
- generateSilence(durationMs, outputFile) {
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; // Stereo
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
- const filterChain = this.getFilterChain(filterName, options);
321
- if (!filterChain) {
322
- return reject(new Error(`Unknown filter: ${filterName}`));
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
- ffmpeg()
326
- .input(inputFile)
327
- .audioFilters(filterChain)
328
- .audioCodec('libmp3lame')
329
- .format('mp3')
330
- .output(outputFile)
331
- .on('end', () => resolve())
332
- .on('error', (err) => reject(err))
333
- .run();
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 loudnorm filter with parameters
341
- const tp = options.tp || -3;
342
- const i = options.i || -20;
343
- const lra = options.lra || 11;
344
- return `loudnorm=I=${i}:TP=${tp}:LRA=${lra}:print_format=none`;
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.8",
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