sfxmix 1.2.2 → 1.2.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/.claude/settings.local.json +9 -0
- package/demo/demo_7.mjs +15 -0
- package/index.js +121 -4
- package/package.json +3 -1
package/demo/demo_7.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import SfxMix from '../index.js';
|
|
2
|
+
const sfx = new SfxMix();
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Trim silence from beginning and end of to_trim.mp3
|
|
6
|
+
await sfx
|
|
7
|
+
.add('to_trim.mp3')
|
|
8
|
+
.trim()
|
|
9
|
+
.save('trimmed_audio.ogg')
|
|
10
|
+
.then(() => {
|
|
11
|
+
console.log('Successfully exported: trimmed_audio.ogg');
|
|
12
|
+
})
|
|
13
|
+
.catch((error) => {
|
|
14
|
+
console.error('Error during audio processing:', error);
|
|
15
|
+
});
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const ffmpeg = require('fluent-ffmpeg');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
const { Readable } = require('stream');
|
|
5
6
|
|
|
6
7
|
class SfxMix {
|
|
7
8
|
constructor(config = {}) {
|
|
@@ -47,7 +48,103 @@ class SfxMix {
|
|
|
47
48
|
return this;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
exportOgg(output) {
|
|
52
|
+
return new Promise(async (resolve, reject) => {
|
|
53
|
+
try {
|
|
54
|
+
for (let action of this.actions) {
|
|
55
|
+
if (action.type === 'add') {
|
|
56
|
+
if (this.currentFile == null) {
|
|
57
|
+
this.currentFile = path.isAbsolute(action.input) ? action.input : path.resolve(process.cwd(), action.input);
|
|
58
|
+
} else {
|
|
59
|
+
const tempFile = path.join(this.TMP_DIR, `temp_concat_${uuidv4()}.mp3`);
|
|
60
|
+
await this.concatenateAudioFiles([this.currentFile, action.input], tempFile);
|
|
61
|
+
if (this.isTempFile(this.currentFile)) {
|
|
62
|
+
fs.unlinkSync(this.currentFile);
|
|
63
|
+
}
|
|
64
|
+
this.currentFile = tempFile;
|
|
65
|
+
}
|
|
66
|
+
} else if (action.type === 'mix') {
|
|
67
|
+
if (this.currentFile == null) {
|
|
68
|
+
throw new Error('No audio to mix with. Add or concatenate audio before mixing.');
|
|
69
|
+
}
|
|
70
|
+
const tempFile = path.join(this.TMP_DIR, `temp_mix_${uuidv4()}.mp3`);
|
|
71
|
+
await this.mixAudioFiles(this.currentFile, action.input, tempFile, action.options);
|
|
72
|
+
if (this.isTempFile(this.currentFile)) {
|
|
73
|
+
fs.unlinkSync(this.currentFile);
|
|
74
|
+
}
|
|
75
|
+
this.currentFile = tempFile;
|
|
76
|
+
} else if (action.type === 'silence') {
|
|
77
|
+
const tempSilenceFile = path.join(this.TMP_DIR, `temp_silence_${uuidv4()}.mp3`);
|
|
78
|
+
await this.generateSilence(action.duration, tempSilenceFile);
|
|
79
|
+
if (this.currentFile == null) {
|
|
80
|
+
this.currentFile = tempSilenceFile;
|
|
81
|
+
} else {
|
|
82
|
+
const tempFile = path.join(this.TMP_DIR, `temp_concat_${uuidv4()}.mp3`);
|
|
83
|
+
await this.concatenateAudioFiles([this.currentFile, tempSilenceFile], tempFile);
|
|
84
|
+
if (this.isTempFile(this.currentFile)) {
|
|
85
|
+
fs.unlinkSync(this.currentFile);
|
|
86
|
+
}
|
|
87
|
+
fs.unlinkSync(tempSilenceFile);
|
|
88
|
+
this.currentFile = tempFile;
|
|
89
|
+
}
|
|
90
|
+
} else if (action.type === 'filter') {
|
|
91
|
+
if (this.currentFile == null) {
|
|
92
|
+
throw new Error('No audio to apply filter to. Add audio before applying filters.');
|
|
93
|
+
}
|
|
94
|
+
const tempFile = path.join(this.TMP_DIR, `temp_filter_${uuidv4()}.mp3`);
|
|
95
|
+
await this.applyFilter(this.currentFile, action.filterName, action.options, tempFile);
|
|
96
|
+
if (this.isTempFile(this.currentFile)) {
|
|
97
|
+
fs.unlinkSync(this.currentFile);
|
|
98
|
+
}
|
|
99
|
+
this.currentFile = tempFile;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const absoluteOutput = path.resolve(process.cwd(), output);
|
|
104
|
+
const outputDir = path.dirname(absoluteOutput);
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(outputDir)) {
|
|
107
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!fs.existsSync(this.currentFile)) {
|
|
111
|
+
throw new Error(`Source file does not exist: ${this.currentFile}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await this.convertToOgg(this.currentFile, absoluteOutput);
|
|
115
|
+
|
|
116
|
+
if (this.isTempFile(this.currentFile)) {
|
|
117
|
+
fs.unlinkSync(this.currentFile);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.reset();
|
|
121
|
+
resolve(absoluteOutput);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error('Error during OGG export:', err);
|
|
124
|
+
reject(err);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
convertToOgg(inputFile, outputFile) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
ffmpeg()
|
|
132
|
+
.input(inputFile)
|
|
133
|
+
.audioCodec('libopus')
|
|
134
|
+
.format('ogg')
|
|
135
|
+
.output(outputFile)
|
|
136
|
+
.on('end', () => resolve())
|
|
137
|
+
.on('error', (err) => reject(err))
|
|
138
|
+
.run();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
50
142
|
save(output) {
|
|
143
|
+
// Check if output file has .ogg extension
|
|
144
|
+
if (output.toLowerCase().endsWith('.ogg')) {
|
|
145
|
+
return this.exportOgg(output);
|
|
146
|
+
}
|
|
147
|
+
|
|
51
148
|
return new Promise(async (resolve, reject) => {
|
|
52
149
|
try {
|
|
53
150
|
for (let action of this.actions) {
|
|
@@ -201,8 +298,8 @@ class SfxMix {
|
|
|
201
298
|
},
|
|
202
299
|
},
|
|
203
300
|
])
|
|
204
|
-
.audioCodec('libmp3lame')
|
|
205
|
-
.format('mp3')
|
|
301
|
+
.audioCodec('libmp3lame')
|
|
302
|
+
.format('mp3')
|
|
206
303
|
.output(outputFile)
|
|
207
304
|
.on('end', () => resolve())
|
|
208
305
|
.on('error', (err) => reject(err))
|
|
@@ -213,9 +310,29 @@ class SfxMix {
|
|
|
213
310
|
generateSilence(durationMs, outputFile) {
|
|
214
311
|
return new Promise((resolve, reject) => {
|
|
215
312
|
const durationSec = durationMs / 1000;
|
|
313
|
+
const sampleRate = 44100;
|
|
314
|
+
const numChannels = 2; // Stereo
|
|
315
|
+
const bytesPerSample = 2; // 16-bit audio
|
|
316
|
+
const bytesPerSecond = sampleRate * numChannels * bytesPerSample;
|
|
317
|
+
let totalBytes = Math.floor(durationSec * bytesPerSecond);
|
|
318
|
+
|
|
319
|
+
const silenceStream = new Readable({
|
|
320
|
+
read(size) {
|
|
321
|
+
const chunkSize = Math.min(size, totalBytes);
|
|
322
|
+
if (chunkSize <= 0) {
|
|
323
|
+
this.push(null);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.push(Buffer.alloc(chunkSize, 0));
|
|
327
|
+
totalBytes -= chunkSize;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
216
331
|
ffmpeg()
|
|
217
|
-
.input(
|
|
218
|
-
.
|
|
332
|
+
.input(silenceStream)
|
|
333
|
+
.inputFormat('s16le')
|
|
334
|
+
.audioChannels(numChannels)
|
|
335
|
+
.audioFrequency(sampleRate)
|
|
219
336
|
.audioCodec('libmp3lame')
|
|
220
337
|
.format('mp3')
|
|
221
338
|
.output(outputFile)
|
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.2.6",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"filters",
|
|
21
21
|
"mp3",
|
|
22
22
|
"wav",
|
|
23
|
+
"ogg",
|
|
24
|
+
"opus",
|
|
23
25
|
"echo",
|
|
24
26
|
"delay",
|
|
25
27
|
"tremolo",
|