sfxmix 1.0.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.
- package/.gitattributes +2 -0
- package/README.md +391 -0
- package/demo/demo_1.mjs +16 -0
- package/demo/demo_2.mjs +14 -0
- package/demo/demo_3.mjs +19 -0
- package/demo/demo_4.mjs +19 -0
- package/demo/glitches.mp3 +0 -0
- package/demo/part1.mp3 +0 -0
- package/demo/part2.mp3 +0 -0
- package/index.js +253 -0
- package/package.json +37 -0
package/.gitattributes
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# 🎧 SfxMix
|
|
2
|
+
|
|
3
|
+
**SfxMix** is a powerful and easy-to-use module for processing audio files using FFmpeg. It provides a fluent interface to concatenate, mix, insert silence, apply filters, and more! ✨
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Features
|
|
8
|
+
|
|
9
|
+
- **Concatenate** multiple audio files seamlessly.
|
|
10
|
+
- **Mix** audio tracks with adjustable durations.
|
|
11
|
+
- **Insert silence** at any point in your audio sequence.
|
|
12
|
+
- **Apply filters** like echo, reverb, normalize, and more.
|
|
13
|
+
- **Parameterizable filters** for fine-grained control.
|
|
14
|
+
- **Fluent interface** for chaining multiple operations.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 📦 Installation
|
|
19
|
+
|
|
20
|
+
Before installing **SfxMix**, ensure that **FFmpeg** is installed and accessible in your system's PATH.
|
|
21
|
+
|
|
22
|
+
### Install FFmpeg
|
|
23
|
+
|
|
24
|
+
- **macOS:** Install via Homebrew
|
|
25
|
+
```bash
|
|
26
|
+
brew install ffmpeg
|
|
27
|
+
```
|
|
28
|
+
- **Windows:** Download from [FFmpeg official website](https://ffmpeg.org/download.html).
|
|
29
|
+
- **Linux:** Install via package manager
|
|
30
|
+
```bash
|
|
31
|
+
sudo apt-get install ffmpeg
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Install SfxMix
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install sfxmix
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 📝 Usage
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
const SfxMix = require('sfxmix');
|
|
46
|
+
|
|
47
|
+
const processor = new SfxMix();
|
|
48
|
+
|
|
49
|
+
processor
|
|
50
|
+
.add('intro.mp3')
|
|
51
|
+
.silence(2000) // 2 seconds of silence
|
|
52
|
+
.add('main.mp3')
|
|
53
|
+
.filter('normalize', { i: -14, tp: -2.0, lra: 7.0 })
|
|
54
|
+
.mix('background.mp3', { duration: 'first' })
|
|
55
|
+
.save('final_output.mp3')
|
|
56
|
+
.then(() => {
|
|
57
|
+
console.log('Audio processing completed successfully! 🎉');
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
console.error('Error during audio processing:', err);
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 🔧 Examples
|
|
66
|
+
|
|
67
|
+
### 1. Concatenate and Mix with Background Music
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
processor
|
|
71
|
+
.add('intro.mp3')
|
|
72
|
+
.add('chapter1.mp3')
|
|
73
|
+
.add('chapter2.mp3')
|
|
74
|
+
.mix('background_music.mp3', { duration: 'first' })
|
|
75
|
+
.save('audiobook_with_music.mp3');
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2. Apply Multiple Filters
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
processor
|
|
82
|
+
.add('voiceover.mp3')
|
|
83
|
+
.filter('normalize', { i: -14 })
|
|
84
|
+
.filter('equalizer', { frequency: 3000, width: 1000, gain: 5 })
|
|
85
|
+
.save('processed_voiceover.mp3');
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. Insert Silence Between Tracks
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
processor
|
|
92
|
+
.add('track1.mp3')
|
|
93
|
+
.silence(2000)
|
|
94
|
+
.add('track2.mp3')
|
|
95
|
+
.silence(2000)
|
|
96
|
+
.add('track3.mp3')
|
|
97
|
+
.save('album_with_silence.mp3');
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 4. Apply Telephone Effect
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
processor
|
|
104
|
+
.add('dialogue.mp3')
|
|
105
|
+
.filter('telephone')
|
|
106
|
+
.save('telephone_effect.mp3');
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 5. Adjust Volume and Add Echo
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
processor
|
|
113
|
+
.add('announcement.mp3')
|
|
114
|
+
.filter('volume', { volume: 1.5 })
|
|
115
|
+
.filter('echo', { delay: 750, decay: 0.7 })
|
|
116
|
+
.save('enhanced_announcement.mp3');
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 📖 API Documentation
|
|
122
|
+
|
|
123
|
+
### Class: `SfxMix`
|
|
124
|
+
|
|
125
|
+
#### Methods
|
|
126
|
+
|
|
127
|
+
- [`add(input)`](#addinput)
|
|
128
|
+
- [`mix(input, options)`](#mixinput-options)
|
|
129
|
+
- [`silence(duration)`](#silenceduration)
|
|
130
|
+
- [`filter(filterName, options)`](#filterfiltername-options)
|
|
131
|
+
- [`save(output)`](#saveoutput)
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### `add(input)`
|
|
136
|
+
|
|
137
|
+
Adds an audio file to the processor for concatenation.
|
|
138
|
+
|
|
139
|
+
- **Parameters:**
|
|
140
|
+
- `input` (string): Path to the audio file.
|
|
141
|
+
- **Returns:** `SfxMix` (for chaining)
|
|
142
|
+
|
|
143
|
+
**Example:**
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
processor.add('part1.mp3').add('part2.mp3');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### `mix(input, options)`
|
|
152
|
+
|
|
153
|
+
Mixes an audio file with the current audio.
|
|
154
|
+
|
|
155
|
+
- **Parameters:**
|
|
156
|
+
- `input` (string): Path to the audio file to mix.
|
|
157
|
+
- `options` (object): (Optional) Mixing options.
|
|
158
|
+
- `duration` (string): Determines the duration of the output. Can be `'longest'`, `'shortest'`, or `'first'`. Default is `'longest'`.
|
|
159
|
+
- **Returns:** `SfxMix` (for chaining)
|
|
160
|
+
|
|
161
|
+
**Example:**
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
processor.mix('sound_effect.wav', { duration: 'first' });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### `silence(duration)`
|
|
170
|
+
|
|
171
|
+
Inserts silence into the audio sequence.
|
|
172
|
+
|
|
173
|
+
- **Parameters:**
|
|
174
|
+
- `duration` (number): Duration of silence in milliseconds.
|
|
175
|
+
- **Returns:** `SfxMix` (for chaining)
|
|
176
|
+
|
|
177
|
+
**Example:**
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
processor.silence(3000); // Inserts 3 seconds of silence
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### `filter(filterName, options)`
|
|
186
|
+
|
|
187
|
+
Applies an audio filter to the current audio.
|
|
188
|
+
|
|
189
|
+
- **Parameters:**
|
|
190
|
+
- `filterName` (string): Name of the filter to apply.
|
|
191
|
+
- `options` (object): (Optional) Filter-specific options.
|
|
192
|
+
- **Returns:** `SfxMix` (for chaining)
|
|
193
|
+
|
|
194
|
+
**Supported Filters:**
|
|
195
|
+
|
|
196
|
+
- [`normalize`](#filternormalize)
|
|
197
|
+
- [`telephone`](#filtertelephone)
|
|
198
|
+
- [`echo`](#filterecho)
|
|
199
|
+
- [`reverb`](#filterreverb)
|
|
200
|
+
- [`highpass`](#filterhighpass)
|
|
201
|
+
- [`lowpass`](#filterlowpass)
|
|
202
|
+
- [`volume`](#filtervolume)
|
|
203
|
+
- [`equalizer`](#filterequalizer)
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### `save(output)`
|
|
208
|
+
|
|
209
|
+
Processes the audio according to the specified actions and saves the result.
|
|
210
|
+
|
|
211
|
+
- **Parameters:**
|
|
212
|
+
- `output` (string): Path to the output audio file.
|
|
213
|
+
- **Returns:** `Promise` (resolves when processing is complete)
|
|
214
|
+
|
|
215
|
+
**Example:**
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
processor.save('output.mp3');
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🎛️ Filters
|
|
224
|
+
|
|
225
|
+
### Filter: `normalize`
|
|
226
|
+
|
|
227
|
+
Normalizes audio loudness to a specified target using the EBU R128 standard.
|
|
228
|
+
|
|
229
|
+
- **Options:**
|
|
230
|
+
- `tp` (number): Maximum true peak level in dBTP (default: `-1.5`).
|
|
231
|
+
- `i` (number): Target integrated loudness in LUFS (default: `-16`).
|
|
232
|
+
- `lra` (number): Loudness range in LU (default: `11`).
|
|
233
|
+
|
|
234
|
+
**Example:**
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
processor.filter('normalize', { i: -14, tp: -2.0, lra: 7.0 });
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### Filter: `telephone`
|
|
243
|
+
|
|
244
|
+
Applies a telephone effect by applying high-pass and low-pass filters.
|
|
245
|
+
|
|
246
|
+
- **Options:**
|
|
247
|
+
- `lowFreq` (number): High-pass filter cutoff frequency in Hz (default: `300`).
|
|
248
|
+
- `highFreq` (number): Low-pass filter cutoff frequency in Hz (default: `3400`).
|
|
249
|
+
|
|
250
|
+
**Example:**
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
processor.filter('telephone', { lowFreq: 400, highFreq: 3000 });
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### Filter: `echo`
|
|
259
|
+
|
|
260
|
+
Adds an echo effect to the audio.
|
|
261
|
+
|
|
262
|
+
- **Options:**
|
|
263
|
+
- `delay` (number): Echo delay in milliseconds (default: `500`).
|
|
264
|
+
- `decay` (number): Echo decay factor between `0` and `1` (default: `0.5`).
|
|
265
|
+
|
|
266
|
+
**Example:**
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
processor.filter('echo', { delay: 1000, decay: 0.6 });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### Filter: `reverb`
|
|
275
|
+
|
|
276
|
+
Applies a reverb effect to the audio.
|
|
277
|
+
|
|
278
|
+
- **Options:** None
|
|
279
|
+
|
|
280
|
+
**Example:**
|
|
281
|
+
|
|
282
|
+
```javascript
|
|
283
|
+
processor.filter('reverb');
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### Filter: `highpass`
|
|
289
|
+
|
|
290
|
+
Applies a high-pass filter to remove frequencies below the cutoff.
|
|
291
|
+
|
|
292
|
+
- **Options:**
|
|
293
|
+
- `frequency` (number): Cutoff frequency in Hz.
|
|
294
|
+
|
|
295
|
+
**Example:**
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
processor.filter('highpass', { frequency: 1000 });
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### Filter: `lowpass`
|
|
304
|
+
|
|
305
|
+
Applies a low-pass filter to remove frequencies above the cutoff.
|
|
306
|
+
|
|
307
|
+
- **Options:**
|
|
308
|
+
- `frequency` (number): Cutoff frequency in Hz.
|
|
309
|
+
|
|
310
|
+
**Example:**
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
processor.filter('lowpass', { frequency: 2000 });
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### Filter: `volume`
|
|
319
|
+
|
|
320
|
+
Adjusts the audio volume.
|
|
321
|
+
|
|
322
|
+
- **Options:**
|
|
323
|
+
- `volume` (number): Volume multiplier (e.g., `0.5` for 50%).
|
|
324
|
+
|
|
325
|
+
**Example:**
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
processor.filter('volume', { volume: 0.8 });
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### Filter: `equalizer`
|
|
334
|
+
|
|
335
|
+
Applies an equalizer effect to adjust specific frequencies.
|
|
336
|
+
|
|
337
|
+
- **Options:**
|
|
338
|
+
- `frequency` (number): Center frequency in Hz.
|
|
339
|
+
- `width` (number): Bandwidth in Hz.
|
|
340
|
+
- `gain` (number): Gain in dB (positive to boost, negative to reduce).
|
|
341
|
+
|
|
342
|
+
**Example:**
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
processor.filter('equalizer', { frequency: 1000, width: 200, gain: -10 });
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## ⚠️ Important Notes
|
|
351
|
+
|
|
352
|
+
- **FFmpeg Installation:** Ensure FFmpeg is installed and accessible in your system's PATH.
|
|
353
|
+
- **File Permissions:** The module creates and deletes temporary files during processing. Ensure the application has the necessary permissions.
|
|
354
|
+
- **Audio Formats:** The module assumes input files are in MP3 format. For other formats, adjust codec and format settings accordingly.
|
|
355
|
+
- **Error Handling:** Always handle rejections from the `save()` method to catch any processing errors.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 📄 License
|
|
360
|
+
|
|
361
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 🙌 Contributing
|
|
366
|
+
|
|
367
|
+
Contributions are welcome! Feel free to submit a pull request or open an issue on GitHub.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 💬 Support
|
|
372
|
+
|
|
373
|
+
If you encounter any issues or have questions, please open an issue on the [GitHub repository](https://github.com/clasen/SfxMix/issues).
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 📚 References
|
|
378
|
+
|
|
379
|
+
- [FFmpeg Documentation](https://ffmpeg.org/documentation.html)
|
|
380
|
+
- [fluent-ffmpeg GitHub](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg)
|
|
381
|
+
- [EBU R128 Loudness Recommendation](https://tech.ebu.ch/docs/r/r128.pdf)
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 🌟 Acknowledgments
|
|
386
|
+
|
|
387
|
+
- Special thanks to the developers of FFmpeg and fluent-ffmpeg for their invaluable tools.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
Enjoy processing your audio with **SfxMix**! 🎶✨
|
package/demo/demo_1.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import SfxMix from '../index.js';
|
|
2
|
+
const sfx = new SfxMix();
|
|
3
|
+
|
|
4
|
+
// Concatenate part1.mp3 and part2.mp3,
|
|
5
|
+
// then mix with glitches.wav for the duration of the concatenated audio
|
|
6
|
+
await sfx
|
|
7
|
+
.add('part1.mp3')
|
|
8
|
+
.add('part2.mp3')
|
|
9
|
+
.mix('glitches.mp3', { duration: 'first' })
|
|
10
|
+
.save('demo1_add_add_mix.mp3')
|
|
11
|
+
.then(() => {
|
|
12
|
+
console.log('Successfully exported: demo1_add_add_mix.mp3');
|
|
13
|
+
})
|
|
14
|
+
.catch((error) => {
|
|
15
|
+
console.error('Error during audio processing:', error);
|
|
16
|
+
});
|
package/demo/demo_2.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import SfxMix from '../index.js';
|
|
2
|
+
const sfx = new SfxMix();
|
|
3
|
+
|
|
4
|
+
// Mix part1.mp3 with part2.mp3 and save the result
|
|
5
|
+
try {
|
|
6
|
+
await sfx
|
|
7
|
+
.add('part1.mp3')
|
|
8
|
+
.mix('part2.mp3')
|
|
9
|
+
.save('demo2_add_mix.mp3');
|
|
10
|
+
|
|
11
|
+
console.log('Successfully exported: demo2_add_mix.mp3');
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Error exporting demo2_add_mix.mp3:', error);
|
|
14
|
+
}
|
package/demo/demo_3.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import SfxMix from '../index.js';
|
|
2
|
+
const sfx = new SfxMix();
|
|
3
|
+
|
|
4
|
+
// Complex audio processing example
|
|
5
|
+
// This demonstrates adding files, inserting silence, mixing, and applying filters
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await sfx.add('part1.mp3')
|
|
9
|
+
.silence(2000) // Add 2 seconds of silence
|
|
10
|
+
.add('part2.mp3')
|
|
11
|
+
.mix('glitches.mp3', { duration: 'first' }) // Mix with glitches.mp3 for the duration of the first audio
|
|
12
|
+
.filter('telephone') // Apply telephone effect
|
|
13
|
+
.filter('normalize') // Normalize audio levels
|
|
14
|
+
.save('demo3_complex.mp3');
|
|
15
|
+
|
|
16
|
+
console.log('Successfully exported: demo3_complex.mp3');
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error exporting demo3_complex.mp3:', error);
|
|
19
|
+
}
|
package/demo/demo_4.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import SfxMix from '../index.js';
|
|
2
|
+
const sfx = new SfxMix();
|
|
3
|
+
|
|
4
|
+
// Complex audio processing example
|
|
5
|
+
// This demonstrates mixing multiple files, adding another file, and normalizing the audio
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await sfx
|
|
9
|
+
.mix('part1.mp3')
|
|
10
|
+
.mix('part2.mp3')
|
|
11
|
+
.add('part2.mp3')
|
|
12
|
+
// Normalize the audio to -3 dB
|
|
13
|
+
.filter('normalize', { tp: -3 })
|
|
14
|
+
.save('demo4_mix_mix_add_normalized.mp3');
|
|
15
|
+
|
|
16
|
+
console.log('Successfully exported: demo4_mix_mix_add_normalized.mp3');
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error exporting demo4_mix_mix_add_normalized.mp3:', error);
|
|
19
|
+
}
|
|
Binary file
|
package/demo/part1.mp3
ADDED
|
Binary file
|
package/demo/part2.mp3
ADDED
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
const ffmpeg = require('fluent-ffmpeg');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
class SfxMix {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.actions = [];
|
|
7
|
+
this.currentFile = null; // Keep track of the current audio file
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
add(input) {
|
|
11
|
+
this.actions.push({ type: 'add', input });
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
mix(input, options = {}) {
|
|
16
|
+
if (this.actions.length === 0) {
|
|
17
|
+
this.actions.push({ type: 'add', input });
|
|
18
|
+
} else {
|
|
19
|
+
this.actions.push({ type: 'mix', input, options });
|
|
20
|
+
}
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
silence(milliseconds) {
|
|
25
|
+
this.actions.push({ type: 'silence', duration: milliseconds });
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
filter(filterName, options = {}) {
|
|
30
|
+
this.actions.push({ type: 'filter', filterName, options });
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
save(output) {
|
|
35
|
+
return new Promise(async (resolve, reject) => {
|
|
36
|
+
try {
|
|
37
|
+
for (let action of this.actions) {
|
|
38
|
+
if (action.type === 'add') {
|
|
39
|
+
// Existing add logic
|
|
40
|
+
if (this.currentFile == null) {
|
|
41
|
+
this.currentFile = action.input;
|
|
42
|
+
} else {
|
|
43
|
+
const tempFile = `temp_concat_${Date.now()}.mp3`;
|
|
44
|
+
await concatenateAudioFiles([this.currentFile, action.input], tempFile);
|
|
45
|
+
if (isTempFile(this.currentFile)) {
|
|
46
|
+
fs.unlinkSync(this.currentFile);
|
|
47
|
+
}
|
|
48
|
+
this.currentFile = tempFile;
|
|
49
|
+
}
|
|
50
|
+
} else if (action.type === 'mix') {
|
|
51
|
+
// Existing mix logic
|
|
52
|
+
if (this.currentFile == null) {
|
|
53
|
+
throw new Error('No audio to mix with. Add or concatenate audio before mixing.');
|
|
54
|
+
}
|
|
55
|
+
const tempFile = `temp_mix_${Date.now()}.mp3`;
|
|
56
|
+
await mixAudioFiles(this.currentFile, action.input, tempFile, action.options);
|
|
57
|
+
if (isTempFile(this.currentFile)) {
|
|
58
|
+
fs.unlinkSync(this.currentFile);
|
|
59
|
+
}
|
|
60
|
+
this.currentFile = tempFile;
|
|
61
|
+
} else if (action.type === 'silence') {
|
|
62
|
+
// Existing silence logic
|
|
63
|
+
const tempSilenceFile = `temp_silence_${Date.now()}.mp3`;
|
|
64
|
+
await generateSilence(action.duration, tempSilenceFile);
|
|
65
|
+
if (this.currentFile == null) {
|
|
66
|
+
this.currentFile = tempSilenceFile;
|
|
67
|
+
} else {
|
|
68
|
+
const tempFile = `temp_concat_${Date.now()}.mp3`;
|
|
69
|
+
await concatenateAudioFiles([this.currentFile, tempSilenceFile], tempFile);
|
|
70
|
+
if (isTempFile(this.currentFile)) {
|
|
71
|
+
fs.unlinkSync(this.currentFile);
|
|
72
|
+
}
|
|
73
|
+
fs.unlinkSync(tempSilenceFile); // Remove the silence file
|
|
74
|
+
this.currentFile = tempFile;
|
|
75
|
+
}
|
|
76
|
+
} else if (action.type === 'filter') {
|
|
77
|
+
// New filter logic
|
|
78
|
+
if (this.currentFile == null) {
|
|
79
|
+
throw new Error('No audio to apply filter to. Add audio before applying filters.');
|
|
80
|
+
}
|
|
81
|
+
const tempFile = `temp_filter_${Date.now()}.mp3`;
|
|
82
|
+
await applyFilter(this.currentFile, action.filterName, action.options, tempFile);
|
|
83
|
+
if (isTempFile(this.currentFile)) {
|
|
84
|
+
fs.unlinkSync(this.currentFile);
|
|
85
|
+
}
|
|
86
|
+
this.currentFile = tempFile;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Finalize output
|
|
90
|
+
fs.renameSync(this.currentFile, output);
|
|
91
|
+
resolve(output);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
reject(err);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Helper functions
|
|
100
|
+
|
|
101
|
+
function isTempFile(filename) {
|
|
102
|
+
return (
|
|
103
|
+
filename.startsWith('temp_concat_') ||
|
|
104
|
+
filename.startsWith('temp_mix_') ||
|
|
105
|
+
filename.startsWith('temp_silence_') ||
|
|
106
|
+
filename.startsWith('temp_filter_')
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function concatenateAudioFiles(inputFiles, outputFile) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const concatList = inputFiles.map(file => `file '${file}'`).join('\n');
|
|
113
|
+
const concatFile = `concat_${Date.now()}.txt`;
|
|
114
|
+
fs.writeFileSync(concatFile, concatList);
|
|
115
|
+
|
|
116
|
+
ffmpeg()
|
|
117
|
+
.input(concatFile)
|
|
118
|
+
.inputOptions(['-f', 'concat', '-safe', '0'])
|
|
119
|
+
.outputOptions(['-c', 'copy'])
|
|
120
|
+
.output(outputFile)
|
|
121
|
+
.on('end', () => {
|
|
122
|
+
fs.unlinkSync(concatFile);
|
|
123
|
+
resolve();
|
|
124
|
+
})
|
|
125
|
+
.on('error', (err) => {
|
|
126
|
+
fs.unlinkSync(concatFile);
|
|
127
|
+
reject(err);
|
|
128
|
+
})
|
|
129
|
+
.run();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mixAudioFiles(inputFile1, inputFile2, outputFile, options = {}) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const durationOption = options.duration || 'longest'; // Default to 'longest'
|
|
136
|
+
ffmpeg()
|
|
137
|
+
.input(inputFile1)
|
|
138
|
+
.input(inputFile2)
|
|
139
|
+
.complexFilter([
|
|
140
|
+
{
|
|
141
|
+
filter: 'amix',
|
|
142
|
+
options: {
|
|
143
|
+
inputs: 2,
|
|
144
|
+
duration: durationOption,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
])
|
|
148
|
+
.audioCodec('libmp3lame') // Ensure the codec is MP3
|
|
149
|
+
.format('mp3') // Ensure the format is MP3
|
|
150
|
+
.output(outputFile)
|
|
151
|
+
.on('end', () => resolve())
|
|
152
|
+
.on('error', (err) => reject(err))
|
|
153
|
+
.run();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function generateSilence(durationMs, outputFile) {
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const durationSec = durationMs / 1000;
|
|
160
|
+
ffmpeg()
|
|
161
|
+
.input('anullsrc=channel_layout=stereo:sample_rate=44100')
|
|
162
|
+
.inputOptions(['-f', 'lavfi', '-t', `${durationSec}`])
|
|
163
|
+
.audioCodec('libmp3lame')
|
|
164
|
+
.format('mp3')
|
|
165
|
+
.output(outputFile)
|
|
166
|
+
.on('end', () => {
|
|
167
|
+
if (fs.existsSync(outputFile)) {
|
|
168
|
+
resolve();
|
|
169
|
+
} else {
|
|
170
|
+
reject(new Error(`Failed to generate silence file: ${outputFile}`));
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
.on('error', (err) => reject(err))
|
|
174
|
+
.run();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function applyFilter(inputFile, filterName, options, outputFile) {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const filterChain = getFilterChain(filterName, options);
|
|
181
|
+
if (!filterChain) {
|
|
182
|
+
return reject(new Error(`Unknown filter: ${filterName}`));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
ffmpeg()
|
|
186
|
+
.input(inputFile)
|
|
187
|
+
.audioFilters(filterChain)
|
|
188
|
+
.audioCodec('libmp3lame')
|
|
189
|
+
.format('mp3')
|
|
190
|
+
.output(outputFile)
|
|
191
|
+
.on('end', () => resolve())
|
|
192
|
+
.on('error', (err) => reject(err))
|
|
193
|
+
.run();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getFilterChain(filterName, options) {
|
|
198
|
+
switch (filterName) {
|
|
199
|
+
case 'normalize':
|
|
200
|
+
// Normalize audio using loudnorm filter with parameters
|
|
201
|
+
// Options: i (target integrated loudness), tp (true peak), lra (loudness range)
|
|
202
|
+
const i = options.i || -16;
|
|
203
|
+
const tp = options.tp || -1.5;
|
|
204
|
+
const lra = options.lra || 11;
|
|
205
|
+
return `loudnorm=I=${i}:TP=${tp}:LRA=${lra}:print_format=none`;
|
|
206
|
+
case 'telephone':
|
|
207
|
+
// Telephone effect with parameters
|
|
208
|
+
// Options: lowFreq (default 300), highFreq (default 3400)
|
|
209
|
+
const lowFreq = options.lowFreq || 300;
|
|
210
|
+
const highFreq = options.highFreq || 3400;
|
|
211
|
+
return `highpass=f=${lowFreq}, lowpass=f=${highFreq}`;
|
|
212
|
+
case 'echo':
|
|
213
|
+
// Default echo parameters: delay=500ms, decay=0.5
|
|
214
|
+
const echoDelay = options.delay || 500;
|
|
215
|
+
const echoDecay = options.decay || 0.5;
|
|
216
|
+
return `aecho=0.8:0.88:${echoDelay}:${echoDecay}`;
|
|
217
|
+
case 'reverb':
|
|
218
|
+
// Simple reverb effect
|
|
219
|
+
return 'areverb';
|
|
220
|
+
case 'highpass':
|
|
221
|
+
// High-pass filter: cutoff frequency in Hz
|
|
222
|
+
if (options.frequency) {
|
|
223
|
+
return `highpass=f=${options.frequency}`;
|
|
224
|
+
} else {
|
|
225
|
+
throw new Error('High-pass filter requires "frequency" option.');
|
|
226
|
+
}
|
|
227
|
+
case 'lowpass':
|
|
228
|
+
// Low-pass filter: cutoff frequency in Hz
|
|
229
|
+
if (options.frequency) {
|
|
230
|
+
return `lowpass=f=${options.frequency}`;
|
|
231
|
+
} else {
|
|
232
|
+
throw new Error('Low-pass filter requires "frequency" option.');
|
|
233
|
+
}
|
|
234
|
+
case 'volume':
|
|
235
|
+
// Volume adjustment: volume multiplier (e.g., 0.5 for 50%)
|
|
236
|
+
if (options.volume !== undefined) {
|
|
237
|
+
return `volume=${options.volume}`;
|
|
238
|
+
} else {
|
|
239
|
+
throw new Error('Volume filter requires "volume" option.');
|
|
240
|
+
}
|
|
241
|
+
case 'equalizer':
|
|
242
|
+
// Equalizer filter: frequency, width, gain
|
|
243
|
+
if (options.frequency && options.width && options.gain !== undefined) {
|
|
244
|
+
return `equalizer=f=${options.frequency}:width_type=h:width=${options.width}:g=${options.gain}`;
|
|
245
|
+
} else {
|
|
246
|
+
throw new Error('Equalizer filter requires "frequency", "width", and "gain" options.');
|
|
247
|
+
}
|
|
248
|
+
default:
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = SfxMix;
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sfxmix",
|
|
3
|
+
"description": "🎧 SfxMix - powerful and easy-to-use module for processing audio",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"fluent-ffmpeg": "^2.1.3"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/clasen/SfxMix.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"audio",
|
|
16
|
+
"concat",
|
|
17
|
+
"mix",
|
|
18
|
+
"silence",
|
|
19
|
+
"effects",
|
|
20
|
+
"filters",
|
|
21
|
+
"mp3",
|
|
22
|
+
"wav",
|
|
23
|
+
"echo",
|
|
24
|
+
"delay",
|
|
25
|
+
"telephone",
|
|
26
|
+
"equalizer",
|
|
27
|
+
"volume",
|
|
28
|
+
"normalize",
|
|
29
|
+
"ffmpeg",
|
|
30
|
+
"fluent-ffmpeg"
|
|
31
|
+
],
|
|
32
|
+
"author": "Martin Clasen",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/clasen/SfxMix/issues"
|
|
36
|
+
}
|
|
37
|
+
}
|