spessoplayer 0.5.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/COMMAND-LINE-OPTIONS.md +59 -0
- package/LICENSE +674 -0
- package/NOTICE +8 -0
- package/README.md +29 -0
- package/audioBuffer.mjs +284 -0
- package/cli.mjs +657 -0
- package/install.mjs +81 -0
- package/main.mjs +815 -0
- package/package.json +33 -0
- package/uninstall.mjs +80 -0
- package/utils.mjs +317 -0
package/main.mjs
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
Copyright (C) 2025 unixatch
|
|
4
|
+
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU General Public License
|
|
16
|
+
along with spessoplayer. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { log } from "./utils.mjs"
|
|
20
|
+
|
|
21
|
+
addEvent({ eventType: "SIGINT" })
|
|
22
|
+
log(1, performance.now().toFixed(2), "Added SIGINT event")
|
|
23
|
+
// In case the user passes some arguments
|
|
24
|
+
const {
|
|
25
|
+
actUpOnPassedArgs,
|
|
26
|
+
} = await import("./cli.mjs");
|
|
27
|
+
log(1, performance.now().toFixed(2), "Checking passed args...")
|
|
28
|
+
await actUpOnPassedArgs(process.argv)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Simply returns an object containing ffmpeg's arguments in all supported formats
|
|
33
|
+
* @param {String} [outFile="pipe:1"] - file path to write to
|
|
34
|
+
* @return {Object} - available formats in Object format
|
|
35
|
+
*/
|
|
36
|
+
function ffmpegArgs(outFile = "pipe:1") {
|
|
37
|
+
return {
|
|
38
|
+
flac: [
|
|
39
|
+
"-i", "-",
|
|
40
|
+
"-f", "flac",
|
|
41
|
+
"-compression_level", "12",
|
|
42
|
+
outFile
|
|
43
|
+
],
|
|
44
|
+
mp3: [
|
|
45
|
+
"-i", "-",
|
|
46
|
+
"-f", "mp3",
|
|
47
|
+
"-aq", "0",
|
|
48
|
+
outFile
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
let spawn,
|
|
53
|
+
spawnSync;
|
|
54
|
+
if (global?.toStdout) {
|
|
55
|
+
await toStdout(global?.loopN, global?.volume)
|
|
56
|
+
process.exit()
|
|
57
|
+
}
|
|
58
|
+
if (global?.fileOutputs?.length > 0) await toFile(global?.loopN, global?.volume)
|
|
59
|
+
await startPlayer(global?.loopN, global?.volume)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Calculates the sample count to use
|
|
63
|
+
* @param {class} midi - The BasicMIDI class to use
|
|
64
|
+
* @param {Number} sampleRate - The sample rate to use
|
|
65
|
+
* @param {Number} loopAmount - The amount of loops to do
|
|
66
|
+
*/
|
|
67
|
+
function getSampleCount(midi, sampleRate, loopAmount) {
|
|
68
|
+
global.loopStart = global?.loopStart ?? midi.midiTicksToSeconds(midi.loop.start);
|
|
69
|
+
let loopDetectedInMidi = false;
|
|
70
|
+
if (midi.loop.start > 0) {
|
|
71
|
+
loopDetectedInMidi = true;
|
|
72
|
+
global.loopStart = midi.midiTicksToSeconds(midi.loop.start);
|
|
73
|
+
global.loopEnd = midi.midiTicksToSeconds(midi.loop.end);
|
|
74
|
+
}
|
|
75
|
+
const possibleLoopAmount = (loopAmount === 0) ? loopAmount+1 : loopAmount ?? 1;
|
|
76
|
+
let sampleCount;
|
|
77
|
+
if ((loopAmount ?? 0) === 0) {
|
|
78
|
+
sampleCount = Math.ceil(sampleRate * midi.duration);
|
|
79
|
+
} else {
|
|
80
|
+
let end;
|
|
81
|
+
if (global?.loopEnd === undefined && !loopDetectedInMidi) {
|
|
82
|
+
end = midi.duration;
|
|
83
|
+
} else if (global.loopEnd !== undefined && !loopDetectedInMidi) {
|
|
84
|
+
end = midi.duration - global.loopEnd;
|
|
85
|
+
} else end = global.loopEnd;
|
|
86
|
+
|
|
87
|
+
sampleCount = Math.ceil(
|
|
88
|
+
sampleRate *
|
|
89
|
+
(
|
|
90
|
+
midi.duration +
|
|
91
|
+
((end - global.loopStart) * possibleLoopAmount)
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
log(1, performance.now().toFixed(2), "Sample count set to " + sampleCount)
|
|
96
|
+
return {
|
|
97
|
+
loopDetectedInMidi,
|
|
98
|
+
sampleCount
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Initializes all the required variables for spessasynth_core usage
|
|
103
|
+
* @param {any} loopAmount - the loop amount
|
|
104
|
+
* @param {Number} [volume=100/100] - the volume to set
|
|
105
|
+
* @param {Boolean} [isToFile=false] - defines or not audioToWav
|
|
106
|
+
*/
|
|
107
|
+
async function initSpessaSynth(loopAmount, volume = 100/100, isToFile = false) {
|
|
108
|
+
let audioToWav,
|
|
109
|
+
BasicMIDI,
|
|
110
|
+
SoundBankLoader,
|
|
111
|
+
SpessaSynthProcessor,
|
|
112
|
+
SpessaSynthSequencer;
|
|
113
|
+
if (isToFile) {
|
|
114
|
+
({
|
|
115
|
+
audioToWav,
|
|
116
|
+
BasicMIDI,
|
|
117
|
+
SoundBankLoader,
|
|
118
|
+
SpessaSynthProcessor,
|
|
119
|
+
SpessaSynthSequencer
|
|
120
|
+
} = await import("spessasynth_core"))
|
|
121
|
+
} else {
|
|
122
|
+
({
|
|
123
|
+
BasicMIDI,
|
|
124
|
+
SoundBankLoader,
|
|
125
|
+
SpessaSynthProcessor,
|
|
126
|
+
SpessaSynthSequencer
|
|
127
|
+
} = await import("spessasynth_core"))
|
|
128
|
+
}
|
|
129
|
+
const mid = fs.readFileSync(global.midiFile);
|
|
130
|
+
const sf = fs.readFileSync(global.soundfontFile);
|
|
131
|
+
const midi = BasicMIDI.fromArrayBuffer(mid);
|
|
132
|
+
const sampleRate = global?.sampleRate ?? 48000;
|
|
133
|
+
const {
|
|
134
|
+
sampleCount,
|
|
135
|
+
loopDetectedInMidi
|
|
136
|
+
} = getSampleCount(midi, sampleRate, loopAmount);
|
|
137
|
+
|
|
138
|
+
if (global.loopStart > 0 && !loopDetectedInMidi) {
|
|
139
|
+
// ((midi.timeDivision * midi.tempoChanges[0].tempo)/60) * global.loopStart;
|
|
140
|
+
midi.loop.start = midi.secondsToMIDITicks(global.loopStart);
|
|
141
|
+
}
|
|
142
|
+
if (global?.loopEnd && global.loopEnd !== midi.duration && !loopDetectedInMidi) {
|
|
143
|
+
// (midi.duration - global.loopEnd) * (midi.tempoChanges[1].tempo/60) * midi.timeDivision;
|
|
144
|
+
midi.loop.end = midi.secondsToMIDITicks(midi.duration - global.loopEnd);
|
|
145
|
+
}
|
|
146
|
+
const synth = new SpessaSynthProcessor(sampleRate, {
|
|
147
|
+
enableEventSystem: false,
|
|
148
|
+
enableEffects: false
|
|
149
|
+
});
|
|
150
|
+
synth.setMasterParameter("masterGain", volume)
|
|
151
|
+
synth.soundBankManager.addSoundBank(
|
|
152
|
+
SoundBankLoader.fromArrayBuffer(sf),
|
|
153
|
+
"main"
|
|
154
|
+
)
|
|
155
|
+
await synth.processorInitialized
|
|
156
|
+
const seq = new SpessaSynthSequencer(synth);
|
|
157
|
+
seq.loadNewSongList([midi])
|
|
158
|
+
seq.loopCount = loopAmount ?? 0;
|
|
159
|
+
seq.play();
|
|
160
|
+
|
|
161
|
+
addEvent({ eventType: "uncaughtException" })
|
|
162
|
+
log(1, performance.now().toFixed(2), "Finished setting up SpessaSynth")
|
|
163
|
+
return {
|
|
164
|
+
audioToWav,
|
|
165
|
+
seq, synth,
|
|
166
|
+
midi,
|
|
167
|
+
sampleCount, sampleRate
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Applies effects using SoX
|
|
172
|
+
* @param {Object} obj - the object passed
|
|
173
|
+
* @param {Stream} obj.program - the process to spawn, sox usually
|
|
174
|
+
* @param {Stream} obj.stdoutHeader - the header to process
|
|
175
|
+
* @param {Stream} obj.readStream - the data to process
|
|
176
|
+
* @param {Stream} obj.stdout - the destination
|
|
177
|
+
* @param {String} obj.destination - the destination path
|
|
178
|
+
* @param {string[]} obj.effects - all effects to pass to SoX
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* applyEffects({ program: "sox", stdoutHeader, readStream })
|
|
182
|
+
*/
|
|
183
|
+
async function applyEffects({
|
|
184
|
+
program,
|
|
185
|
+
stdoutHeader, readStream,
|
|
186
|
+
promisesOfPrograms,
|
|
187
|
+
stdout = process.stdout,
|
|
188
|
+
destination = "-",
|
|
189
|
+
effects = ["reverb", (global?.reverbVolume) ? global.reverbVolume : "0", "36", "100", "100", "10", "10"]
|
|
190
|
+
}) {
|
|
191
|
+
/*
|
|
192
|
+
ffmpeg
|
|
193
|
+
-i -
|
|
194
|
+
-i parking-garage-response.wav
|
|
195
|
+
-lavfi "afir,volume=70"
|
|
196
|
+
-f wav
|
|
197
|
+
pipe:1
|
|
198
|
+
*/
|
|
199
|
+
if (!spawn) ({ spawn } = await import("child_process"));
|
|
200
|
+
// In case it's custom
|
|
201
|
+
if (effects[0]?.effect) {
|
|
202
|
+
// cloning the effects array so that it can be unpacked
|
|
203
|
+
const oldEffectsArray = [...effects];
|
|
204
|
+
effects.length = 0;
|
|
205
|
+
oldEffectsArray
|
|
206
|
+
.forEach((i) => {
|
|
207
|
+
if (i.values) {
|
|
208
|
+
effects.push(i.effect, ...i.values)
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
effects.push(i.effect)
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
const sox = spawn(program, [
|
|
215
|
+
"-t", "wav", "-",
|
|
216
|
+
"-t", "wav", destination,
|
|
217
|
+
...effects
|
|
218
|
+
], {stdio: ["pipe", stdout, "pipe"], detached: true})
|
|
219
|
+
// For SIGINT event to work, sometimes... ↑
|
|
220
|
+
log(1, performance.now().toFixed(2), "Spawned SoX with " + sox.spawnargs.join(" "))
|
|
221
|
+
|
|
222
|
+
promisesOfPrograms.push(
|
|
223
|
+
new Promise(resolve => {
|
|
224
|
+
sox.stderr.on("data", (data) => {
|
|
225
|
+
const stringOfError = data.toString();
|
|
226
|
+
// Do not print if these match stringOfError
|
|
227
|
+
if (stringOfError.match(/sox FAIL sox: \`-\' error writing output file: Connection reset by peer\n/g)
|
|
228
|
+
|| stringOfError.match(/\n*sox WARN \w*:.*can't seek.*\n*/g)) return;
|
|
229
|
+
|
|
230
|
+
const modifiedString = stringOfError
|
|
231
|
+
.replace( // Adds yellow to numbers
|
|
232
|
+
/(-*[0-9]+(?:ms|dB|%|q)*)/g,
|
|
233
|
+
`${normalYellow}$1${normal}`
|
|
234
|
+
)
|
|
235
|
+
.replace( // Adds bold gray to the default parameters that can be overriden
|
|
236
|
+
/(\] |\) )(\[)([\w-]*)/g,
|
|
237
|
+
`$1$2${dimGrayBold}$3${normal}`
|
|
238
|
+
)
|
|
239
|
+
.replace( // Adds green to the parameter that has wrong values
|
|
240
|
+
/(parameter )(`\w*')/g,
|
|
241
|
+
`$1${green}$2${normal}`
|
|
242
|
+
)
|
|
243
|
+
.replace( // Adds red to the sox FAIL... text
|
|
244
|
+
/(sox FAIL \w*)/g,
|
|
245
|
+
`${red}$1${normal}`
|
|
246
|
+
)
|
|
247
|
+
.replace( // Adds yellow and a new line to the warn text for programs like mpv
|
|
248
|
+
/(sox WARN \w*)/g,
|
|
249
|
+
`\n${yellow}$1${normal}`
|
|
250
|
+
)
|
|
251
|
+
.replace( // Adds gray to the optional parameters for the effects
|
|
252
|
+
/(\[[ \w|-]*\])/g,
|
|
253
|
+
`${gray}$1${normal}`
|
|
254
|
+
)
|
|
255
|
+
// Patch for the regex above
|
|
256
|
+
.replace(/m\[0m/g, "\x1b[0m");
|
|
257
|
+
|
|
258
|
+
console.error(modifiedString);
|
|
259
|
+
})
|
|
260
|
+
sox.on("exit", () => resolve())
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
sox.stdin.write(stdoutHeader)
|
|
264
|
+
readStream.pipe(sox.stdin)
|
|
265
|
+
log(1, performance.now().toFixed(2), "Finished setting up SoX")
|
|
266
|
+
return promisesOfPrograms;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Adds events to process
|
|
270
|
+
* @param {Object} obj - the object passed
|
|
271
|
+
* @param {String} obj.eventType - the type of event to add
|
|
272
|
+
* @param {Function} obj.func - optional function for eventType "exit"
|
|
273
|
+
* @example
|
|
274
|
+
* addEvent({ eventType: "SIGINT" })
|
|
275
|
+
*/
|
|
276
|
+
function addEvent({ eventType, func }) {
|
|
277
|
+
if (eventType === "uncaughtException") {
|
|
278
|
+
// Adds on top of spessasynth_core's uncaughtException
|
|
279
|
+
const oldUncaughtException = process.rawListeners("uncaughtException")[0];
|
|
280
|
+
process.removeListener("uncaughtException", oldUncaughtException)
|
|
281
|
+
process.on("uncaughtException", async (error) => {
|
|
282
|
+
if (global.SIGINT) return process.exit();
|
|
283
|
+
if (error?.code === "EPIPE") {
|
|
284
|
+
// Needed so that SoX can show its stderr
|
|
285
|
+
await new Promise(resolve => {
|
|
286
|
+
setTimeout(() => resolve(), 4);
|
|
287
|
+
})
|
|
288
|
+
console.error(`${gray}Closed the program before finishing to render${normal}`);
|
|
289
|
+
return process.exit(2);
|
|
290
|
+
}
|
|
291
|
+
oldUncaughtException(error)
|
|
292
|
+
})
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (eventType === "exit") {
|
|
296
|
+
process.on("exit", func)
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
if (eventType === "SIGINT") {
|
|
300
|
+
process.on("SIGINT", () => {
|
|
301
|
+
console.error(`${gray}Closed with Ctrl+c${normal}`);
|
|
302
|
+
global.SIGINT = true;
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Creates a Readable stream given the variables needed
|
|
308
|
+
* @param {Readable} Readable - Readable stream function
|
|
309
|
+
* @param {Boolean} [isStdout=false] - if it's for toStdout or not
|
|
310
|
+
* @param {Object} obj - the object passed
|
|
311
|
+
* @param {Number} obj.BUFFER_SIZE - static size of the buffer
|
|
312
|
+
* @param {Number} obj.filledSamples - how many samples have been rendered
|
|
313
|
+
* @param {Boolean} obj.lastBytes - check if it's the last sample
|
|
314
|
+
* @param {Number} obj.sampleCount - sample count
|
|
315
|
+
* @param {Number} obj.sampleRate - sample rate
|
|
316
|
+
* @param {Number} obj.i - counter for the progress
|
|
317
|
+
* @param {Number} obj.durationRounded - duration of the song rounded by percentage
|
|
318
|
+
* @param {Function} obj.clearLastLines - util function to clear lines, see utils.mjs
|
|
319
|
+
* @param {class} obj.seq - spessasynth_core' sequencer
|
|
320
|
+
* @param {class} obj.synth - spessasynth_core's processor
|
|
321
|
+
* @param {Function} obj.getData - translator: Float32Arrays → Uint8Arrays
|
|
322
|
+
*/
|
|
323
|
+
function createReadable(Readable, isStdout = false, {
|
|
324
|
+
BUFFER_SIZE, filledSamples,
|
|
325
|
+
lastBytes,
|
|
326
|
+
sampleCount, sampleRate,
|
|
327
|
+
i, durationRounded,
|
|
328
|
+
clearLastLines,
|
|
329
|
+
seq, synth,
|
|
330
|
+
getData
|
|
331
|
+
}) {
|
|
332
|
+
const readStream = new Readable({
|
|
333
|
+
read() {
|
|
334
|
+
const bufferSize = Math.min(BUFFER_SIZE, sampleCount - filledSamples);
|
|
335
|
+
const left = new Float32Array(bufferSize);
|
|
336
|
+
const right = new Float32Array(bufferSize);
|
|
337
|
+
const arr = [left, right]
|
|
338
|
+
seq.processTick();
|
|
339
|
+
synth.renderAudio(
|
|
340
|
+
arr, [], [],
|
|
341
|
+
0,
|
|
342
|
+
bufferSize
|
|
343
|
+
);
|
|
344
|
+
filledSamples += bufferSize;
|
|
345
|
+
if (!isStdout) {
|
|
346
|
+
i++;
|
|
347
|
+
if (i % 100 === 0) {
|
|
348
|
+
if (i > 0) clearLastLines([0, -1])
|
|
349
|
+
console.info(
|
|
350
|
+
`Rendered ${magenta}` +
|
|
351
|
+
// Gets the ISO format and then gets mm:ss.sss
|
|
352
|
+
new Date(
|
|
353
|
+
(Math.floor(seq.currentTime * 100) / 100) * 1000
|
|
354
|
+
)
|
|
355
|
+
.toISOString()
|
|
356
|
+
.replace(/.*T...(.*)Z/, "$1") +
|
|
357
|
+
`${normal}`,
|
|
358
|
+
"/",
|
|
359
|
+
`${brightMagenta}` +
|
|
360
|
+
// Same down here
|
|
361
|
+
new Date(durationRounded * 1000)
|
|
362
|
+
.toISOString()
|
|
363
|
+
.replace(/.*T...(.*)Z/, "$1"),
|
|
364
|
+
`${normal}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (filledSamples <= sampleCount && !lastBytes) {
|
|
370
|
+
if (filledSamples === sampleCount) lastBytes = true;
|
|
371
|
+
const data = getData(arr, sampleRate);
|
|
372
|
+
return this.push(data)
|
|
373
|
+
}
|
|
374
|
+
this.push(null)
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
log(1, performance.now().toFixed(2), `Created Readable for ${(isStdout) ? "toStdout" : "toFile"}`)
|
|
378
|
+
return readStream;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Reads the generated samples from spessasynth_core
|
|
382
|
+
* and spits them out to stdout
|
|
383
|
+
* @param {Number} loopAmount - the number of loops to do
|
|
384
|
+
* @param {Number} volume - the volume of the song
|
|
385
|
+
* @param {Number} [obj=false] - object for additional options
|
|
386
|
+
* @param {ChildProcess} obj.mpv - mpv's process
|
|
387
|
+
* @param {Boolean} obj.isStartPlayer - if it's from startPlayer
|
|
388
|
+
* @param {class} obj.seq - spessasynth_core's sequencer
|
|
389
|
+
* @param {class} obj.synth - spessasynth_core's processor
|
|
390
|
+
* @param {Number} obj.sampleCount - sample count of the song
|
|
391
|
+
* @param {Number} obj.sampleRate - sample rate of the song
|
|
392
|
+
*/
|
|
393
|
+
async function toStdout(
|
|
394
|
+
loopAmount,
|
|
395
|
+
volume = 100/100,
|
|
396
|
+
{
|
|
397
|
+
mpv,
|
|
398
|
+
isStartPlayer,
|
|
399
|
+
seq, synth,
|
|
400
|
+
sampleCount, sampleRate
|
|
401
|
+
} = {}
|
|
402
|
+
) {
|
|
403
|
+
if (!global?.midiFile || !global?.soundfontFile) {
|
|
404
|
+
throw new ReferenceError("Missing some required files")
|
|
405
|
+
}
|
|
406
|
+
log(1, performance.now().toFixed(2), "Started toStdout")
|
|
407
|
+
if (!isStartPlayer) {
|
|
408
|
+
({
|
|
409
|
+
seq, synth,
|
|
410
|
+
sampleCount, sampleRate
|
|
411
|
+
} = await initSpessaSynth(loopAmount, volume));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
({ spawn, spawnSync } = await import("child_process"));
|
|
415
|
+
if (!isStartPlayer) {
|
|
416
|
+
addEvent({ eventType: "exit",
|
|
417
|
+
func: () => {
|
|
418
|
+
// Necessary for programs like mpv
|
|
419
|
+
if (doneStreaming) {
|
|
420
|
+
let command,
|
|
421
|
+
commandToSend,
|
|
422
|
+
argumentsForCommand,
|
|
423
|
+
regexForCommand;
|
|
424
|
+
const arrayOfProgramsWinVersion = ["mpv.exe"];
|
|
425
|
+
const arrayOfPrograms = ["mpv"];
|
|
426
|
+
|
|
427
|
+
switch (process.platform) {
|
|
428
|
+
case "win32":
|
|
429
|
+
command = "tasklist";
|
|
430
|
+
argumentsForCommand = [];
|
|
431
|
+
regexForCommand = new RegExp(
|
|
432
|
+
`(?:${arrayOfProgramsWinVersion.join("|")})\\s*(?<pid>\\d+)`,
|
|
433
|
+
"g"
|
|
434
|
+
);
|
|
435
|
+
commandToSend = () => spawnSync("taskkill", [
|
|
436
|
+
"/PID", process.pid, "/T", "/F"
|
|
437
|
+
]);
|
|
438
|
+
break;
|
|
439
|
+
|
|
440
|
+
case "linux":
|
|
441
|
+
case "android":
|
|
442
|
+
case "darwin":
|
|
443
|
+
command = "ps";
|
|
444
|
+
argumentsForCommand = [
|
|
445
|
+
"-o", "pid,comm",
|
|
446
|
+
"-C", "node,"+arrayOfPrograms.join(",")
|
|
447
|
+
];
|
|
448
|
+
regexForCommand = new RegExp(
|
|
449
|
+
`(?<pid>\\d+) (?:${arrayOfPrograms.join("|")})`,
|
|
450
|
+
"g"
|
|
451
|
+
);
|
|
452
|
+
commandToSend = () => process.kill(process.pid, "SIGKILL");
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Get PIDs by group name ?<pid>
|
|
457
|
+
const iteratorObject = spawnSync(command, argumentsForCommand)
|
|
458
|
+
.stdout.toString()
|
|
459
|
+
.matchAll(regexForCommand)
|
|
460
|
+
.map(i => i.groups);
|
|
461
|
+
// If it matches something,
|
|
462
|
+
// check whether it's a connected pipe to the program before SIGKILLing
|
|
463
|
+
for (const foundProgram of iteratorObject) {
|
|
464
|
+
if (Number(foundProgram.pid) >= process.pid
|
|
465
|
+
&& Number(foundProgram.pid) <= process.pid+20) commandToSend()
|
|
466
|
+
if (process.platform === "win32") commandToSend()
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
log(1, performance.now().toFixed(2), "Added event exit")
|
|
473
|
+
const {
|
|
474
|
+
getWavHeader,
|
|
475
|
+
getData
|
|
476
|
+
} = await import("./audioBuffer.mjs")
|
|
477
|
+
const { Readable } = await import("node:stream");
|
|
478
|
+
|
|
479
|
+
const BUFFER_SIZE = 128;
|
|
480
|
+
let filledSamples = 0;
|
|
481
|
+
let lastBytes = false;
|
|
482
|
+
let doneStreaming = false;
|
|
483
|
+
|
|
484
|
+
const readStream = createReadable(Readable, true, {
|
|
485
|
+
BUFFER_SIZE, filledSamples,
|
|
486
|
+
lastBytes,
|
|
487
|
+
sampleCount, sampleRate,
|
|
488
|
+
seq, synth,
|
|
489
|
+
getData
|
|
490
|
+
});
|
|
491
|
+
const stdoutHeader = getWavHeader({ length: sampleCount, numChannels: 2 }, sampleRate);
|
|
492
|
+
log(1, performance.now().toFixed(2), "Created header file ", stdoutHeader)
|
|
493
|
+
|
|
494
|
+
const promisesOfPrograms = [];
|
|
495
|
+
switch (global?.format) {
|
|
496
|
+
case "wave": {
|
|
497
|
+
if (global?.effects) {
|
|
498
|
+
await applyEffects({
|
|
499
|
+
program: "sox",
|
|
500
|
+
stdoutHeader, readStream,
|
|
501
|
+
promisesOfPrograms,
|
|
502
|
+
stdout: (isStartPlayer) ? mpv.stdin : undefined,
|
|
503
|
+
effects: (Array.isArray(global?.effects)) ? global.effects : undefined
|
|
504
|
+
})
|
|
505
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
if (isStartPlayer) {
|
|
509
|
+
mpv.stdin.write(stdoutHeader)
|
|
510
|
+
readStream.pipe(mpv.stdin)
|
|
511
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
process.stdout.write(stdoutHeader)
|
|
515
|
+
readStream.pipe(process.stdout)
|
|
516
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "flac": {
|
|
520
|
+
const ffmpeg = spawn("ffmpeg",
|
|
521
|
+
ffmpegArgs().flac,
|
|
522
|
+
{stdio: [
|
|
523
|
+
"pipe",
|
|
524
|
+
(!isStartPlayer) ? process.stdout : mpv.stdin,
|
|
525
|
+
"pipe"
|
|
526
|
+
], detached: true}
|
|
527
|
+
);
|
|
528
|
+
log(1, performance.now().toFixed(2), "Spawned ffmpeg with " + ffmpeg.spawnargs.join(" "))
|
|
529
|
+
if (global?.effects) {
|
|
530
|
+
await applyEffects({
|
|
531
|
+
program: "sox",
|
|
532
|
+
stdoutHeader, readStream,
|
|
533
|
+
promisesOfPrograms,
|
|
534
|
+
stdout: ffmpeg.stdin,
|
|
535
|
+
effects: (Array.isArray(global?.effects)) ? global.effects : undefined
|
|
536
|
+
})
|
|
537
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
promisesOfPrograms.push(
|
|
541
|
+
new Promise((resolve, reject) => {
|
|
542
|
+
ffmpeg.on("error", e => reject(e))
|
|
543
|
+
ffmpeg.on("exit", () => resolve())
|
|
544
|
+
})
|
|
545
|
+
)
|
|
546
|
+
log(1, performance.now().toFixed(2), "Added promise")
|
|
547
|
+
ffmpeg.stdin.write(stdoutHeader)
|
|
548
|
+
readStream.pipe(ffmpeg.stdin)
|
|
549
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
case "mp3": {
|
|
553
|
+
const ffmpeg = spawn("ffmpeg",
|
|
554
|
+
ffmpegArgs().mp3,
|
|
555
|
+
{stdio: [
|
|
556
|
+
"pipe",
|
|
557
|
+
(!isStartPlayer) ? process.stdout : mpv.stdin,
|
|
558
|
+
"pipe"
|
|
559
|
+
], detached: true}
|
|
560
|
+
);
|
|
561
|
+
log(1, performance.now().toFixed(2), "Spawned ffmpeg with " + ffmpeg.spawnargs.join(" "))
|
|
562
|
+
if (global?.effects) {
|
|
563
|
+
await applyEffects({
|
|
564
|
+
program: "sox",
|
|
565
|
+
stdoutHeader, readStream,
|
|
566
|
+
promisesOfPrograms,
|
|
567
|
+
stdout: ffmpeg.stdin,
|
|
568
|
+
effects: (Array.isArray(global?.effects)) ? global.effects : undefined
|
|
569
|
+
})
|
|
570
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
promisesOfPrograms.push(
|
|
574
|
+
new Promise((resolve, reject) => {
|
|
575
|
+
ffmpeg.on("error", e => reject(e))
|
|
576
|
+
ffmpeg.on("exit", () => resolve())
|
|
577
|
+
})
|
|
578
|
+
)
|
|
579
|
+
log(1, performance.now().toFixed(2), "Added promise")
|
|
580
|
+
ffmpeg.stdin.write(stdoutHeader)
|
|
581
|
+
readStream.pipe(ffmpeg.stdin)
|
|
582
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case "pcm": {
|
|
586
|
+
readStream.pipe((!isStartPlayer) ? process.stdout : mpv.stdin)
|
|
587
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
default:
|
|
592
|
+
if (global?.effects) {
|
|
593
|
+
await applyEffects({
|
|
594
|
+
program: "sox",
|
|
595
|
+
stdoutHeader, readStream,
|
|
596
|
+
promisesOfPrograms,
|
|
597
|
+
stdout: (isStartPlayer) ? mpv.stdin : undefined,
|
|
598
|
+
effects: (Array.isArray(global?.effects)) ? global.effects : undefined
|
|
599
|
+
})
|
|
600
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
if (isStartPlayer) {
|
|
604
|
+
mpv.stdin.write(stdoutHeader)
|
|
605
|
+
readStream.pipe(mpv.stdin)
|
|
606
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
process.stdout.write(stdoutHeader)
|
|
610
|
+
readStream.pipe(process.stdout)
|
|
611
|
+
log(1, performance.now().toFixed(2), "Done setting up")
|
|
612
|
+
}
|
|
613
|
+
await Promise.all([
|
|
614
|
+
new Promise((resolve, reject) => {
|
|
615
|
+
readStream.on("error", e => reject(e))
|
|
616
|
+
readStream.on("end", () => {
|
|
617
|
+
doneStreaming = true;
|
|
618
|
+
resolve()
|
|
619
|
+
})
|
|
620
|
+
}),
|
|
621
|
+
(isStartPlayer) ? new Promise((resolve, reject) => {
|
|
622
|
+
mpv.on("error", e => reject(e))
|
|
623
|
+
mpv.on("exit", () => resolve())
|
|
624
|
+
mpv.on("end", () => resolve())
|
|
625
|
+
}) : undefined,
|
|
626
|
+
...promisesOfPrograms // If there are any
|
|
627
|
+
])
|
|
628
|
+
log(1, performance.now().toFixed(2), (!isStartPlayer) ? "Finished printing to stdout" : "Finished sending data to mpv's process")
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Reads the generated samples from spessasynth_core
|
|
633
|
+
* and renders them to a wav file
|
|
634
|
+
* @param {Number} loopAmount - the number of loops to do
|
|
635
|
+
* @param {Number} volume - the volume of the song
|
|
636
|
+
*/
|
|
637
|
+
async function toFile(loopAmount, volume = 100/100) {
|
|
638
|
+
if (!global?.midiFile || !global?.soundfontFile || global.fileOutputs.length === 0 ) {
|
|
639
|
+
throw new ReferenceError("Missing some required files")
|
|
640
|
+
}
|
|
641
|
+
log(1, performance.now().toFixed(2), "Started toFile")
|
|
642
|
+
const {
|
|
643
|
+
seq, synth,
|
|
644
|
+
sampleCount, sampleRate
|
|
645
|
+
} = await initSpessaSynth(loopAmount, volume, true);
|
|
646
|
+
|
|
647
|
+
const {
|
|
648
|
+
getWavHeader,
|
|
649
|
+
getData
|
|
650
|
+
} = await import("./audioBuffer.mjs");
|
|
651
|
+
const { Readable } = await import("node:stream");
|
|
652
|
+
const { clearLastLines } = await import("./utils.mjs");
|
|
653
|
+
|
|
654
|
+
let i = 0;
|
|
655
|
+
const durationRounded = Math.floor(seq.midiData.duration * 100) / 100;
|
|
656
|
+
|
|
657
|
+
const BUFFER_SIZE = 128;
|
|
658
|
+
let filledSamples = 0;
|
|
659
|
+
let lastBytes = false;
|
|
660
|
+
const stdoutHeader = getWavHeader({ length: sampleCount, numChannels: 2 }, sampleRate);
|
|
661
|
+
log(1, performance.now().toFixed(2), "Created header file ", stdoutHeader)
|
|
662
|
+
|
|
663
|
+
const readStream = createReadable(Readable, false, {
|
|
664
|
+
BUFFER_SIZE, filledSamples,
|
|
665
|
+
lastBytes,
|
|
666
|
+
sampleCount, sampleRate,
|
|
667
|
+
seq, synth,
|
|
668
|
+
getData, i, durationRounded,
|
|
669
|
+
clearLastLines
|
|
670
|
+
});
|
|
671
|
+
const { newFileName } = await import("./utils.mjs");
|
|
672
|
+
const promisesOfPrograms = [];
|
|
673
|
+
for (let outFile of global.fileOutputs) {
|
|
674
|
+
switch (true) {
|
|
675
|
+
case /^.*(?:\.wav|\.wave)$/.test(outFile): {
|
|
676
|
+
const newName = newFileName(outFile);
|
|
677
|
+
global.fileOutputs[global.fileOutputs.indexOf(outFile)] = newName;
|
|
678
|
+
outFile = newName;
|
|
679
|
+
|
|
680
|
+
if (global?.effects) {
|
|
681
|
+
await applyEffects({
|
|
682
|
+
program: "sox",
|
|
683
|
+
stdoutHeader, readStream,
|
|
684
|
+
promisesOfPrograms,
|
|
685
|
+
destination: outFile,
|
|
686
|
+
effects: (Array.isArray(global?.effects)) ? global.effects : undefined
|
|
687
|
+
})
|
|
688
|
+
log(1, performance.now().toFixed(2), "Done setting up wav outFile")
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
const wav = fs.createWriteStream(outFile);
|
|
692
|
+
wav.write(stdoutHeader)
|
|
693
|
+
readStream.pipe(wav)
|
|
694
|
+
log(1, performance.now().toFixed(2), "Done setting up wav outFile")
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case /^.*\.flac$/.test(outFile): {
|
|
698
|
+
if (!spawn) ({ spawn } = await import("child_process"));
|
|
699
|
+
const newName = newFileName(outFile);
|
|
700
|
+
global.fileOutputs[global.fileOutputs.indexOf(outFile)] = newName;
|
|
701
|
+
outFile = newFileName(outFile);
|
|
702
|
+
|
|
703
|
+
const ffmpeg = spawn("ffmpeg", ffmpegArgs(outFile).flac);
|
|
704
|
+
log(1, performance.now().toFixed(2), "Spawned ffmpeg with " + ffmpeg.spawnargs.join(" "))
|
|
705
|
+
if (global?.effects) {
|
|
706
|
+
await applyEffects({
|
|
707
|
+
program: "sox",
|
|
708
|
+
stdoutHeader, readStream,
|
|
709
|
+
promisesOfPrograms,
|
|
710
|
+
stdout: ffmpeg.stdin,
|
|
711
|
+
effects: (Array.isArray(global?.effects)) ? global.effects : undefined
|
|
712
|
+
})
|
|
713
|
+
log(1, performance.now().toFixed(2), "Done setting up flac outFile")
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
promisesOfPrograms.push(
|
|
717
|
+
new Promise((resolve, reject) => {
|
|
718
|
+
ffmpeg.on("error", e => reject(e))
|
|
719
|
+
ffmpeg.on("exit", () => resolve())
|
|
720
|
+
})
|
|
721
|
+
)
|
|
722
|
+
log(1, performance.now().toFixed(2), "Added promise")
|
|
723
|
+
ffmpeg.stdin.write(stdoutHeader)
|
|
724
|
+
readStream.pipe(ffmpeg.stdin)
|
|
725
|
+
log(1, performance.now().toFixed(2), "Done setting up flac outFile")
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
case /^.*\.mp3$/.test(outFile): {
|
|
729
|
+
if (!spawn) ({ spawn } = await import("child_process"));
|
|
730
|
+
const newName = newFileName(outFile);
|
|
731
|
+
global.fileOutputs[global.fileOutputs.indexOf(outFile)] = newName;
|
|
732
|
+
outFile = newFileName(outFile);
|
|
733
|
+
|
|
734
|
+
const ffmpeg = spawn("ffmpeg", ffmpegArgs(outFile).mp3);
|
|
735
|
+
log(1, performance.now().toFixed(2), "Spawned ffmpeg with " + ffmpeg.spawnargs.join(" "))
|
|
736
|
+
if (global?.effects) {
|
|
737
|
+
await applyEffects({
|
|
738
|
+
program: "sox",
|
|
739
|
+
stdoutHeader, readStream,
|
|
740
|
+
promisesOfPrograms,
|
|
741
|
+
stdout: ffmpeg.stdin,
|
|
742
|
+
effects: (Array.isArray(global?.effects)) ? global.effects : undefined
|
|
743
|
+
})
|
|
744
|
+
log(1, performance.now().toFixed(2), "Done setting up mp3 outFile")
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
promisesOfPrograms.push(
|
|
748
|
+
new Promise((resolve, reject) => {
|
|
749
|
+
ffmpeg.on("error", e => reject(e))
|
|
750
|
+
ffmpeg.on("exit", () => resolve())
|
|
751
|
+
})
|
|
752
|
+
)
|
|
753
|
+
log(1, performance.now().toFixed(2), "Added promise")
|
|
754
|
+
ffmpeg.stdin.write(stdoutHeader)
|
|
755
|
+
readStream.pipe(ffmpeg.stdin)
|
|
756
|
+
log(1, performance.now().toFixed(2), "Done setting up mp3 outFile")
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
case /^.*\.(?:s16le|s32le|pcm)$/.test(outFile): {
|
|
760
|
+
const newName = newFileName(outFile);
|
|
761
|
+
global.fileOutputs[global.fileOutputs.indexOf(outFile)] = newName;
|
|
762
|
+
outFile = newFileName(outFile);
|
|
763
|
+
|
|
764
|
+
const pcm = fs.createWriteStream(outFile);
|
|
765
|
+
readStream.pipe(pcm)
|
|
766
|
+
log(1, performance.now().toFixed(2), "Done setting up pcm outFile")
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
await Promise.all([
|
|
772
|
+
new Promise((resolve, reject) => {
|
|
773
|
+
readStream.on("error", e => reject(e))
|
|
774
|
+
readStream.on("end", () => resolve())
|
|
775
|
+
}),
|
|
776
|
+
...promisesOfPrograms // if there are any
|
|
777
|
+
])
|
|
778
|
+
console.log("Written", global.fileOutputs.filter(ifil => ifil));
|
|
779
|
+
// Required because some child_processes sometimes blocks node from exiting
|
|
780
|
+
process.exit()
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Reads the generated samples from spessasynth_core
|
|
785
|
+
* and plays them using mpv
|
|
786
|
+
* @param {Number} loopAmount - the number of loops to do
|
|
787
|
+
* @param {Number} volume - the volume of the song
|
|
788
|
+
*/
|
|
789
|
+
async function startPlayer(loopAmount, volume = 100/100) {
|
|
790
|
+
({ spawn, spawnSync } = await import("child_process"));
|
|
791
|
+
const {
|
|
792
|
+
seq, synth,
|
|
793
|
+
sampleCount, sampleRate
|
|
794
|
+
} = await initSpessaSynth(loopAmount, volume);
|
|
795
|
+
const isRawAudio = (global?.format === "pcm") ? [
|
|
796
|
+
"--demuxer=rawaudio",
|
|
797
|
+
"--demuxer-rawaudio-format=s16le",
|
|
798
|
+
"--demuxer-rawaudio-rate="+sampleRate,
|
|
799
|
+
"--demuxer-rawaudio-channels=2"
|
|
800
|
+
] : "";
|
|
801
|
+
const mpv = spawn("mpv", [
|
|
802
|
+
...isRawAudio,
|
|
803
|
+
"-"
|
|
804
|
+
],
|
|
805
|
+
{stdio: ["pipe", "inherit", "inherit"]}
|
|
806
|
+
);
|
|
807
|
+
await toStdout(loopAmount, volume, {
|
|
808
|
+
mpv,
|
|
809
|
+
isStartPlayer: true,
|
|
810
|
+
seq, synth,
|
|
811
|
+
sampleCount, sampleRate
|
|
812
|
+
})
|
|
813
|
+
// Required because some child_processes sometimes blocks node from exiting
|
|
814
|
+
process.exit()
|
|
815
|
+
}
|