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/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# spessoplayer
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
This is a midi player/converter that uses the spessasynth_core package for maximum compatibility while also providing functionality
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
$ npm install --global spessoplayer
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
You'll be prompted if you want to install ffmpeg (convertion of files) and SoX (effects) using your current package manager,
|
|
14
|
+
|
|
15
|
+
it is recommended to install both (they weigh *~10.2* MB combined)
|
|
16
|
+
|
|
17
|
+
## Basic usage
|
|
18
|
+
|
|
19
|
+
For printing to stdout:
|
|
20
|
+
```bash
|
|
21
|
+
$ spessoplayer midi.mid soundfont.sf2 -
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For writing to file:
|
|
25
|
+
```bash
|
|
26
|
+
$ spessoplayer midi.mid soundfont.sf2 out.wav
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
for a more comprehensive look at all the options go to [COMMAND-LINE-OPTIONS](./COMMAND-LINE-OPTIONS.md)
|
package/audioBuffer.mjs
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (C) 2025 unixatch
|
|
3
|
+
|
|
4
|
+
it under the terms of the GNU General Public License as published by
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
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 {
|
|
19
|
+
IndexedByteArray,
|
|
20
|
+
DEFAULT_WAV_WRITE_OPTIONS
|
|
21
|
+
} from "spessasynth_core"
|
|
22
|
+
// This section is identical to what's inside spessasynth_core but not being exported, that's why it's here
|
|
23
|
+
function fillWithDefaults(obj, defObj) {
|
|
24
|
+
return {
|
|
25
|
+
...defObj,
|
|
26
|
+
...obj ?? {}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function writeBinaryStringIndexed(outArray, string, padLength = 0) {
|
|
30
|
+
if (padLength > 0) {
|
|
31
|
+
if (string.length > padLength) {
|
|
32
|
+
string = string.slice(0, padLength);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (let i = 0; i < string.length; i++) {
|
|
36
|
+
outArray[outArray.currentIndex++] = string.charCodeAt(i);
|
|
37
|
+
}
|
|
38
|
+
if (padLength > string.length) {
|
|
39
|
+
for (let i = 0; i < padLength - string.length; i++) {
|
|
40
|
+
outArray[outArray.currentIndex++] = 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return outArray;
|
|
44
|
+
}
|
|
45
|
+
function writeLittleEndianIndexed(dataArray, number, byteTarget) {
|
|
46
|
+
for (let i = 0; i < byteTarget; i++) {
|
|
47
|
+
dataArray[dataArray.currentIndex++] = number >> i * 8 & 255;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeRIFFChunkParts(header, chunks, isList = false) {
|
|
51
|
+
let dataOffset = 8;
|
|
52
|
+
let headerWritten = header;
|
|
53
|
+
const dataLength = chunks.reduce((len, c) => c.length + len, 0);
|
|
54
|
+
let writtenSize = dataLength;
|
|
55
|
+
if (isList) {
|
|
56
|
+
dataOffset += 4;
|
|
57
|
+
writtenSize += 4;
|
|
58
|
+
headerWritten = "LIST";
|
|
59
|
+
}
|
|
60
|
+
let finalSize = dataOffset + dataLength;
|
|
61
|
+
if (finalSize % 2 !== 0) {
|
|
62
|
+
finalSize++;
|
|
63
|
+
}
|
|
64
|
+
const outArray = new IndexedByteArray(finalSize);
|
|
65
|
+
writeBinaryStringIndexed(outArray, headerWritten);
|
|
66
|
+
writeDword(outArray, writtenSize);
|
|
67
|
+
if (isList) {
|
|
68
|
+
writeBinaryStringIndexed(outArray, header);
|
|
69
|
+
}
|
|
70
|
+
chunks.forEach((c) => {
|
|
71
|
+
outArray.set(c, dataOffset);
|
|
72
|
+
dataOffset += c.length;
|
|
73
|
+
});
|
|
74
|
+
return outArray;
|
|
75
|
+
}
|
|
76
|
+
function writeRIFFChunkRaw(header, data, addZeroByte = false, isList = false) {
|
|
77
|
+
if (header.length !== 4) {
|
|
78
|
+
throw new Error(`Invalid header length: ${header}`);
|
|
79
|
+
}
|
|
80
|
+
let dataStartOffset = 8;
|
|
81
|
+
let headerWritten = header;
|
|
82
|
+
let dataLength = data.length;
|
|
83
|
+
if (addZeroByte) {
|
|
84
|
+
dataLength++;
|
|
85
|
+
}
|
|
86
|
+
let writtenSize = dataLength;
|
|
87
|
+
if (isList) {
|
|
88
|
+
dataStartOffset += 4;
|
|
89
|
+
writtenSize += 4;
|
|
90
|
+
headerWritten = "LIST";
|
|
91
|
+
}
|
|
92
|
+
let finalSize = dataStartOffset + dataLength;
|
|
93
|
+
if (finalSize % 2 !== 0) {
|
|
94
|
+
finalSize++;
|
|
95
|
+
}
|
|
96
|
+
const outArray = new IndexedByteArray(finalSize);
|
|
97
|
+
writeBinaryStringIndexed(outArray, headerWritten);
|
|
98
|
+
writeDword(outArray, writtenSize);
|
|
99
|
+
if (isList) {
|
|
100
|
+
writeBinaryStringIndexed(outArray, header);
|
|
101
|
+
}
|
|
102
|
+
outArray.set(data, dataStartOffset);
|
|
103
|
+
return outArray;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* WAV Header Generator
|
|
108
|
+
* @param {Object} audioData - An object that contains infos about the audio
|
|
109
|
+
* @param {Number} audioData.length - Audio length in samples, essentially the sample count
|
|
110
|
+
* @param {Number} audioData.numChannels - How many channels the audio has
|
|
111
|
+
* @param {Number} sampleRate - Sample rate of the audio
|
|
112
|
+
* @param {Object} options - Optional, adds loop timestamps and more
|
|
113
|
+
* @returns {Uint8Array} the wav header
|
|
114
|
+
*/
|
|
115
|
+
function getWavHeader({ length, numChannels }, sampleRate, options = DEFAULT_WAV_WRITE_OPTIONS) {
|
|
116
|
+
const bytesPerSample = 2;
|
|
117
|
+
const fullOptions = fillWithDefaults(options, DEFAULT_WAV_WRITE_OPTIONS);
|
|
118
|
+
const loop = fullOptions.loop;
|
|
119
|
+
const metadata = fullOptions.metadata;
|
|
120
|
+
let infoChunk = new IndexedByteArray(0);
|
|
121
|
+
const infoOn = Object.keys(metadata).length > 0;
|
|
122
|
+
if (infoOn) {
|
|
123
|
+
const encoder = new TextEncoder();
|
|
124
|
+
const infoChunks = [
|
|
125
|
+
writeRIFFChunkRaw(
|
|
126
|
+
"ICMT",
|
|
127
|
+
encoder.encode("Created with SpessaSynth"),
|
|
128
|
+
true
|
|
129
|
+
)
|
|
130
|
+
];
|
|
131
|
+
if (metadata.artist) {
|
|
132
|
+
infoChunks.push(
|
|
133
|
+
writeRIFFChunkRaw("IART", encoder.encode(metadata.artist), true)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (metadata.album) {
|
|
137
|
+
infoChunks.push(
|
|
138
|
+
writeRIFFChunkRaw("IPRD", encoder.encode(metadata.album), true)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (metadata.genre) {
|
|
142
|
+
infoChunks.push(
|
|
143
|
+
writeRIFFChunkRaw("IGNR", encoder.encode(metadata.genre), true)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (metadata.title) {
|
|
147
|
+
infoChunks.push(
|
|
148
|
+
writeRIFFChunkRaw("INAM", encoder.encode(metadata.title), true)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
infoChunk = writeRIFFChunkParts("INFO", infoChunks, true);
|
|
152
|
+
}
|
|
153
|
+
let cueChunk = new IndexedByteArray(0);
|
|
154
|
+
const cueOn = loop?.end !== undefined && loop?.start !== undefined;
|
|
155
|
+
if (cueOn) {
|
|
156
|
+
const loopStartSamples = Math.floor(loop.start * sampleRate);
|
|
157
|
+
const loopEndSamples = Math.floor(loop.end * sampleRate);
|
|
158
|
+
const cueStart = new IndexedByteArray(24);
|
|
159
|
+
writeLittleEndianIndexed(cueStart, 0, 4);
|
|
160
|
+
writeLittleEndianIndexed(cueStart, 0, 4);
|
|
161
|
+
writeBinaryStringIndexed(cueStart, "data");
|
|
162
|
+
writeLittleEndianIndexed(cueStart, 0, 4);
|
|
163
|
+
writeLittleEndianIndexed(cueStart, 0, 4);
|
|
164
|
+
writeLittleEndianIndexed(cueStart, loopStartSamples, 4);
|
|
165
|
+
const cueEnd = new IndexedByteArray(24);
|
|
166
|
+
writeLittleEndianIndexed(cueEnd, 1, 4);
|
|
167
|
+
writeLittleEndianIndexed(cueEnd, 0, 4);
|
|
168
|
+
writeBinaryStringIndexed(cueEnd, "data");
|
|
169
|
+
writeLittleEndianIndexed(cueEnd, 0, 4);
|
|
170
|
+
writeLittleEndianIndexed(cueEnd, 0, 4);
|
|
171
|
+
writeLittleEndianIndexed(cueEnd, loopEndSamples, 4);
|
|
172
|
+
cueChunk = writeRIFFChunkParts("cue ", [
|
|
173
|
+
new IndexedByteArray([2, 0, 0, 0]),
|
|
174
|
+
// Cue points count
|
|
175
|
+
cueStart,
|
|
176
|
+
cueEnd
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
const headerSize = 44;
|
|
180
|
+
const dataSize = length * numChannels * bytesPerSample;
|
|
181
|
+
const fileSize = headerSize + dataSize + infoChunk.length + cueChunk.length - 8;
|
|
182
|
+
const header = new Uint8Array(headerSize);
|
|
183
|
+
header.set([82, 73, 70, 70], 0);
|
|
184
|
+
header.set(
|
|
185
|
+
new Uint8Array([
|
|
186
|
+
fileSize & 255,
|
|
187
|
+
fileSize >> 8 & 255,
|
|
188
|
+
fileSize >> 16 & 255,
|
|
189
|
+
fileSize >> 24 & 255
|
|
190
|
+
]),
|
|
191
|
+
4
|
|
192
|
+
);
|
|
193
|
+
header.set([87, 65, 86, 69], 8);
|
|
194
|
+
header.set([102, 109, 116, 32], 12);
|
|
195
|
+
header.set([16, 0, 0, 0], 16);
|
|
196
|
+
header.set([1, 0], 20);
|
|
197
|
+
header.set([numChannels & 255, numChannels >> 8], 22);
|
|
198
|
+
header.set(
|
|
199
|
+
new Uint8Array([
|
|
200
|
+
sampleRate & 255,
|
|
201
|
+
sampleRate >> 8 & 255,
|
|
202
|
+
sampleRate >> 16 & 255,
|
|
203
|
+
sampleRate >> 24 & 255
|
|
204
|
+
]),
|
|
205
|
+
24
|
|
206
|
+
);
|
|
207
|
+
const byteRate = sampleRate * numChannels * bytesPerSample;
|
|
208
|
+
header.set(
|
|
209
|
+
new Uint8Array([
|
|
210
|
+
byteRate & 255,
|
|
211
|
+
byteRate >> 8 & 255,
|
|
212
|
+
byteRate >> 16 & 255,
|
|
213
|
+
byteRate >> 24 & 255
|
|
214
|
+
]),
|
|
215
|
+
28
|
|
216
|
+
);
|
|
217
|
+
header.set([numChannels * bytesPerSample, 0], 32);
|
|
218
|
+
header.set([16, 0], 34);
|
|
219
|
+
header.set([100, 97, 116, 97], 36);
|
|
220
|
+
header.set(
|
|
221
|
+
new Uint8Array([
|
|
222
|
+
dataSize & 255,
|
|
223
|
+
dataSize >> 8 & 255,
|
|
224
|
+
dataSize >> 16 & 255,
|
|
225
|
+
dataSize >> 24 & 255
|
|
226
|
+
]),
|
|
227
|
+
40
|
|
228
|
+
);
|
|
229
|
+
return header;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Translates to PCM data
|
|
234
|
+
* @param {Array} audioData - An array that contains the audio buffers
|
|
235
|
+
* @param {Number} sampleRate - Sample rate of the audio
|
|
236
|
+
* @param {Object} options - Optional, adds loop timestamps and more
|
|
237
|
+
* @returns {Uint8Array} the translated data
|
|
238
|
+
*/
|
|
239
|
+
function getData(audioData, sampleRate, options = DEFAULT_WAV_WRITE_OPTIONS) {
|
|
240
|
+
const length = audioData[0].length;
|
|
241
|
+
const numChannels = audioData.length;
|
|
242
|
+
const fullOptions = fillWithDefaults(options, DEFAULT_WAV_WRITE_OPTIONS);
|
|
243
|
+
const headerSize = 44;
|
|
244
|
+
const bytesPerSample = 2;
|
|
245
|
+
|
|
246
|
+
const dataSize = length * numChannels * bytesPerSample;
|
|
247
|
+
const fileSize = dataSize;
|
|
248
|
+
|
|
249
|
+
const Data = new Uint8Array(fileSize);
|
|
250
|
+
let offset = 0;
|
|
251
|
+
// Volume
|
|
252
|
+
let multiplier = 32767;
|
|
253
|
+
/*if (fullOptions.normalizeAudio) {
|
|
254
|
+
const numSamples = audioData[0].length;
|
|
255
|
+
let maxAbsValue = 0;
|
|
256
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
257
|
+
const data = audioData[ch];
|
|
258
|
+
|
|
259
|
+
for (let i = 0; i < numSamples; i++) {
|
|
260
|
+
const sample = Math.abs(data[i]);
|
|
261
|
+
if (sample > maxAbsValue) {
|
|
262
|
+
maxAbsValue = sample;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
multiplier = maxAbsValue > 0
|
|
267
|
+
? 32767 / maxAbsValue
|
|
268
|
+
: 1;
|
|
269
|
+
}*/
|
|
270
|
+
for (let i = 0; i < length; i++) {
|
|
271
|
+
audioData.forEach((d) => {
|
|
272
|
+
const sample = Math.min(32767, Math.max(-32768, d[i] * multiplier));
|
|
273
|
+
Data[offset++] = sample & 255;
|
|
274
|
+
Data[offset++] = sample >> 8 & 255;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return Data;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
export {
|
|
282
|
+
getWavHeader,
|
|
283
|
+
getData
|
|
284
|
+
}
|