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/cli.mjs ADDED
@@ -0,0 +1,657 @@
1
+ /*
2
+ Copyright (C) 2025 unixatch
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with spessoplayer. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { join, basename, parse } from "path"
19
+ import { _dirname_, log } from "./utils.mjs"
20
+
21
+ const regexes = {
22
+ help: /^(?:--help|\/help|-h|\/h|\/\?)$/,
23
+ version: /^(?:--version|\/version|-V|\/V)$/,
24
+ uninstall: /^(?:--uninstall|\/uninstall|-u|\/u)$/,
25
+
26
+ verboseLevel: new RegExp([
27
+ "^(?:--verbose(?:=(?<number>\\d))*", // --verbose[=n]
28
+ "|\\/verbose(?:=(?<number>\\d))*", // /verbose[=n]
29
+ "|-v(?:=(?<number>\\d))*", // -v[=n]
30
+ "|\\/v(?:=(?<number>\\d))*)$" // /v[=n]
31
+ ].join("")),
32
+
33
+ logFile: new RegExp([
34
+ "^(?:--log-file(?:=(?<path>\\w+))*", // --log-file[=n]
35
+ "|\\/log-file(?:=(?<path>\\w+))*", // /log-file[=n]
36
+ "|-lf(?:=(?<path>\\w+))*", // -lf[=n]
37
+ "|\\/lf(?:=(?<path>\\w+))*)$" // /lf[=n]
38
+ ].join("")),
39
+
40
+ stdout: /^-$/,
41
+ wav: /^.*(?:\.wav|\.wave)$/,
42
+ wavFormat: /^(?:wav|wave)$/,
43
+ flac: /^.*\.flac$/,
44
+ mp3: /^.*\.mp3$/,
45
+ raw: /^.*\.(?:s16le|s32le|pcm)$/,
46
+ rawFormat: /^(?:s16le|s32le|pcm)$/,
47
+
48
+ reverbVolume: /^(?:--reverb-volume|\/reverb-volume|-rvb|\/rvb)$/,
49
+ effects: /^(?:--effects|\/effects|-e|\/e)$/,
50
+
51
+ format: /^(?:--format|\/format|-f|\/f)$/,
52
+ volume: /^(?:--volume|\/volume|-vol|\/vol)$/,
53
+ sampleRate: /^(?:--sample-rate|\/sample-rate|-r|\/r)$/,
54
+
55
+ loop: /^(?:--loop|\/loop|-l|\/l)$/,
56
+ loopStart: /^(?:--loop-start|\/loop-start|-ls|\/ls)$/,
57
+ loopEnd: /^(?:--loop-end|\/loop-end|-le|\/le)$/,
58
+
59
+ fileCheck: /^(?!-|\/)(?:\w|\W)*$/,
60
+
61
+ infinity: /^(?:Infinity|infinity)$/,
62
+ // HH:MM:SS.sss
63
+ ISOTimestamp: /[0-9]{1,2}:[0-9]{2}:[0-9]{2}(\.[0-9])*/,
64
+ areDecibels: /^(?:-|\+*)[\d.]+dB/,
65
+ decibelNumber: /^((?:-|\+*)[\d.]+)dB/,
66
+ isPercentage: /^[\d.]+%$/,
67
+ percentageNumber: /^([\d.]+)%$/
68
+ };
69
+ /**
70
+ * Sets necessary variables in global object for main.mjs
71
+ * @param {Array} args - The process.argv to analyse
72
+ */
73
+ const actUpOnPassedArgs = async (args) => {
74
+ const lastMidis = [];
75
+ let lastParam,
76
+ lastIndex,
77
+ lastSoundfont;
78
+ let newArguments = args.slice(2);
79
+ if (newArguments.length === 0) {
80
+ help()
81
+ process.exit()
82
+ }
83
+ if (newArguments.filter(i => regexes.help.test(i)).length > 0) {
84
+ help()
85
+ process.exit()
86
+ }
87
+ if (newArguments.filter(i => regexes.version.test(i)).length > 0) {
88
+ await version()
89
+ process.exit()
90
+ }
91
+ if (newArguments.filter(i => regexes.uninstall.test(i)).length > 0) {
92
+ await uninstall()
93
+ process.exit()
94
+ }
95
+
96
+ const isVerboseLevelSet = newArguments.find(i => regexes.verboseLevel.test(i));
97
+ if (isVerboseLevelSet) {
98
+ let verboseOptionNumber = isVerboseLevelSet.match(regexes.verboseLevel).groups.number;
99
+ const verboseOptionPosition = newArguments.indexOf(isVerboseLevelSet);
100
+
101
+ if (!verboseOptionNumber) verboseOptionNumber = "1";
102
+ // Delete verbose-level from newArguments
103
+ newArguments.splice(verboseOptionPosition, 1)
104
+
105
+ if (!process.env["DEBUG_LEVEL_SPESSO"]) {
106
+ await setVerboseLevel(verboseOptionNumber)
107
+ } else log(1, performance.now().toFixed(2), `Using variable DEBUG_LEVEL_SPESSO=${process.env["DEBUG_LEVEL_SPESSO"]}`)
108
+ } else if (process.env["DEBUG_LEVEL_SPESSO"]) {
109
+ log(1, performance.now().toFixed(2), `Using variable DEBUG_LEVEL_SPESSO=${process.env["DEBUG_LEVEL_SPESSO"]}`)
110
+ }
111
+ const isPathOfLogFileSet = newArguments.find(i => regexes.logFile.test(i));
112
+ if (isPathOfLogFileSet
113
+ && !isVerboseLevelSet
114
+ && !process.env["DEBUG_LEVEL_SPESSO"]) {
115
+ await setVerboseLevel("1")
116
+ }
117
+
118
+ if (isPathOfLogFileSet) {
119
+ const pathOfLogFile = isPathOfLogFileSet.match(regexes.logFile).groups.path;
120
+ const pathOfLogFilePosition = newArguments.indexOf(isPathOfLogFileSet);
121
+
122
+ // Delete verbose-level from newArguments
123
+ newArguments.splice(pathOfLogFilePosition, 1)
124
+
125
+ if (!process.env["DEBUG_FILE_SPESSO"]) {
126
+ setLogFilePath(pathOfLogFile)
127
+ } else log(1, performance.now().toFixed(2), `Using variable DEBUG_FILE_SPESSO=${process.env["DEBUG_FILE_SPESSO"]}`)
128
+ } else if (process.env["DEBUG_FILE_SPESSO"]) {
129
+ log(1, performance.now().toFixed(2), `Using variable DEBUG_FILE_SPESSO=${process.env["DEBUG_FILE_SPESSO"]}`)
130
+ }
131
+
132
+ global.fileOutputs = [];
133
+ for (const arg of newArguments) {
134
+ switch (arg) {
135
+ case regexes.wav.test(arg) && arg: {
136
+ global.fileOutputs[0] = arg;
137
+ log(1, performance.now().toFixed(2), "Set file output to wav")
138
+ break;
139
+ }
140
+ case regexes.flac.test(arg) && arg: {
141
+ global.fileOutputs[1] = arg;
142
+ log(1, performance.now().toFixed(2), "Set file output to flac")
143
+ break;
144
+ }
145
+ case regexes.mp3.test(arg) && arg: {
146
+ global.fileOutputs[2] = arg;
147
+ log(1, performance.now().toFixed(2), "Set file output to mp3")
148
+ break;
149
+ }
150
+ case regexes.raw.test(arg) && arg: {
151
+ global.fileOutputs[3] = arg;
152
+ log(1, performance.now().toFixed(2), "Set file output to pcm")
153
+ break;
154
+ }
155
+ case regexes.stdout.test(arg) && arg: {
156
+ global.toStdout = true;
157
+ break;
158
+ }
159
+ case regexes.reverbVolume.test(arg) && arg: {
160
+ // In case there's no other argument
161
+ const indexOfArg = newArguments.indexOf(arg);
162
+ if (newArguments[indexOfArg + 1] === undefined) throw new ReferenceError("Missing necessary argument");
163
+
164
+ lastParam = "reverb"
165
+ break;
166
+ }
167
+ case regexes.volume.test(arg) && arg: {
168
+ // In case there's no other argument
169
+ const indexOfArg = newArguments.indexOf(arg);
170
+ if (newArguments[indexOfArg + 1] === undefined) throw new ReferenceError("Missing necessary argument");
171
+
172
+ lastParam = "volume"
173
+ break;
174
+ }
175
+ case regexes.effects.test(arg) && arg: {
176
+ // In case there's no other argument
177
+ const indexOfArg = newArguments.indexOf(arg);
178
+ if (newArguments[indexOfArg + 1] === undefined) throw new ReferenceError("Missing necessary argument");
179
+
180
+ lastParam = "effects"
181
+ break;
182
+ }
183
+ case regexes.format.test(arg) && arg: {
184
+ // In case there's no other argument
185
+ const indexOfArg = newArguments.indexOf(arg);
186
+ if (newArguments[indexOfArg + 1] === undefined) throw new ReferenceError("Missing necessary argument");
187
+
188
+ lastParam = "format"
189
+ break;
190
+ }
191
+ case regexes.sampleRate.test(arg) && arg: {
192
+ // In case there's no other argument
193
+ const indexOfArg = newArguments.indexOf(arg);
194
+ if (newArguments[indexOfArg + 1] === undefined) throw new ReferenceError("Missing necessary argument");
195
+
196
+ lastParam = "sample-rate"
197
+ break;
198
+ }
199
+ case regexes.loopStart.test(arg) && arg: {
200
+ // In case there's no other argument
201
+ const indexOfArg = newArguments.indexOf(arg);
202
+ if (newArguments[indexOfArg + 1] === undefined) throw new ReferenceError("Missing necessary argument");
203
+
204
+ lastParam = "loop-start"
205
+ break;
206
+ }
207
+ case regexes.loopEnd.test(arg) && arg: {
208
+ // In case there's no other argument
209
+ const indexOfArg = newArguments.indexOf(arg);
210
+ if (newArguments[indexOfArg + 1] === undefined) throw new ReferenceError("Missing necessary argument");
211
+
212
+ lastParam = "loop-end"
213
+ break;
214
+ }
215
+ case regexes.loop.test(arg) && arg: {
216
+ // In case there's no other argument
217
+ const indexOfArg = newArguments.indexOf(arg);
218
+ if (newArguments[indexOfArg + 1] === undefined) throw new ReferenceError("Missing necessary argument");
219
+
220
+ lastParam = "loop"
221
+ break;
222
+ }
223
+ case regexes.fileCheck.test(basename(arg)) && arg: {
224
+ if (lastParam === undefined) {
225
+ const fs = await import("node:fs");
226
+ global["fs"] = fs;
227
+ let fileMagicNumber;
228
+ await new Promise((resolve, reject) => {
229
+ const readStream = fs.createReadStream(arg, { start: 0, end: 20 });
230
+ readStream.on("data", (data) => {
231
+ fileMagicNumber = data.toString();
232
+ resolve()
233
+ })
234
+ readStream.on("error", (e) => {
235
+ if (e.code === "ENOENT") console.error(`${red}Can't open '${arg}' because it doesn't exist${normal}`)
236
+ process.exit(1)
237
+ })
238
+ })
239
+ // MIDI files
240
+ if (fileMagicNumber.includes("MThd")) {
241
+ global.midiFile = arg;
242
+ log(1, performance.now().toFixed(2), `Set midi file to "${global.midiFile}"`)
243
+ break;
244
+ }
245
+ if (fileMagicNumber.includes("sfbk")) {
246
+ // Soundfont files
247
+ global.soundfontFile = arg;
248
+ log(1, performance.now().toFixed(2), `Set soundfont file to "${global.soundfontFile}"`)
249
+ break;
250
+ }
251
+ if (fileMagicNumber.includes("DLS")) {
252
+ // Downloadable sounds files
253
+ global.soundfontFile = arg;
254
+ log(1, performance.now().toFixed(2), `Set downloadable sounds file to "${global.soundfontFile}"`)
255
+ break;
256
+ }
257
+ }
258
+ }
259
+
260
+ default:
261
+ switch (lastParam) {
262
+ case "loop":
263
+ setLoop(arg)
264
+ lastParam = undefined;
265
+ break;
266
+ case "loop-start":
267
+ setLoopStart(arg)
268
+ lastParam = undefined;
269
+ break;
270
+ case "loop-end":
271
+ setLoopEnd(arg)
272
+ lastParam = undefined;
273
+ break;
274
+ case "sample-rate":
275
+ setSampleRate(arg)
276
+ lastParam = undefined;
277
+ break;
278
+ case "format":
279
+ setFormat(arg)
280
+ lastParam = undefined;
281
+ break;
282
+ case "volume":
283
+ setVolume(arg)
284
+ lastParam = undefined;
285
+ break;
286
+ case "reverb":
287
+ setReverb(arg)
288
+ lastParam = undefined;
289
+ break;
290
+ case "effects":
291
+ setEffects(arg)
292
+ lastParam = undefined;
293
+ break;
294
+
295
+ default:
296
+ // Invalid param
297
+ console.error(red+`'${
298
+ underline+dimRed +
299
+ arg +
300
+ normal+red
301
+ }' is an invalid parameter`+normal)
302
+ help()
303
+ process.exit()
304
+ }
305
+ }
306
+ }
307
+ if (global?.midiFile === undefined) {
308
+ console.error(`${red}Missing a required midi file${normal}`);
309
+ process.exit(1)
310
+ }
311
+ if (global?.soundfontFile === undefined) {
312
+ console.error(`${red}Missing a required soundfont file${normal}`);
313
+ process.exit(1)
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Sets the global.loopN variable
319
+ * @param {String} arg - the loop amount
320
+ */
321
+ const setLoop = arg => {
322
+ if (typeof Number(arg) === "number"
323
+ && !regexes.infinity.test(arg)) {
324
+ global.loopN = Number(arg);
325
+ log(1, performance.now().toFixed(2), `Set loop amount to ${global.loopN}`)
326
+ return;
327
+ }
328
+ if (regexes.infinity.test(arg)) {
329
+ console.error(`${normalRed}Can't use infinity, sorry${normal}`)
330
+ process.exit(1);
331
+ }
332
+ console.error(`${normalRed}Passed something that wasn't a number${normal}`)
333
+ process.exit(1);
334
+ }
335
+ /**
336
+ * Sets the global.loopStart variable
337
+ * @param {String} arg - the start of the loop in seconds or in HH:MM:SS:ms format
338
+ */
339
+ const setLoopStart = arg => {
340
+ if (typeof Number(arg) === "number"
341
+ || !Number.isNaN(Date.parse(`1970T${arg}Z`))) {
342
+ if (regexes.ISOTimestamp.test(arg)) {
343
+ const seconds = Date.parse(`1970T${arg}Z`) / 1000;
344
+ global.loopStart = seconds;
345
+ log(1, performance.now().toFixed(2), `Set loop-start to ${global.loopStart}`)
346
+ return;
347
+ }
348
+ log(1, performance.now().toFixed(2), `Set loop-start to ${global.loopStart}`)
349
+ global.loopStart = Number(arg);
350
+ return;
351
+ }
352
+ console.error(`${normalRed}Passed something that wasn't a number or in ISO string format${normal}`)
353
+ process.exit(1);
354
+ }
355
+ /**
356
+ * Sets the global.loopEnd variable
357
+ * @param {String} arg - the end of the loop in seconds or in HH:MM:SS:ms format
358
+ */
359
+ const setLoopEnd = arg => {
360
+ if (typeof Number(arg) === "number"
361
+ || !Number.isNaN(Date.parse(`1970T${arg}Z`))) {
362
+ if (regexes.ISOTimestamp.test(arg)) {
363
+ const seconds = Date.parse(`1970T${arg}Z`) / 1000;
364
+ global.loopEnd = seconds;
365
+ log(1, performance.now().toFixed(2), `Set loop-end to ${global.loopEnd}`)
366
+ return;
367
+ }
368
+ log(1, performance.now().toFixed(2), `Set loop-end to ${global.loopEnd}`)
369
+ global.loopEnd = Number(arg);
370
+ return;
371
+ }
372
+ console.error(`${normalRed}Passed something that wasn't a number or in ISO string format${normal}`)
373
+ process.exit(1);
374
+ }
375
+ /**
376
+ * Sets the global.sampleRate variable
377
+ * @param {String} arg - the sample rate to set
378
+ */
379
+ const setSampleRate = arg => {
380
+ if (typeof Number(arg) === "number" && !arg.startsWith("-")) {
381
+ log(1, performance.now().toFixed(2), `Set sample rate to ${global.sampleRate}`)
382
+ global.sampleRate = Number(arg);
383
+ return;
384
+ }
385
+ console.error(`${normalRed}Passed something that wasn't a valid number${normal}`)
386
+ process.exit(1);
387
+ }
388
+ /**
389
+ * Simply changes how the program should log
390
+ * @param {Number} arg - the level of how much it should log
391
+ */
392
+ const setVerboseLevel = async (arg) => {
393
+ const isFromUser = arg !== undefined;
394
+ if (!arg) arg = "2";
395
+ if (!global.fs) global.fs = await import("fs");
396
+ if (typeof Number(arg) === "number"
397
+ && !(Number(arg) < 0 && Number(arg) > 2)
398
+ && !arg.startsWith("-")) {
399
+ global.verboseLevel = Number(arg);
400
+ if (isFromUser) {
401
+ log(1, performance.now().toFixed(2), `Set verbose level asked by the user to ${global.verboseLevel}`)
402
+ } else log(1, performance.now().toFixed(2), `Set verbose level to ${global.verboseLevel}`)
403
+ return;
404
+ }
405
+ console.error(`${normalRed}Passed something that wasn't a valid number${normal}`)
406
+ process.exit(1);
407
+ }
408
+ /**
409
+ * Sets the global.format variable for use in stdout mode
410
+ * @param {String} arg - the format to use (similar to ffmpeg's -f)
411
+ */
412
+ const setFormat = arg => {
413
+ switch (arg) {
414
+ case regexes.wavFormat.test(arg) && arg: {
415
+ global.format = "wave";
416
+ log(1, performance.now().toFixed(2), `Set stdout format to ${global.format}`)
417
+ return;
418
+ }
419
+ case "flac": {
420
+ global.format = "flac";
421
+ log(1, performance.now().toFixed(2), `Set stdout format to ${global.format}`)
422
+ return;
423
+ }
424
+ case "mp3": {
425
+ global.format = "mp3";
426
+ log(1, performance.now().toFixed(2), `Set stdout format to ${global.format}`)
427
+ return;
428
+ }
429
+ case regexes.rawFormat.test(arg) && arg: {
430
+ global.format = "pcm";
431
+ log(1, performance.now().toFixed(2), `Set stdout format to ${global.format}`)
432
+ return;
433
+ }
434
+ }
435
+ console.error(`${normalRed}Passed something that wasn't an available format${normal}`)
436
+ process.exit(1);
437
+ }
438
+ /**
439
+ * Applies effects from the user's string passed through --effects
440
+ * @param {String} arg - the comma-separeted string to parse
441
+ */
442
+ const setEffects = arg => {
443
+ const regexListOfEffects =
444
+ "allpass|band|bandpass|bandreject|bass|bend|biquad" +
445
+ "|chorus|channels|compand|contrast|dcshift|deemph|delay" +
446
+ "|dither|divide|downsample|earwax|echo|echos|equalizer" +
447
+ "|fade|fir|firfit|flanger|gain|highpass|hilbert|input" +
448
+ "|ladspa|loudness|lowpass|mcompand|noiseprof|noisered" +
449
+ "|norm|oops|output|overdrive|pad|phaser|pitch|rate|remix" +
450
+ "|repeat|reverb|reverse|riaa|silence|sinc|spectrogram" +
451
+ "|speed|splice|stat|stats|stretch|swap|synth|tempo" +
452
+ "|treble|tremolo|trim|upsample|vad|vol";
453
+ const regexGroupListGetter = /([a-z]+) ?([-a-z\d ]+)?/gm;
454
+ const regexTests = {
455
+ // is it a list structured like
456
+ // <effect1>[values1],<effect2>[values2]?
457
+ normalList: new RegExp(`${regexGroupListGetter.source},${regexGroupListGetter.source}`).test(arg),
458
+ // is it a single effect like
459
+ // <effect>[values]?
460
+ isIncorrect: new RegExp(`^${regexGroupListGetter.source}[^,](?:${regexListOfEffects}).*$`).test(arg)
461
+ }
462
+ if (regexTests.normalList || !regexTests.isIncorrect) {
463
+ const list = [
464
+ ...arg
465
+ .matchAll(regexGroupListGetter)
466
+ .map(i => ({
467
+ effect: i[1],
468
+ values: (i[2])
469
+ ? i[2].split(
470
+ (i[2].includes(",")) ? "," : " "
471
+ )
472
+ : undefined
473
+ }) )
474
+ ];
475
+
476
+ if (!list
477
+ .every(i => new RegExp(regexListOfEffects).test(i.effect))
478
+ ) {
479
+ console.error(`${normalRed}One effect that you passed doesn't exist in SoX${normal}`);
480
+ process.exit(1);
481
+ }
482
+
483
+ global.effects = list;
484
+ log(1, performance.now().toFixed(2), `Set list of SoX effects as ${global.effects}`)
485
+ return;
486
+ }
487
+ console.error(`${normalRed}The string for SoX effects you passed is not usable${normal}`);
488
+ process.exit(1);
489
+ }
490
+ /**
491
+ * Sets the global.volume variable for the masterGain
492
+ * @param {String} arg - the volume in either percentage, decibels or decimals
493
+ */
494
+ const setVolume = arg => {
495
+ if (regexes.areDecibels.test(arg)) {
496
+ const dBNumber = Number(arg.match(regexes.decibelNumber)[1]);
497
+ const toPercentage = 10**(dBNumber/10);
498
+ global.volume = toPercentage;
499
+ log(1, performance.now().toFixed(2), `Set volume to ${global.volume}`)
500
+ return;
501
+ }
502
+ if (regexes.isPercentage.test(arg)) {
503
+ const percentage = Number(arg.match(regexes.percentageNumber)[1]);
504
+ global.volume = percentage / 100;
505
+ log(1, performance.now().toFixed(2), `Set volume to ${global.volume}`)
506
+ return;
507
+ }
508
+ if (typeof Number(arg) === "number" && !arg.startsWith("-")) {
509
+ global.volume = Number(arg);
510
+ log(1, performance.now().toFixed(2), `Set volume to ${global.volume}`)
511
+ return;
512
+ }
513
+ console.error(`${normalRed}Passed something that wasn't a valid number/dB/percentage${normal}`)
514
+ process.exit(1);
515
+ }
516
+ /**
517
+ * Sets the global.reverb variable
518
+ * @param {String} arg - the volume in either percentage, decibels or decimals
519
+ */
520
+ const setReverb = arg => {
521
+ if (regexes.areDecibels.test(arg)) {
522
+ const dBNumber = Number(arg.match(regexes.decibelNumber)[1]);
523
+ global.reverbVolume = dBNumber;
524
+ global.effects = true;
525
+ log(1, performance.now().toFixed(2), `Set reverb volume to ${global.reverbVolume} and effects variable to ${global.effects}`)
526
+ return;
527
+ }
528
+ if (regexes.isPercentage.test(arg)) {
529
+ const percentage = Number(arg.match(regexes.percentageNumber)[1]);
530
+ const toDB = 10 * 10**(percentage/100);
531
+ global.reverbVolume = toDB;
532
+ global.effects = true;
533
+ log(1, performance.now().toFixed(2), `Set reverb volume to ${global.reverbVolume} and effects variable to ${global.effects}`)
534
+ return;
535
+ }
536
+ if (typeof Number(arg) === "number" && !arg.startsWith("-")) {
537
+ global.reverbVolume = Number(arg);
538
+ global.effects = true;
539
+ log(1, performance.now().toFixed(2), `Set reverb volume to ${global.reverbVolume} and effects variable to ${global.effects}`)
540
+ return;
541
+ }
542
+ console.error(`${normalRed}Passed something that wasn't a valid number/dB/percentage${normal}`)
543
+ process.exit(1);
544
+ }
545
+ /**
546
+ * Sets the file path to the log file
547
+ * @param {String} arg - Path to the log file
548
+ */
549
+ const setLogFilePath = arg => {
550
+ global.logFilePath = arg ?? "./spesso.log";
551
+ log(1, performance.now().toFixed(2), `Set log file path to ${global.logFilePath}`)
552
+ }
553
+ /**
554
+ * Runs uninstall.mjs and uninstall spessoplayer
555
+ */
556
+ const uninstall = async () => {
557
+ const { execSync } = await import("child_process");
558
+ const uninstallScriptPath = join(_dirname_, "uninstall.mjs");
559
+ const isGloballyInstalled = /spessoplayer/.test(execSync("npm ls -g").toString());
560
+
561
+ log(1, performance.now().toFixed(2), `Launched ${uninstallScriptPath}`)
562
+ try {
563
+ execSync(`node ${uninstallScriptPath}`, {stdio: "inherit"})
564
+ } catch (e) {
565
+ if (e.status !== 0 && e.status !== 2) {
566
+ console.error(`${red}Uninstallation interrupted with error ${e.status}${normal}`);
567
+ process.exit(2);
568
+ }
569
+ if (e.status === 2) process.exit(2)
570
+ }
571
+ log(1, performance.now().toFixed(2), "Uninstalling spessoplayer")
572
+ execSync(`npm uninstall ${(isGloballyInstalled) ? "-g" : ""} spessoplayer`, { cwd: ".", stdio: "inherit" })
573
+ }
574
+ /**
575
+ * Shows the help text
576
+ */
577
+ const help = () => {
578
+ const helpText = `${underline}spessoplayer${normal}
579
+ ${dimGrayBold}A midi converter that uses spessasynth_core to generate the data${normal}
580
+
581
+ Usage:
582
+ ${bold}spessoplayer${normal} [${dimGray}options${normal}] <midi> <soundfont> [${dimGray}outFile${normal}]
583
+
584
+ Available parameters:
585
+ ${green}--volume${normal}, ${green}/volume${normal}, ${green}-vol${normal}, ${green}/vol${normal}:
586
+ ${dimGray+italics}Volume to set (default: 100%)${normal}
587
+
588
+ ${dimGray+italics}Available formats:${normal}
589
+ ${dimGray+italics}- dB (example -10dB)${normal}
590
+ ${dimGray+italics}- percentages (example 70%)${normal}
591
+ ${dimGray+italics}- decimals (example 0.9)${normal}
592
+
593
+ ${green}--reverb-volume${normal}, ${green}/reverb-volume${normal}, ${green}-rvb${normal}, ${green}/rvb${normal}:
594
+ ${dimGray+italics}Volume to set for reverb (default: none)${normal}
595
+ ${dimGray+italics}Same formats as volume${normal}
596
+
597
+ ${green}--effects${normal}, ${green}/effects${normal}, ${green}-e${normal}, ${green}/e${normal}:
598
+ ${dimGray+italics}Adds any effects that SoX provides (e.g "reverb,fade 1")${normal}
599
+
600
+ ${green}--loop${normal}, ${green}/loop${normal}, ${green}-l${normal}, ${green}/l${normal}:
601
+ ${dimGray+italics}Loop x amount of times (default: 0)${normal}
602
+ ${dimGray+italics}(It might be slow with bigger numbers)${normal}
603
+
604
+ ${green}--loop-start${normal}, ${green}/loop-start${normal}, ${green}-ls${normal}, ${green}/ls${normal}:
605
+ ${dimGray+italics}When the loop starts${normal}
606
+
607
+ ${green}--loop-end${normal}, ${green}/loop-end${normal}, ${green}-le${normal}, ${green}/le${normal}:
608
+ ${dimGray+italics}When the loop ends${normal}
609
+
610
+ ${green}--sample-rate${normal}, ${green}/sample-rate${normal}, ${green}-r${normal}, ${green}/r${normal}:
611
+ ${dimGray+italics}Sample rate to use (default: 48000)${normal}
612
+ ${dimGray+italics}(It might be slow with bigger numbers for players like mpv)${normal}
613
+ ${dimGray+italics}(Some players might downsize it to a smaller frequency)${normal}
614
+
615
+ ${green}--format${normal}, ${green}/format${normal}, ${green}-f${normal}, ${green}/f${normal}:
616
+ ${dimGray+italics}Format to use for stdout (default: wav)${normal}
617
+
618
+ ${dimGray+italics}Available formats:${normal}
619
+ ${dimGray+italics}- wav${normal}
620
+ ${dimGray+italics}- mp3${normal}
621
+ ${dimGray+italics}- flac${normal}
622
+ ${dimGray+italics}- pcm (s32le)${normal}
623
+
624
+ ${green}--verbose${normal}, ${green}/verbose${normal}, ${green}-v${normal}, ${green}/v${normal}:
625
+ ${dimGray+italics}Sets the verbosity (default: 2)${normal}
626
+
627
+ ${green}--log-file${normal}, ${green}/log-file${normal}, ${green}-lf${normal}, ${green}/lf${normal}:
628
+ ${dimGray+italics}Sets path to the log file (default: ./spesso.log)${normal}
629
+ ${dimGray+italics}(Meanwhile it writes to file, it also prints to stderr)${normal}
630
+
631
+ ${green}--uninstall${normal}, ${green}/uninstall${normal}, ${green}-u${normal}, ${green}/u${normal}:
632
+ ${dimGray+italics}Uninstalls dependencies with confirmation and the entire program${normal}
633
+
634
+ ${green}--help${normal}, ${green}/help${normal}, ${green}-h${normal}, ${green}/h${normal}, ${green}/?${normal}:
635
+ ${dimGray+italics}Shows this help message${normal}
636
+
637
+ ${green}--version${normal}, ${green}/version${normal}:
638
+ ${dimGray+italics}Shows the installed version${normal}
639
+ `
640
+ console.log(helpText)
641
+ }
642
+ /**
643
+ * Shows the version number taken from package.json
644
+ */
645
+ const version = async () => {
646
+ const fs = await import("node:fs");
647
+ const packageJSONPath = join(_dirname_, "package.json");
648
+ const { versionNumber } = JSON.parse(fs.readFileSync(packageJSONPath).toString());
649
+
650
+ log(1, performance.now().toFixed(2), `Taken version number from ${packageJSONPath}`)
651
+ console.log(`${green + versionNumber + normal}`)
652
+ }
653
+
654
+ export {
655
+ actUpOnPassedArgs,
656
+ join, parse
657
+ }