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/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)
@@ -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
+ }