spessasynth_core 3.27.4 → 3.27.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_core",
3
- "version": "3.27.4",
3
+ "version": "3.27.6",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -41,7 +41,7 @@ class BasicMIDI extends MIDISequenceData
41
41
  isDLSRMIDI = false;
42
42
 
43
43
  /**
44
- * Copies a MIDI
44
+ * Copies a MIDI (tracks are shallowly copied!)
45
45
  * @param mid {BasicMIDI}
46
46
  * @returns {BasicMIDI}
47
47
  */
@@ -51,9 +51,27 @@ class BasicMIDI extends MIDISequenceData
51
51
  m._copyFromSequence(mid);
52
52
 
53
53
  m.isDLSRMIDI = mid.isDLSRMIDI;
54
- m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice(0) : undefined; // Deep copy
54
+ m.embeddedSoundFont = mid?.embeddedSoundFont ? mid.embeddedSoundFont : undefined; // Shallow copy
55
55
  m.tracks = mid.tracks.map(track => [...track]); // Shallow copy of each track array
56
-
56
+ return m;
57
+ }
58
+
59
+ /**
60
+ * Copies a MIDI with deep copy
61
+ * @param mid {BasicMIDI}
62
+ * @returns {BasicMIDI}
63
+ */
64
+ static copyFromDeep(mid)
65
+ {
66
+ const m = new BasicMIDI();
67
+ m._copyFromSequence(mid);
68
+ m.isDLSRMIDI = mid.isDLSRMIDI;
69
+ m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice(0) : undefined; // Deep copy
70
+ m.tracks = mid.tracks.map(track => track.map(event => new MIDIMessage(
71
+ event.ticks,
72
+ event.messageStatusByte,
73
+ event.messageData
74
+ ))); // Deep copy
57
75
  return m;
58
76
  }
59
77
 
@@ -217,7 +235,6 @@ class BasicMIDI extends MIDISequenceData
217
235
  copyrightComponents.push(readBytesAsString(
218
236
  e.messageData,
219
237
  e.messageData.length,
220
- undefined,
221
238
  false
222
239
  ));
223
240
  e.messageData.currentIndex = 0;
@@ -466,7 +483,7 @@ class BasicMIDI extends MIDISequenceData
466
483
  {
467
484
  this.rawMidiName = name.messageData;
468
485
  name.messageData.currentIndex = 0;
469
- this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
486
+ this.midiName = readBytesAsString(name.messageData, name.messageData.length, false);
470
487
  }
471
488
  }
472
489
  }
@@ -478,7 +495,7 @@ class BasicMIDI extends MIDISequenceData
478
495
  {
479
496
  this.rawMidiName = name.messageData;
480
497
  name.messageData.currentIndex = 0;
481
- this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
498
+ this.midiName = readBytesAsString(name.messageData, name.messageData.length, false);
482
499
  }
483
500
  }
484
501
  }
@@ -45,7 +45,7 @@ class MIDI extends BasicMIDI
45
45
  // possibly an RMID file (https://github.com/spessasus/sf2-rmidi-specification#readme)
46
46
  // skip size
47
47
  binaryData.currentIndex += 8;
48
- const rmid = readBytesAsString(binaryData, 4, undefined, false);
48
+ const rmid = readBytesAsString(binaryData, 4, false);
49
49
  if (rmid !== "RMID")
50
50
  {
51
51
  SpessaSynthGroupEnd();
@@ -101,18 +101,15 @@ class MIDI extends BasicMIDI
101
101
  this.copyright = readBytesAsString(
102
102
  this.RMIDInfo["ICOP"],
103
103
  this.RMIDInfo["ICOP"].length,
104
- undefined,
105
104
  false
106
105
  ).replaceAll("\n", " ");
107
106
  }
108
107
  if (this.RMIDInfo["INAM"])
109
108
  {
110
109
  this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name];
111
- // noinspection JSCheckFunctionSignatures
112
110
  this.midiName = readBytesAsString(
113
- this.rawMidiName,
111
+ /** @type {IndexedByteArray}*/this.rawMidiName,
114
112
  this.rawMidiName.length,
115
- undefined,
116
113
  false
117
114
  ).replaceAll("\n", " ");
118
115
  }
@@ -1,3 +1,5 @@
1
+ import { IndexedByteArray } from "../utils/indexed_array.js";
2
+
1
3
  /**
2
4
  * This is the base type for MIDI files. It contains all the "metadata" and information.
3
5
  * It extends to:
@@ -133,7 +135,7 @@ class MIDISequenceData
133
135
  * The RMID (Resource-Interchangeable MIDI) info data, if the file is RMID formatted.
134
136
  * Otherwise, this field is undefined.
135
137
  * Chunk type (e.g. "INAM"): Chunk data as a binary array.
136
- * @type {Object<string, IndexedByteArray>}
138
+ * @type {Record<string, IndexedByteArray>}
137
139
  */
138
140
  RMIDInfo = {};
139
141
 
@@ -217,7 +219,11 @@ class MIDISequenceData
217
219
  // copying objects
218
220
  this.loop = { ...sequence.loop };
219
221
  this.keyRange = { ...sequence.keyRange };
220
- this.RMIDInfo = { ...sequence.RMIDInfo };
222
+ this.RMIDInfo = {};
223
+ for (const [key, value] of Object.entries(sequence.RMIDInfo))
224
+ {
225
+ this.RMIDInfo[key] = new IndexedByteArray(value);
226
+ }
221
227
  }
222
228
  }
223
229
 
@@ -180,7 +180,7 @@ export function modifyMIDI(
180
180
  const midiPorts = midi.midiPorts.slice();
181
181
  /**
182
182
  * midi port: channel offset
183
- * @type {Object<number, number>}
183
+ * @type {Record<number, number>}
184
184
  */
185
185
  const midiPortChannelOffsets = {};
186
186
  let midiPortChannelOffset = 0;
@@ -48,7 +48,7 @@ const DEFAULT_COPYRIGHT = "Created using SpessaSynth";
48
48
  */
49
49
 
50
50
  /**
51
- * Writes an RMIDI file
51
+ * Writes an RMIDI file. Note that this method modifies the MIDI file in-place.
52
52
  * @this {BasicMIDI}
53
53
  * @param soundfontBinary {Uint8Array}
54
54
  * @param soundfont {BasicSoundBank}
@@ -10,7 +10,7 @@ import { SoundFontManager } from "../../synthetizer/audio_engine/engine_componen
10
10
  * Gets the used programs and keys for this MIDI file with a given sound bank
11
11
  * @this {BasicMIDI}
12
12
  * @param soundfont {SoundFontManager|BasicSoundBank} - the sound bank
13
- * @returns {Object<string, Set<string>>} Object<bank:program, Set<key-velocity>>
13
+ * @returns {Record<string, Set<string>>} Record<bank:program, Set<key-velocity>>
14
14
  */
15
15
  export function getUsedProgramsAndKeys(soundfont)
16
16
  {
@@ -81,7 +81,7 @@ export function getUsedProgramsAndKeys(soundfont)
81
81
  /**
82
82
  * find all programs used and key-velocity combos in them
83
83
  * bank:program each has a set of midiNote-velocity
84
- * @type {Object<string, Set<string>>}
84
+ * @type {Record<string, Set<string>>}
85
85
  */
86
86
  const usedProgramsAndKeys = {};
87
87
 
@@ -90,7 +90,7 @@ class XMFNode
90
90
  metadataLength;
91
91
 
92
92
  /**
93
- * @type {Object<string, any>}
93
+ * @type {Record<string, any>}
94
94
  */
95
95
  metadata = {};
96
96
 
@@ -111,8 +111,8 @@ class SpessaSynthSequencer
111
111
  midiPortChannelOffset = 0;
112
112
  /**
113
113
  * stored as:
114
- * Object<midi port, channel offset>
115
- * @type {Object<number, number>}
114
+ * Record<midi port, channel offset>
115
+ * @type {Record<number, number>}
116
116
  */
117
117
  midiPortChannelOffsets = {};
118
118
 
@@ -147,7 +147,7 @@ export function loadNewSongList(midiBuffers, autoPlay = true)
147
147
  * parse the MIDIs (only the array buffers, MIDI is unchanged)
148
148
  * @type {BasicMIDI[]}
149
149
  */
150
- this.songs = midiBuffers.map(m => BasicMIDI.copyFrom(m));
150
+ this.songs = midiBuffers;
151
151
  if (this.songs.length < 1)
152
152
  {
153
153
  return;
@@ -32,7 +32,7 @@ class BasicSoundBank
32
32
 
33
33
  /**
34
34
  * Soundfont's info stored as name: value. ifil and iver are stored as string representation of float (e.g., 2.1)
35
- * @type {Object<string, string|IndexedByteArray>}
35
+ * @type {Record<string, string|IndexedByteArray>}
36
36
  */
37
37
  soundFontInfo = {};
38
38
 
@@ -77,7 +77,7 @@ class BasicSoundBank
77
77
 
78
78
  /**
79
79
  * Creates a new basic soundfont template (or copies)
80
- * @param data {undefined|{presets: BasicPreset[], info: Object<string, string>}}
80
+ * @param data {undefined|{presets: BasicPreset[], info: Record<string, string>}}
81
81
  */
82
82
  constructor(data = undefined)
83
83
  {
@@ -34,6 +34,20 @@ export const modulatorCurveTypes = {
34
34
  switch: 3
35
35
  };
36
36
 
37
+
38
+ export function getModSourceEnum(curveType, polarity, direction, isCC, index)
39
+ {
40
+ return (curveType << 10) | (polarity << 9) | (direction << 8) | (isCC << 7) | index;
41
+ }
42
+
43
+ const defaultResonantModSource = getModSourceEnum(
44
+ modulatorCurveTypes.linear,
45
+ 1,
46
+ 0,
47
+ 1,
48
+ midiControllers.filterResonance
49
+ ); // linear forwards bipolar cc 74
50
+
37
51
  export class Modulator
38
52
  {
39
53
  /**
@@ -70,6 +84,7 @@ export class Modulator
70
84
  * - still can be disabled if the soundfont has its own modulator curve
71
85
  * - this fixes the very low amount of reverb by default and doesn't break soundfonts
72
86
  * @type {boolean}
87
+ * @readonly
73
88
  */
74
89
  isEffectModulator = false;
75
90
 
@@ -77,8 +92,9 @@ export class Modulator
77
92
  * The default resonant modulator does not affect the filter gain.
78
93
  * Neither XG nor GS responded to cc #74 in that way.
79
94
  * @type {boolean}
95
+ * @readonly
80
96
  */
81
- isDefaultResonanceModulator = false;
97
+ isDefaultResonantModulator = false;
82
98
 
83
99
  /**
84
100
  * 1 if the source is bipolar (min is -1, max is 1)
@@ -158,6 +174,7 @@ export class Modulator
158
174
  * @param amount {number}
159
175
  * @param transformType {0|2}
160
176
  * @param isEffectModulator {boolean}
177
+ * @param isDefaultResonantModulator {boolean}
161
178
  */
162
179
  constructor(sourceIndex,
163
180
  sourceCurveType,
@@ -172,7 +189,8 @@ export class Modulator
172
189
  destination,
173
190
  amount,
174
191
  transformType,
175
- isEffectModulator = false)
192
+ isEffectModulator = false,
193
+ isDefaultResonantModulator = false)
176
194
  {
177
195
  this.sourcePolarity = sourcePolarity;
178
196
  this.sourceDirection = sourceDirection;
@@ -190,6 +208,7 @@ export class Modulator
190
208
  this.transformAmount = amount;
191
209
  this.transformType = transformType;
192
210
  this.isEffectModulator = isEffectModulator;
211
+ this.isDefaultResonantModulator = isDefaultResonantModulator;
193
212
 
194
213
 
195
214
  if (this.modulatorDestination > MAX_GENERATOR)
@@ -205,7 +224,7 @@ export class Modulator
205
224
  */
206
225
  static copy(modulator)
207
226
  {
208
- const m = new Modulator(
227
+ return new Modulator(
209
228
  modulator.sourceIndex,
210
229
  modulator.sourceCurveType,
211
230
  modulator.sourceUsesCC,
@@ -219,10 +238,9 @@ export class Modulator
219
238
  modulator.modulatorDestination,
220
239
  modulator.transformAmount,
221
240
  modulator.transformType,
222
- modulator.isEffectModulator
241
+ modulator.isEffectModulator,
242
+ modulator.isDefaultResonantModulator
223
243
  );
224
- m.isDefaultResonanceModulator = modulator.isDefaultResonanceModulator;
225
- return m;
226
244
  }
227
245
 
228
246
  /**
@@ -322,7 +340,7 @@ export class Modulator
322
340
  */
323
341
  sumTransform(modulator)
324
342
  {
325
- const m = new Modulator(
343
+ return new Modulator(
326
344
  this.sourceIndex,
327
345
  this.sourceCurveType,
328
346
  this.sourceUsesCC,
@@ -336,10 +354,9 @@ export class Modulator
336
354
  this.modulatorDestination,
337
355
  this.transformAmount + modulator.transformAmount,
338
356
  this.transformType,
339
- this.isEffectModulator
357
+ this.isEffectModulator,
358
+ this.isDefaultResonantModulator
340
359
  );
341
- m.isDefaultResonanceModulator = modulator.isDefaultResonanceModulator;
342
- return m;
343
360
  }
344
361
  }
345
362
 
@@ -396,16 +413,19 @@ export class DecodedModulator extends Modulator
396
413
  this.modulatorDestination === generatorTypes.reverbEffectsSend
397
414
  || this.modulatorDestination === generatorTypes.chorusEffectsSend
398
415
  );
416
+
417
+
418
+ this.isDefaultResonantModulator = (
419
+ sourceEnum === defaultResonantModSource
420
+ && secondarySourceEnum === 0x0
421
+ && this.modulatorDestination === generatorTypes.initialFilterQ
422
+ );
399
423
  }
400
424
  }
401
425
 
402
426
  export const DEFAULT_ATTENUATION_MOD_AMOUNT = 960;
403
427
  export const DEFAULT_ATTENUATION_MOD_CURVE_TYPE = modulatorCurveTypes.concave;
404
428
 
405
- export function getModSourceEnum(curveType, polarity, direction, isCC, index)
406
- {
407
- return (curveType << 10) | (polarity << 9) | (direction << 8) | (isCC << 7) | index;
408
- }
409
429
 
410
430
  const soundFontModulators = [
411
431
  // vel to attenuation
@@ -542,26 +562,18 @@ const customModulators = [
542
562
  generatorTypes.initialFilterFc,
543
563
  6000,
544
564
  0
565
+ ),
566
+
567
+ // cc 71 (filter Q) to filter Q (default resonant modulator)
568
+ new DecodedModulator(
569
+ defaultResonantModSource,
570
+ 0x0, // no controller
571
+ generatorTypes.initialFilterQ,
572
+ 250,
573
+ 0
545
574
  )
546
575
 
547
576
  ];
548
- // cc 71 (filter Q) to filter Q
549
- const resonanceModulator = new DecodedModulator(
550
- getModSourceEnum(
551
- modulatorCurveTypes.linear,
552
- 1,
553
- 0,
554
- 1,
555
- midiControllers.filterResonance
556
- ), // linear forwards bipolar cc 74
557
- 0x0, // no controller
558
- generatorTypes.initialFilterQ,
559
- 250,
560
- 0
561
- );
562
-
563
- resonanceModulator.isDefaultResonanceModulator = true;
564
- customModulators.push(resonanceModulator);
565
577
 
566
578
  /**
567
579
  * @type {Modulator[]}
@@ -234,7 +234,7 @@ export function combineZones(preset, globalize = true)
234
234
  continue;
235
235
  }
236
236
  /**
237
- * @type {Object<string, number>}
237
+ * @type {Record<string, number>}
238
238
  */
239
239
  let occurencesForValues = {};
240
240
  const defaultForChecked = generatorLimits[checkedType]?.def || 0;
@@ -12,7 +12,7 @@ export function loadSoundFont(buffer)
12
12
  {
13
13
  const check = buffer.slice(8, 12);
14
14
  const a = new IndexedByteArray(check);
15
- const id = readBytesAsString(a, 4, undefined, false).toLowerCase();
15
+ const id = readBytesAsString(a, 4, false).toLowerCase();
16
16
  if (id === "dls ")
17
17
  {
18
18
  return new DLSSoundFont(buffer);
@@ -101,7 +101,7 @@ export class SoundFont2 extends BasicSoundBank
101
101
  break;
102
102
 
103
103
  case "icmt":
104
- text = readBytesAsString(chunk.chunkData, chunk.chunkData.length, undefined, false);
104
+ text = readBytesAsString(chunk.chunkData, chunk.chunkData.length, false);
105
105
  this.soundFontInfo[chunk.header] = text;
106
106
  break;
107
107
 
@@ -114,7 +114,7 @@ export function computeModulator(controllerTable, modulator, voice)
114
114
  }
115
115
 
116
116
  // resonant modulator: take its value and ensure that it won't change the final gain
117
- if (modulator.isDefaultResonanceModulator)
117
+ if (modulator.isDefaultResonantModulator)
118
118
  {
119
119
  // half the gain, negates the filter
120
120
  voice.resonanceOffset = Math.max(0, computedValue / 2);
@@ -33,7 +33,7 @@ export class SoundFontManager
33
33
  {
34
34
  /**
35
35
  * <"bank-program", "presetName">
36
- * @type {Object<string, string>}
36
+ * @type {Record<string, string>}
37
37
  */
38
38
  const presetList = {};
39
39
  // gather the presets in reverse and replace if necessary
@@ -3,50 +3,39 @@ import { IndexedByteArray } from "../indexed_array.js";
3
3
  /**
4
4
  * @param dataArray {IndexedByteArray}
5
5
  * @param bytes {number}
6
- * @param encoding {string} the textElement encoding
7
6
  * @param trimEnd {boolean} if we should trim once we reach an invalid byte
8
7
  * @returns {string}
9
8
  */
10
- export function readBytesAsString(dataArray, bytes, encoding = undefined, trimEnd = true)
9
+ export function readBytesAsString(dataArray, bytes, trimEnd = true)
11
10
  {
12
- if (!encoding)
11
+ let finished = false;
12
+ let string = "";
13
+ for (let i = 0; i < bytes; i++)
13
14
  {
14
- let finished = false;
15
- let string = "";
16
- for (let i = 0; i < bytes; i++)
15
+ let byte = dataArray[dataArray.currentIndex++];
16
+ if (finished)
17
17
  {
18
- let byte = dataArray[dataArray.currentIndex++];
19
- if (finished)
18
+ continue;
19
+ }
20
+ if ((byte < 32 || byte > 127) && byte !== 10) // 10 is "\n"
21
+ {
22
+ if (trimEnd)
20
23
  {
24
+ finished = true;
21
25
  continue;
22
26
  }
23
- if ((byte < 32 || byte > 127) && byte !== 10) // 10 is "\n"
27
+ else
24
28
  {
25
- if (trimEnd)
29
+ if (byte === 0)
26
30
  {
27
31
  finished = true;
28
32
  continue;
29
33
  }
30
- else
31
- {
32
- if (byte === 0)
33
- {
34
- finished = true;
35
- continue;
36
- }
37
- }
38
34
  }
39
- string += String.fromCharCode(byte);
40
35
  }
41
- return string;
42
- }
43
- else
44
- {
45
- let byteBuffer = dataArray.slice(dataArray.currentIndex, dataArray.currentIndex + bytes);
46
- dataArray.currentIndex += bytes;
47
- let decoder = new TextDecoder(encoding.replace(/[^\x20-\x7E]/g, ""));
48
- return decoder.decode(byteBuffer.buffer);
36
+ string += String.fromCharCode(byte);
49
37
  }
38
+ return string;
50
39
  }
51
40
 
52
41
  /**