sfxmix 1.2.8 → 1.3.0

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.js +90 -23
  3. package/package.json +1 -1
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/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 {
@@ -127,7 +130,16 @@ class SfxMix {
127
130
  this.currentFile = tempFile;
128
131
  } else if (action.type === 'silence') {
129
132
  const tempSilenceFile = path.join(this.TMP_DIR, `temp_silence_${uuidv4()}.mp3`);
130
- await this.generateSilence(action.duration, tempSilenceFile);
133
+ // Get audio info from current file to match channels and sample rate
134
+ let audioInfo = null;
135
+ if (this.currentFile != null) {
136
+ try {
137
+ audioInfo = await this.getAudioInfo(this.currentFile);
138
+ } catch (err) {
139
+ console.warn('Could not get audio info, using defaults:', err.message);
140
+ }
141
+ }
142
+ await this.generateSilence(action.duration, tempSilenceFile, audioInfo);
131
143
  if (this.currentFile == null) {
132
144
  this.currentFile = tempSilenceFile;
133
145
  } else {
@@ -274,11 +286,38 @@ class SfxMix {
274
286
  });
275
287
  }
276
288
 
277
- generateSilence(durationMs, outputFile) {
289
+ getAudioInfo(inputFile) {
290
+ return new Promise((resolve, reject) => {
291
+ ffmpeg.ffprobe(inputFile, (err, metadata) => {
292
+ if (err) {
293
+ reject(err);
294
+ return;
295
+ }
296
+
297
+ const audioStream = metadata.streams.find(s => s.codec_type === 'audio');
298
+ if (!audioStream) {
299
+ reject(new Error('No audio stream found'));
300
+ return;
301
+ }
302
+
303
+ resolve({
304
+ channels: audioStream.channels || 2,
305
+ sampleRate: audioStream.sample_rate || 44100,
306
+ bitrate: metadata.format.bit_rate || 128000
307
+ });
308
+ });
309
+ });
310
+ }
311
+
312
+ generateSilence(durationMs, outputFile, audioInfo = null) {
278
313
  return new Promise((resolve, reject) => {
279
314
  const durationSec = durationMs / 1000;
280
- const sampleRate = 44100;
281
- const numChannels = 2; // Stereo
315
+ const sampleRate = audioInfo?.sampleRate || 44100;
316
+ const numChannels = audioInfo?.channels || 2;
317
+
318
+ // Use audioInfo bitrate if available, otherwise use bitrate or 128000 as fallback
319
+ const bitrate = audioInfo?.bitrate || this.bitrate || 128000;
320
+
282
321
  const bytesPerSample = 2; // 16-bit audio
283
322
  const bytesPerSecond = sampleRate * numChannels * bytesPerSample;
284
323
  let totalBytes = Math.floor(durationSec * bytesPerSecond);
@@ -295,12 +334,15 @@ class SfxMix {
295
334
  }
296
335
  });
297
336
 
337
+ const bitrateKbps = Math.floor(bitrate / 1000) + 'k';
338
+
298
339
  ffmpeg()
299
340
  .input(silenceStream)
300
341
  .inputFormat('s16le')
301
342
  .audioChannels(numChannels)
302
343
  .audioFrequency(sampleRate)
303
344
  .audioCodec('libmp3lame')
345
+ .audioBitrate(bitrateKbps)
304
346
  .format('mp3')
305
347
  .output(outputFile)
306
348
  .on('end', () => {
@@ -316,32 +358,57 @@ class SfxMix {
316
358
  }
317
359
 
318
360
  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}`));
323
- }
361
+ return new Promise(async (resolve, reject) => {
362
+ try {
363
+ const filterChain = this.getFilterChain(filterName, options);
364
+ if (!filterChain) {
365
+ return reject(new Error(`Unknown filter: ${filterName}`));
366
+ }
324
367
 
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();
368
+ let bitrateKbps;
369
+
370
+ // If bitrate is null, auto-detect from input file
371
+ if (this.bitrate === null) {
372
+ let audioInfo = null;
373
+ try {
374
+ audioInfo = await this.getAudioInfo(inputFile);
375
+ } catch (err) {
376
+ console.warn('Could not get audio info for filter, using 128k default:', err.message);
377
+ }
378
+ bitrateKbps = audioInfo ? Math.floor(audioInfo.bitrate / 1000) + 'k' : '128k';
379
+ } else {
380
+ // Use configured default bitrate
381
+ bitrateKbps = Math.floor(this.bitrate / 1000) + 'k';
382
+ }
383
+
384
+ const command = ffmpeg()
385
+ .input(inputFile)
386
+ .audioFilters(filterChain)
387
+ .audioCodec('libmp3lame')
388
+ .audioBitrate(bitrateKbps)
389
+ .format('mp3')
390
+ .output(outputFile);
391
+
392
+ command
393
+ .on('end', () => resolve())
394
+ .on('error', (err) => reject(err))
395
+ .run();
396
+ } catch (err) {
397
+ reject(err);
398
+ }
334
399
  });
335
400
  }
336
401
 
337
402
  getFilterChain(filterName, options) {
338
403
  switch (filterName) {
339
404
  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`;
405
+ // Normalize audio using dynaudnorm filter which is more robust
406
+ // tp parameter is converted to target peak (p parameter, range 0-1)
407
+ // Default tp of -1.5 dB corresponds to approximately 0.95 peak
408
+ const tp = options.tp || -1.5;
409
+ const targetPeak = Math.pow(10, tp / 20); // Convert dB to linear scale
410
+ const p = Math.min(0.99, Math.max(0.5, targetPeak));
411
+ return `dynaudnorm=p=${p.toFixed(2)}:m=100:s=10`;
345
412
 
346
413
  case 'telephone':
347
414
  // 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.0",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "fluent-ffmpeg": "^2.1.3",