spessasynth_lib 3.24.38 → 3.24.40

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 CHANGED
@@ -43,6 +43,8 @@ Supported formats list:
43
43
  - `.rmi` - RIFF MIDI File
44
44
  - `.rmi` - RIFF MIDI File With Embedded DLS
45
45
  - `.rmi` - [RIFF MIDI File With Embedded SF2](https://github.com/spessasus/sf2-rmidi-specification)
46
+ - `.xmf` - eXtensible Music Format
47
+ - `.mxmf` - Mobile eXtensible Music format
46
48
 
47
49
  *With [an easy way of converting between them!](https://github.com/spessasus/SpessaSynth/wiki/Converting-Between-Formats)*
48
50
 
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Arjun Barrett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ let tr;(()=>{var l=Uint8Array,T=Uint16Array,ur=Int32Array,W=new l([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),X=new l([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),wr=new l([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Y=function(r,a){for(var e=new T(31),f=0;f<31;++f)e[f]=a+=1<<r[f-1];for(var v=new ur(e[30]),f=1;f<30;++f)for(var g=e[f];g<e[f+1];++g)v[g]=g-e[f]<<5|f;return{b:e,r:v}},Z=Y(W,2),$=Z.b,cr=Z.r;$[28]=258,cr[258]=28;var j=Y(X,0),hr=j.b,Fr=j.r,_=new T(32768);for(i=0;i<32768;++i)c=(i&43690)>>1|(i&21845)<<1,c=(c&52428)>>2|(c&13107)<<2,c=(c&61680)>>4|(c&3855)<<4,_[i]=((c&65280)>>8|(c&255)<<8)>>1;var c,i,A=function(r,a,e){for(var f=r.length,v=0,g=new T(a);v<f;++v)r[v]&&++g[r[v]-1];var k=new T(a);for(v=1;v<a;++v)k[v]=k[v-1]+g[v-1]<<1;var b;if(e){b=new T(1<<a);var m=15-a;for(v=0;v<f;++v)if(r[v])for(var U=v<<4|r[v],x=a-r[v],n=k[r[v]-1]++<<x,o=n|(1<<x)-1;n<=o;++n)b[_[n]>>m]=U}else for(b=new T(f),v=0;v<f;++v)r[v]&&(b[v]=_[k[r[v]-1]++]>>15-r[v]);return b},M=new l(288);for(i=0;i<144;++i)M[i]=8;var i;for(i=144;i<256;++i)M[i]=9;var i;for(i=256;i<280;++i)M[i]=7;var i;for(i=280;i<288;++i)M[i]=8;var i,L=new l(32);for(i=0;i<32;++i)L[i]=5;var i,gr=A(M,9,1),br=A(L,5,1),q=function(r){for(var a=r[0],e=1;e<r.length;++e)r[e]>a&&(a=r[e]);return a},u=function(r,a,e){var f=a/8|0;return(r[f]|r[f+1]<<8)>>(a&7)&e},C=function(r,a){var e=a/8|0;return(r[e]|r[e+1]<<8|r[e+2]<<16)>>(a&7)},kr=function(r){return(r+7)/8|0},xr=function(r,a,e){return(a==null||a<0)&&(a=0),(e==null||e>r.length)&&(e=r.length),new l(r.subarray(a,e))},yr=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],h=function(r,a,e){var f=new Error(a||yr[r]);if(f.code=r,Error.captureStackTrace&&Error.captureStackTrace(f,h),!e)throw f;return f},Sr=function(r,a,e,f){var v=r.length,g=f?f.length:0;if(!v||a.f&&!a.l)return e||new l(0);var k=!e,b=k||a.i!=2,m=a.i;k&&(e=new l(v*3));var U=function(fr){var or=e.length;if(fr>or){var lr=new l(Math.max(or*2,fr));lr.set(e),e=lr}},x=a.f||0,n=a.p||0,o=a.b||0,S=a.l,I=a.d,z=a.m,D=a.n,G=v*8;do{if(!S){x=u(r,n,1);var H=u(r,n+1,3);if(n+=3,H)if(H==1)S=gr,I=br,z=9,D=5;else if(H==2){var N=u(r,n,31)+257,s=u(r,n+10,15)+4,d=N+u(r,n+5,31)+1;n+=14;for(var F=new l(d),P=new l(19),t=0;t<s;++t)P[wr[t]]=u(r,n+t*3,7);n+=s*3;for(var rr=q(P),Ar=(1<<rr)-1,Mr=A(P,rr,1),t=0;t<d;){var ar=Mr[u(r,n,Ar)];n+=ar&15;var w=ar>>4;if(w<16)F[t++]=w;else{var E=0,O=0;for(w==16?(O=3+u(r,n,3),n+=2,E=F[t-1]):w==17?(O=3+u(r,n,7),n+=3):w==18&&(O=11+u(r,n,127),n+=7);O--;)F[t++]=E}}var er=F.subarray(0,N),y=F.subarray(N);z=q(er),D=q(y),S=A(er,z,1),I=A(y,D,1)}else h(1);else{var w=kr(n)+4,J=r[w-4]|r[w-3]<<8,K=w+J;if(K>v){m&&h(0);break}b&&U(o+J),e.set(r.subarray(w,K),o),a.b=o+=J,a.p=n=K*8,a.f=x;continue}if(n>G){m&&h(0);break}}b&&U(o+131072);for(var Ur=(1<<z)-1,zr=(1<<D)-1,Q=n;;Q=n){var E=S[C(r,n)&Ur],p=E>>4;if(n+=E&15,n>G){m&&h(0);break}if(E||h(2),p<256)e[o++]=p;else if(p==256){Q=n,S=null;break}else{var nr=p-254;if(p>264){var t=p-257,B=W[t];nr=u(r,n,(1<<B)-1)+$[t],n+=B}var R=I[C(r,n)&zr],V=R>>4;R||h(3),n+=R&15;var y=hr[V];if(V>3){var B=X[V];y+=C(r,n)&(1<<B)-1,n+=B}if(n>G){m&&h(0);break}b&&U(o+131072);var vr=o+nr;if(o<y){var ir=g-y,Dr=Math.min(y,vr);for(ir+o<0&&h(3);o<Dr;++o)e[o]=f[ir+o]}for(;o<vr;++o)e[o]=e[o-y]}}a.l=S,a.p=Q,a.b=o,a.f=x,S&&(x=1,a.m=z,a.d=I,a.n=D)}while(!x);return o!=e.length&&k?xr(e,0,o):e.subarray(0,o)},Tr=new l(0);function mr(r,a){return Sr(r,{i:2},a&&a.out,a&&a.dictionary)}var Er=typeof TextDecoder<"u"&&new TextDecoder,pr=0;try{Er.decode(Tr,{stream:!0}),pr=1}catch{}tr=mr})();export{tr as inflateSync};
@@ -59,6 +59,7 @@ class BasicMIDI extends MIDISequenceData
59
59
  m.format = mid.format;
60
60
  m.bankOffset = mid.bankOffset;
61
61
  m.isKaraokeFile = mid.isKaraokeFile;
62
+ m.isMultiPort = mid.isMultiPort;
62
63
  m.isDLSRMIDI = mid.isDLSRMIDI;
63
64
 
64
65
  // Copying arrays
@@ -452,6 +453,7 @@ class BasicMIDI extends MIDISequenceData
452
453
  }
453
454
  else
454
455
  {
456
+ this.isMultiPort = true;
455
457
  SpessaSynthInfo(`%cMIDI Ports detected!`, consoleColors.recognized);
456
458
  }
457
459
 
@@ -44,6 +44,7 @@ export class MIDIData extends MIDISequenceData
44
44
  this.RMIDInfo = midi.RMIDInfo;
45
45
  this.bankOffset = midi.bankOffset;
46
46
  this.isKaraokeFile = midi.isKaraokeFile;
47
+ this.isMultiPort = midi.isMultiPort;
47
48
 
48
49
  // Set isEmbedded based on the presence of an embeddedSoundFont
49
50
  this.isEmbedded = midi.embeddedSoundFont !== undefined;
@@ -9,6 +9,7 @@ import { readBytesAsString } from "../utils/byte_functions/string.js";
9
9
  import { readLittleEndian } from "../utils/byte_functions/little_endian.js";
10
10
  import { RMIDINFOChunks } from "./rmidi_writer.js";
11
11
  import { BasicMIDI } from "./basic_midi.js";
12
+ import { loadXMF } from "./xmf_loader.js";
12
13
 
13
14
  /**
14
15
  * midi_loader.js
@@ -145,6 +146,11 @@ class MIDI extends BasicMIDI
145
146
  this.bankOffset = 0;
146
147
  }
147
148
  }
149
+ else if (initialString === "XMF_")
150
+ {
151
+ // XMF file
152
+ fileByteArray = loadXMF(this, binaryData);
153
+ }
148
154
  else
149
155
  {
150
156
  fileByteArray = binaryData;
@@ -212,7 +218,7 @@ class MIDI extends BasicMIDI
212
218
  }
213
219
  else
214
220
  { // noinspection PointlessBooleanExpressionJS
215
- if (!runningByte && statusByteCheck < 0x80)
221
+ if (runningByte === undefined && statusByteCheck < 0x80)
216
222
  {
217
223
  // if we don't have a running byte and the status byte isn't valid, it's an error.
218
224
  SpessaSynthGroupEnd();
@@ -144,6 +144,12 @@ class MIDISequenceData
144
144
  */
145
145
  isKaraokeFile = false;
146
146
 
147
+ /**
148
+ * Indicates if this file is a Multi-Port MIDI file.
149
+ * @type {boolean}
150
+ */
151
+ isMultiPort = false;
152
+
147
153
  /**
148
154
  * Converts ticks to time in seconds
149
155
  * @param ticks {number} time in MIDI ticks
@@ -31,7 +31,7 @@ export function writeMIDI()
31
31
  */
32
32
  let messageData;
33
33
  // determine the message
34
- if (event.messageStatusByte <= messageTypes.keySignature || event.messageStatusByte === messageTypes.sequenceSpecific)
34
+ if (event.messageStatusByte <= messageTypes.sequenceSpecific)
35
35
  {
36
36
  // this is a meta-message
37
37
  // syntax is FF<type><length><data>
@@ -0,0 +1,454 @@
1
+ import { readBytesAsString } from "../utils/byte_functions/string.js";
2
+ import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js";
3
+ import { consoleColors } from "../utils/other.js";
4
+ import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
5
+ import { readVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js";
6
+ import { RMIDINFOChunks } from "./rmidi_writer.js";
7
+ import { inflateSync } from "../externals/fflate/fflate.min.js";
8
+ import { IndexedByteArray } from "../utils/indexed_array.js";
9
+
10
+ /**
11
+ * @enum {number}
12
+ */
13
+ const metadataTypes = {
14
+ XMFFileType: 0,
15
+ nodeName: 1,
16
+ nodeIDNumber: 2,
17
+ resourceFormat: 3,
18
+ filenameOnDisk: 4,
19
+ filenameExtensionOnDisk: 5,
20
+ macOSFileTypeAndCreator: 6,
21
+ mimeType: 7,
22
+ title: 8,
23
+ copyrightNotice: 9,
24
+ comment: 10,
25
+ autoStart: 11, // Node Name of the FileNode containing the SMF image to autostart when the XMF file loads
26
+ preload: 12, // Used to preload specific SMF and DLS file images.
27
+ contentDescription: 13, // RP-42a (https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp42.pdf)
28
+ ID3Metadata: 14 // RP-47 (https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp47.pdf)
29
+ };
30
+
31
+ /**
32
+ * @enum {number}
33
+ */
34
+ const referenceTypeIds = {
35
+ inLineResource: 1,
36
+ inFileResource: 2,
37
+ inFileNode: 3,
38
+ externalFile: 4,
39
+ externalXMF: 5,
40
+ XMFFileURIandNodeID: 6
41
+ };
42
+
43
+ /**
44
+ * @enum {number}
45
+ */
46
+ const resourceFormatIDs = {
47
+ StandardMIDIFile: 0,
48
+ StandardMIDIFileType1: 1,
49
+ DLS1: 2,
50
+ DLS2: 3,
51
+ DLS22: 4,
52
+ mobileDLS: 5
53
+ };
54
+
55
+ /**
56
+ * @enum {number}
57
+ */
58
+ const formatTypeIDs = {
59
+ standard: 0,
60
+ MMA: 1,
61
+ registered: 2,
62
+ nonRegistered: 3
63
+ };
64
+
65
+
66
+ /**
67
+ * @enum {number}
68
+ */
69
+ const unpackerIDs = {
70
+ none: 0,
71
+ MMAUnpacker: 1,
72
+ registered: 2,
73
+ nonRegistered: 3
74
+ };
75
+
76
+ class XMFNode
77
+ {
78
+ /**
79
+ * @type {number}
80
+ */
81
+ length;
82
+ /**
83
+ * 0 means it's a file node
84
+ * @type {number}
85
+ */
86
+ itemCount;
87
+ /**
88
+ * @type {number}
89
+ */
90
+ metadataLength;
91
+
92
+ /**
93
+ * @type {Object<string, any>}
94
+ */
95
+ metadata = {};
96
+
97
+ /**
98
+ * @type {IndexedByteArray}
99
+ */
100
+ nodeData;
101
+
102
+ /**
103
+ * @type {XMFNode[]}
104
+ */
105
+ innerNodes = [];
106
+
107
+ packedContent = false;
108
+
109
+ nodeUnpackers = [];
110
+
111
+
112
+ /**
113
+ * @type {"StandardMIDIFile"|
114
+ * "StandardMIDIFileType1"|
115
+ * "DLS1"|
116
+ * "DLS2"|
117
+ * "DLS22"|
118
+ * "mobileDLS"|
119
+ * "unknown"|"folder"}
120
+ */
121
+ resourceFormat = "unknown";
122
+
123
+ /**
124
+ * @param binaryData {IndexedByteArray}
125
+ */
126
+ constructor(binaryData)
127
+ {
128
+ let nodeStartIndex = binaryData.currentIndex;
129
+ this.length = readVariableLengthQuantity(binaryData);
130
+ this.itemCount = readVariableLengthQuantity(binaryData);
131
+ // header length
132
+ const headerLength = readVariableLengthQuantity(binaryData);
133
+ const readBytes = binaryData.currentIndex - nodeStartIndex;
134
+
135
+ const remainingHeader = headerLength - readBytes;
136
+ const headerData = binaryData.slice(
137
+ binaryData.currentIndex,
138
+ binaryData.currentIndex + remainingHeader
139
+ );
140
+ binaryData.currentIndex += remainingHeader;
141
+
142
+ this.metadataLength = readVariableLengthQuantity(headerData);
143
+
144
+ const metadataChunk = headerData.slice(
145
+ headerData.currentIndex,
146
+ headerData.currentIndex + this.metadataLength
147
+ );
148
+ headerData.currentIndex += this.metadataLength;
149
+
150
+ /**
151
+ * @type {metadataTypes|string|number}
152
+ */
153
+ let fieldSpecifier;
154
+ let key;
155
+ while (metadataChunk.currentIndex < metadataChunk.length)
156
+ {
157
+ const firstSpecifierByte = metadataChunk[metadataChunk.currentIndex];
158
+ if (firstSpecifierByte === 0)
159
+ {
160
+ metadataChunk.currentIndex++;
161
+ fieldSpecifier = readVariableLengthQuantity(metadataChunk);
162
+ if (Object.values(metadataTypes).indexOf(fieldSpecifier) === -1)
163
+ {
164
+ SpessaSynthWarn(`Unknown field specifier: ${fieldSpecifier}`);
165
+ key = `unknown_${fieldSpecifier}`;
166
+ }
167
+ else
168
+ {
169
+ key = Object.keys(metadataTypes).find(k => metadataTypes[k] === fieldSpecifier);
170
+ }
171
+ }
172
+ else
173
+ {
174
+ // this is the length of string
175
+ const stringLength = readVariableLengthQuantity(metadataChunk);
176
+ fieldSpecifier = readBytesAsString(metadataChunk, stringLength);
177
+ key = fieldSpecifier;
178
+ }
179
+
180
+ const numberOfVersions = readVariableLengthQuantity(metadataChunk);
181
+ if (numberOfVersions === 0)
182
+ {
183
+ const dataLength = readVariableLengthQuantity(metadataChunk);
184
+ const contentsChunk = metadataChunk.slice(
185
+ metadataChunk.currentIndex,
186
+ metadataChunk.currentIndex + dataLength
187
+ );
188
+ metadataChunk.currentIndex += dataLength;
189
+ const formatID = readVariableLengthQuantity(contentsChunk);
190
+ // text only
191
+ if (formatID < 4)
192
+ {
193
+ this.metadata[key] = readBytesAsString(contentsChunk, dataLength - 1);
194
+ }
195
+ else
196
+ {
197
+ this.metadata[key] = contentsChunk.slice(contentsChunk.currentIndex);
198
+ }
199
+ }
200
+ else
201
+ {
202
+ // throw new Error ("International content is not supported.");
203
+ // Skip the number of versions
204
+ SpessaSynthWarn(`International content: ${numberOfVersions}`);
205
+ // Length in bytes
206
+ // Skip the whole thing!
207
+ metadataChunk.currentIndex += readVariableLengthQuantity(metadataChunk);
208
+ }
209
+ }
210
+
211
+ const unpackersStart = headerData.currentIndex;
212
+ const unpackersLength = readVariableLengthQuantity(headerData);
213
+ const unpackersData = headerData.slice(headerData.currentIndex, unpackersStart + unpackersLength);
214
+ headerData.currentIndex = unpackersStart + unpackersLength;
215
+ if (unpackersLength > 0)
216
+ {
217
+ this.packedContent = true;
218
+ while (unpackersData.currentIndex < unpackersLength)
219
+ {
220
+ const unpacker = {};
221
+ unpacker.id = readVariableLengthQuantity(unpackersData);
222
+ switch (unpacker.id)
223
+ {
224
+ case unpackerIDs.nonRegistered:
225
+ case unpackerIDs.registered:
226
+ SpessaSynthGroupEnd();
227
+ throw new Error(`Unsupported unpacker ID: ${unpacker.id}`);
228
+
229
+ default:
230
+ SpessaSynthGroupEnd();
231
+ throw new Error(`Unknown unpacker ID: ${unpacker.id}`);
232
+
233
+ case unpackerIDs.none:
234
+ unpacker.standardID = readVariableLengthQuantity(unpackersData);
235
+ break;
236
+
237
+ case unpackerIDs.MMAUnpacker:
238
+ let manufacturerID = unpackersData[unpackersData.currentIndex++];
239
+ // one or three byte form, depending on if the first byte is zero
240
+ if (manufacturerID === 0)
241
+ {
242
+ manufacturerID <<= 8;
243
+ manufacturerID |= unpackersData[unpackersData.currentIndex++];
244
+ manufacturerID <<= 8;
245
+ manufacturerID |= unpackersData[unpackersData.currentIndex++];
246
+ }
247
+ const manufacturerInternalID = readVariableLengthQuantity(unpackersData);
248
+ unpacker.manufacturerID = manufacturerID;
249
+ unpacker.manufacturerInternalID = manufacturerInternalID;
250
+ break;
251
+ }
252
+ unpacker.decodedSize = readVariableLengthQuantity(unpackersData);
253
+ this.nodeUnpackers.push(unpacker);
254
+ }
255
+ }
256
+ binaryData.currentIndex = nodeStartIndex + headerLength;
257
+ /**
258
+ * @type {referenceTypeIds|number}
259
+ */
260
+ this.referenceTypeID = readVariableLengthQuantity(binaryData);
261
+ this.nodeData = binaryData.slice(binaryData.currentIndex, nodeStartIndex + this.length);
262
+ binaryData.currentIndex = nodeStartIndex + this.length;
263
+ switch (this.referenceTypeID)
264
+ {
265
+ case referenceTypeIds.inLineResource:
266
+ break;
267
+
268
+ case referenceTypeIds.externalXMF:
269
+ case referenceTypeIds.inFileNode:
270
+ case referenceTypeIds.XMFFileURIandNodeID:
271
+ case referenceTypeIds.externalFile:
272
+ case referenceTypeIds.inFileResource:
273
+ SpessaSynthGroupEnd();
274
+ throw new Error(`Unsupported reference type: ${this.referenceTypeID}`);
275
+
276
+ default:
277
+ SpessaSynthGroupEnd();
278
+ throw new Error(`Unknown reference type: ${this.referenceTypeID}`);
279
+ }
280
+
281
+ // read the data
282
+ if (this.isFile)
283
+ {
284
+ if (this.packedContent)
285
+ {
286
+ const compressed = this.nodeData.slice(2, this.nodeData.length);
287
+ SpessaSynthInfo(
288
+ `%cPacked content. Attemting to deflate. Target size: %c${this.nodeUnpackers[0].decodedSize}`,
289
+ consoleColors.warn,
290
+ consoleColors.value
291
+ );
292
+ try
293
+ {
294
+ this.nodeData = new IndexedByteArray(inflateSync(compressed).buffer);
295
+ }
296
+ catch (e)
297
+ {
298
+ SpessaSynthGroupEnd();
299
+ throw new Error(`Error unpacking XMF file contents: ${e.message}.`);
300
+ }
301
+ }
302
+ /**
303
+ * interpret the content
304
+ * @type {number[]}
305
+ */
306
+ const resourceFormat = this.metadata["resourceFormat"];
307
+ if (resourceFormat === undefined)
308
+ {
309
+ SpessaSynthWarn("No resource format for this file node!");
310
+ }
311
+ else
312
+ {
313
+ const formatTypeID = resourceFormat[0];
314
+ if (formatTypeID !== formatTypeIDs.standard)
315
+ {
316
+ SpessaSynthWarn(`Non-standard formatTypeID: ${resourceFormat}`);
317
+ this.resourceFormat = resourceFormat.toString();
318
+ }
319
+ const resourceFormatID = resourceFormat[1];
320
+ if (Object.values(resourceFormatIDs).indexOf(resourceFormatID) === -1)
321
+ {
322
+ SpessaSynthWarn(`Unrecognized resource format: ${resourceFormatID}`);
323
+ }
324
+ else
325
+ {
326
+ this.resourceFormat = Object.keys(resourceFormatIDs)
327
+ .find(k => resourceFormatIDs[k] === resourceFormatID);
328
+ }
329
+ }
330
+ }
331
+ else
332
+ {
333
+ // folder node
334
+ this.resourceFormat = "folder";
335
+ while (this.nodeData.currentIndex < this.nodeData.length)
336
+ {
337
+ const nodeStartIndex = this.nodeData.currentIndex;
338
+ const nodeLength = readVariableLengthQuantity(this.nodeData);
339
+ const nodeData = this.nodeData.slice(nodeStartIndex, nodeStartIndex + nodeLength);
340
+ this.nodeData.currentIndex = nodeStartIndex + nodeLength;
341
+ this.innerNodes.push(new XMFNode(nodeData));
342
+ }
343
+ }
344
+ }
345
+
346
+ get isFile()
347
+ {
348
+ return this.itemCount === 0;
349
+ }
350
+ }
351
+
352
+ /**
353
+ * @param midi {MIDI}
354
+ * @param binaryData {IndexedByteArray}
355
+ * @returns {IndexedByteArray} the file byte array
356
+ */
357
+ export function loadXMF(midi, binaryData)
358
+ {
359
+ midi.bankOffset = 0;
360
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/xmf-v1a.pdf
361
+ // https://wiki.multimedia.cx/index.php?title=Extensible_Music_Format_(XMF)
362
+ const sanityCheck = readBytesAsString(binaryData, 4);
363
+ if (sanityCheck !== "XMF_")
364
+ {
365
+ SpessaSynthGroupEnd();
366
+ throw new SyntaxError(`Invalid XMF Header! Expected "_XMF", got "${sanityCheck}"`);
367
+ }
368
+
369
+ SpessaSynthGroup("%cParsing XMF file...", consoleColors.info);
370
+ const version = readBytesAsString(binaryData, 4);
371
+ SpessaSynthInfo(
372
+ `%cXMF version: %c${version}`,
373
+ consoleColors.info, consoleColors.recognized
374
+ );
375
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp43.pdf
376
+ // version 2.00 has additional bytes
377
+ if (version === "2.00")
378
+ {
379
+ const fileTypeId = readBytesAsUintBigEndian(binaryData, 4);
380
+ const fileTypeRevisionId = readBytesAsUintBigEndian(binaryData, 4);
381
+ SpessaSynthInfo(
382
+ `%cFile Type ID: %c${fileTypeId}%c, File Type Revision ID: %c${fileTypeRevisionId}`,
383
+ consoleColors.info,
384
+ consoleColors.recognized,
385
+ consoleColors.info,
386
+ consoleColors.recognized
387
+ );
388
+ }
389
+
390
+ // file length
391
+ readVariableLengthQuantity(binaryData);
392
+
393
+ const metadataTableLength = readVariableLengthQuantity(binaryData);
394
+ // skip metadata
395
+ binaryData.currentIndex += metadataTableLength;
396
+
397
+ // skip to tree root
398
+ binaryData.currentIndex = readVariableLengthQuantity(binaryData);
399
+ const rootNode = new XMFNode(binaryData);
400
+ /**
401
+ * @type {IndexedByteArray}
402
+ */
403
+ let midiArray;
404
+ /**
405
+ * find the stuff we care about
406
+ * @param node {XMFNode}
407
+ */
408
+ const searchNode = node =>
409
+ {
410
+ const checkMeta = (xmf, rmid) =>
411
+ {
412
+ if (node.metadata[xmf] !== undefined && typeof node.metadata[xmf] === "string")
413
+ {
414
+ midi.RMIDInfo[rmid] = node.metadata[xmf];
415
+ }
416
+ };
417
+ // meta
418
+ checkMeta("nodeName", RMIDINFOChunks.name);
419
+ checkMeta("title", RMIDINFOChunks.name);
420
+ checkMeta("copyrightNotice", RMIDINFOChunks.copyright);
421
+ checkMeta("comment", RMIDINFOChunks.comment);
422
+ if (node.isFile)
423
+ {
424
+ switch (node.resourceFormat)
425
+ {
426
+ default:
427
+ return;
428
+ case "DLS1":
429
+ case "DLS2":
430
+ case "DLS22":
431
+ case "mobileDLS":
432
+ SpessaSynthInfo("%cFound embedded DLS!", consoleColors.recognized);
433
+ midi.embeddedSoundFont = node.nodeData.buffer;
434
+ break;
435
+
436
+ case "StandardMIDIFile":
437
+ case "StandardMIDIFileType1":
438
+ SpessaSynthInfo("%cFound embedded MIDI!", consoleColors.recognized);
439
+ midiArray = node.nodeData;
440
+ break;
441
+ }
442
+ }
443
+ else
444
+ {
445
+ for (const n of node.innerNodes)
446
+ {
447
+ searchNode(n);
448
+ }
449
+ }
450
+ };
451
+ searchNode(rootNode);
452
+ SpessaSynthGroupEnd();
453
+ return midiArray;
454
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.24.38",
3
+ "version": "3.24.40",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "browser": "index.js",
6
6
  "type": "module",
@@ -53,7 +53,7 @@ export class Sequencer
53
53
  {
54
54
  /**
55
55
  * Executes when MIDI parsing has an error.
56
- * @type {function(string)}
56
+ * @type {function(Error)}
57
57
  */
58
58
  onError;
59
59
 
@@ -138,12 +138,22 @@ export function _playTo(time, ticks = undefined)
138
138
  break;
139
139
 
140
140
  case messageTypes.programChange:
141
+ // empty tracks cannot program change
142
+ if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0)
143
+ {
144
+ break;
145
+ }
141
146
  const p = programs[channel];
142
147
  p.program = event.messageData[0];
143
148
  p.actualBank = p.bank;
144
149
  break;
145
150
 
146
151
  case messageTypes.controllerChange:
152
+ // empty tracks cannot controller change
153
+ if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0)
154
+ {
155
+ break;
156
+ }
147
157
  // do not skip data entries
148
158
  const controllerNumber = event.messageData[0];
149
159
  if (isCCNonSkippable(controllerNumber))
@@ -65,10 +65,20 @@ export function _processEvent(event, trackIndex)
65
65
  break;
66
66
 
67
67
  case messageTypes.controllerChange:
68
+ // empty tracks cannot cc change
69
+ if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0)
70
+ {
71
+ return;
72
+ }
68
73
  this.synth.controllerChange(statusByteData.channel, event.messageData[0], event.messageData[1]);
69
74
  break;
70
75
 
71
76
  case messageTypes.programChange:
77
+ // empty tracks cannot program change
78
+ if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0)
79
+ {
80
+ return;
81
+ }
72
82
  this.synth.programChange(statusByteData.channel, event.messageData[0]);
73
83
  break;
74
84
 
@@ -174,7 +174,8 @@ export function loadNewSongList(midiBuffers, autoPlay = true)
174
174
  }
175
175
  catch (e)
176
176
  {
177
- this.post(WorkletSequencerReturnMessageType.midiError, e.message);
177
+ console.error(e);
178
+ this.post(WorkletSequencerReturnMessageType.midiError, e);
178
179
  return mids;
179
180
  }
180
181
  return mids;
@@ -16,7 +16,7 @@ export function getReverbProcessor(context, reverbBuffer = undefined)
16
16
  else
17
17
  {
18
18
  // decode
19
- context.decodeAudioData(reverbBufferBinary).then(b =>
19
+ context.decodeAudioData(reverbBufferBinary.slice(0, reverbBufferBinary.byteLength)).then(b =>
20
20
  {
21
21
  convolver.buffer = b;
22
22
  });