spessasynth_lib 3.25.4 → 3.25.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/README.md CHANGED
@@ -55,7 +55,7 @@ Supported formats list:
55
55
  - **Easy to Use:** *Basic setup is just [two lines of code!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#minimal-setup)*
56
56
  - **No dependencies:** *Batteries included!*
57
57
 
58
- ### Powerful Synthesizer
58
+ ### Powerful MIDI Synthesizer
59
59
  - Suitable for both **real-time** and **offline** synthesis
60
60
  - **Excellent SoundFont support:**
61
61
  - **Full Generator Support**
@@ -533,35 +533,7 @@ class BasicMIDI extends MIDISequenceData
533
533
  );
534
534
  }
535
535
 
536
- // lyrics fix:
537
- // sometimes, all lyrics events lack spaces at the start or end of the lyric
538
- // then, and only then, add space at the end of each lyric
539
- // space ASCII is 32
540
- let lacksSpaces = true;
541
- for (const lyric of this.lyrics)
542
- {
543
- if (lyric[0] === 32 || lyric[lyric.length - 1] === 32)
544
- {
545
- lacksSpaces = false;
546
- break;
547
- }
548
- }
549
536
 
550
- if (lacksSpaces)
551
- {
552
- this.lyrics = this.lyrics.map(lyric =>
553
- {
554
- // One exception: hyphens at the end. Don't add a space to them
555
- if (lyric[lyric.length - 1] === 45)
556
- {
557
- return lyric;
558
- }
559
- const withSpaces = new Uint8Array(lyric.length + 1);
560
- withSpaces.set(lyric, 0);
561
- withSpaces[lyric.length] = 32;
562
- return withSpaces;
563
- });
564
- }
565
537
  /**
566
538
  * The total playback time, in seconds
567
539
  * @type {number}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.25.4",
3
+ "version": "3.25.6",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "browser": "index.js",
6
6
  "type": "module",
@@ -1,19 +1,23 @@
1
1
  export class BasicInstrument
2
2
  {
3
- constructor()
4
- {
5
- /**
6
- * The instrument's name
7
- * @type {string}
8
- */
9
- this.instrumentName = "";
10
- /**
11
- * The instrument's zones
12
- * @type {BasicInstrumentZone[]}
13
- */
14
- this.instrumentZones = [];
15
- this._useCount = 0;
16
- }
3
+ /**
4
+ * The instrument's name
5
+ * @type {string}
6
+ */
7
+ instrumentName = "";
8
+
9
+ /**
10
+ * The instrument's zones
11
+ * @type {BasicInstrumentZone[]}
12
+ */
13
+ instrumentZones = [];
14
+
15
+ /**
16
+ * Instrument's use count, used for trimming
17
+ * @type {number}
18
+ * @private
19
+ */
20
+ _useCount = 0;
17
21
 
18
22
  /**
19
23
  * @returns {number}
@@ -14,69 +14,69 @@ import { isXGDrums } from "../../utils/xg_hacks.js";
14
14
  export class BasicPreset
15
15
  {
16
16
  /**
17
- * @param modulators {Modulator[]}
17
+ * The parent soundbank instance
18
+ * Currently used for determining default modulators and XG status
19
+ * @type {BasicSoundBank}
18
20
  */
19
- constructor(modulators)
21
+ parentSoundBank;
22
+
23
+ /**
24
+ * The preset's name
25
+ * @type {string}
26
+ */
27
+ presetName = "";
28
+
29
+ /**
30
+ * The preset's MIDI program number
31
+ * @type {number}
32
+ */
33
+ program = 0;
34
+
35
+ /**
36
+ * The preset's MIDI bank number
37
+ * @type {number}
38
+ */
39
+ bank = 0;
40
+
41
+ /**
42
+ * The preset's zones
43
+ * @type {BasicPresetZone[]}
44
+ */
45
+ presetZones = [];
46
+
47
+ /**
48
+ * Stores already found getSamplesAndGenerators for reuse
49
+ * @type {SampleAndGenerators[][][]}
50
+ */
51
+ foundSamplesAndGenerators = [];
52
+
53
+ /**
54
+ * unused metadata
55
+ * @type {number}
56
+ */
57
+ library = 0;
58
+ /**
59
+ * unused metadata
60
+ * @type {number}
61
+ */
62
+ genre = 0;
63
+ /**
64
+ * unused metadata
65
+ * @type {number}
66
+ */
67
+ morphology = 0;
68
+
69
+ /**
70
+ * Creates a new preset representation
71
+ * @param parentSoundBank {BasicSoundBank}
72
+ */
73
+ constructor(parentSoundBank)
20
74
  {
21
- /**
22
- * The preset's name
23
- * @type {string}
24
- */
25
- this.presetName = "";
26
- /**
27
- * The preset's MIDI program number
28
- * @type {number}
29
- */
30
- this.program = 0;
31
- /**
32
- * The preset's MIDI bank number
33
- * @type {number}
34
- */
35
- this.bank = 0;
36
-
37
- /**
38
- * The preset's zones
39
- * @type {BasicPresetZone[]}
40
- */
41
- this.presetZones = [];
42
-
43
- /**
44
- * SampleID offset for this preset
45
- * @type {number}
46
- */
47
- this.sampleIDOffset = 0;
48
-
49
- /**
50
- * Stores already found getSamplesAndGenerators for reuse
51
- * @type {SampleAndGenerators[][][]}
52
- */
53
- this.foundSamplesAndGenerators = [];
75
+ this.parentSoundBank = parentSoundBank;
54
76
  for (let i = 0; i < 128; i++)
55
77
  {
56
78
  this.foundSamplesAndGenerators[i] = [];
57
79
  }
58
-
59
- /**
60
- * unused metadata
61
- * @type {number}
62
- */
63
- this.library = 0;
64
- /**
65
- * unused metadata
66
- * @type {number}
67
- */
68
- this.genre = 0;
69
- /**
70
- * unused metadata
71
- * @type {number}
72
- */
73
- this.morphology = 0;
74
-
75
- /**
76
- * Default modulators
77
- * @type {Modulator[]}
78
- */
79
- this.defaultModulators = modulators;
80
80
  }
81
81
 
82
82
  /**
@@ -86,8 +86,13 @@ export class BasicPreset
86
86
  */
87
87
  isDrumPreset(allowXG, allowSFX = false)
88
88
  {
89
+ const xg = allowXG && this.parentSoundBank.isXGBank;
90
+ console.log(xg);
89
91
  // sfx is not cool
90
- return this.bank === 128 || (allowXG && isXGDrums(this.bank) && (this.bank !== 126 || allowSFX));
92
+ return this.bank === 128 || (
93
+ xg &&
94
+ (isXGDrums(this.bank) && (this.bank !== 126 || allowSFX))
95
+ );
91
96
  }
92
97
 
93
98
  deletePreset()
@@ -286,7 +291,7 @@ export class BasicPreset
286
291
  // default mods
287
292
  addUniqueMods(
288
293
  instrumentModulators,
289
- this.defaultModulators
294
+ this.parentSoundBank.defaultModulators
290
295
  );
291
296
 
292
297
  /**
@@ -10,6 +10,79 @@ const RESAMPLE_RATE = 48000;
10
10
 
11
11
  export class BasicSample
12
12
  {
13
+
14
+ /**
15
+ * The sample's name
16
+ * @type {string}
17
+ */
18
+ sampleName;
19
+
20
+ /**
21
+ * Sample rate in Hz
22
+ * @type {number}
23
+ */
24
+ sampleRate;
25
+
26
+ /**
27
+ * Original pitch of the sample as a MIDI note number
28
+ * @type {number}
29
+ */
30
+ samplePitch;
31
+
32
+ /**
33
+ * Pitch correction, in cents. Can be negative
34
+ * @type {number}
35
+ */
36
+ samplePitchCorrection;
37
+
38
+ /**
39
+ * Sample link, currently unused here
40
+ * @type {number}
41
+ */
42
+ sampleLink;
43
+
44
+ /**
45
+ * Type of the sample, currently only used for SF3
46
+ * @type {number}
47
+ */
48
+ sampleType;
49
+
50
+ /**
51
+ * Relative to the start of the sample in sample points
52
+ * @type {number}
53
+ */
54
+ sampleLoopStartIndex;
55
+
56
+ /**
57
+ * Relative to the start of the sample in sample points
58
+ * @type {number}
59
+ */
60
+ sampleLoopEndIndex;
61
+
62
+ /**
63
+ * Indicates if the sample is compressed
64
+ * @type {boolean}
65
+ */
66
+ isCompressed;
67
+
68
+ /**
69
+ * The compressed sample data if it was compressed by spessasynth
70
+ * @type {Uint8Array}
71
+ */
72
+ compressedData = undefined;
73
+
74
+ /**
75
+ * The sample's use count
76
+ * @type {number}
77
+ */
78
+ useCount = 0;
79
+
80
+ /**
81
+ * The sample's audio data
82
+ * @type {Float32Array}
83
+ */
84
+ sampleData = undefined;
85
+
13
86
  /**
14
87
  * The basic representation of a soundfont sample
15
88
  * @param sampleName {string} The sample's name
@@ -32,72 +105,19 @@ export class BasicSample
32
105
  loopEnd
33
106
  )
34
107
  {
35
- /**
36
- * Sample's name
37
- * @type {string}
38
- */
39
108
  this.sampleName = sampleName;
40
- /**
41
- * Sample rate in Hz
42
- * @type {number}
43
- */
44
109
  this.sampleRate = sampleRate;
45
- /**
46
- * Original pitch of the sample as a MIDI note number
47
- * @type {number}
48
- */
49
110
  this.samplePitch = samplePitch;
50
- /**
51
- * Pitch correction, in cents. Can be negative
52
- * @type {number}
53
- */
54
111
  this.samplePitchCorrection = samplePitchCorrection;
55
- /**
56
- * Sample link, currently unused.
57
- * @type {number}
58
- */
59
112
  this.sampleLink = sampleLink;
60
- /**
61
- * Type of the sample, an enum
62
- * @type {number}
63
- */
64
113
  this.sampleType = sampleType;
65
- /**
66
- * Relative to the start of the sample in sample points
67
- * @type {number}
68
- */
69
114
  this.sampleLoopStartIndex = loopStart;
70
- /**
71
- * Relative to the start of the sample in sample points
72
- * @type {number}
73
- */
74
115
  this.sampleLoopEndIndex = loopEnd;
75
-
76
- /**
77
- * Indicates if the sample is compressed
78
- * @type {boolean}
79
- */
116
+ // https://github.com/FluidSynth/fluidsynth/wiki/SoundFont3Format
80
117
  this.isCompressed = (sampleType & 0x10) > 0;
81
-
82
- /**
83
- * The compressed sample data if it was compressed by spessasynth
84
- * @type {Uint8Array}
85
- */
86
- this.compressedData = undefined;
87
-
88
- /**
89
- * The sample's use count
90
- * @type {number}
91
- */
92
- this.useCount = 0;
93
-
94
- /**
95
- * The sample's audio data
96
- * @type {Float32Array}
97
- */
98
- this.sampleData = undefined;
99
118
  }
100
119
 
120
+
101
121
  /**
102
122
  * @returns {Uint8Array|IndexedByteArray}
103
123
  */
@@ -18,42 +18,49 @@ import { isXGDrums } from "../../utils/xg_hacks.js";
18
18
 
19
19
  class BasicSoundBank
20
20
  {
21
+
22
+ /**
23
+ * Soundfont's info stored as name: value. ifil and iver are stored as string representation of float (e.g., 2.1)
24
+ * @type {Object<string, string|IndexedByteArray>}
25
+ */
26
+ soundFontInfo = {};
27
+
28
+ /**
29
+ * The soundfont's presets
30
+ * @type {BasicPreset[]}
31
+ */
32
+ presets = [];
33
+
34
+ /**
35
+ * The soundfont's samples
36
+ * @type {BasicSample[]}
37
+ */
38
+ samples = [];
39
+
40
+ /**
41
+ * The soundfont's instruments
42
+ * @type {BasicInstrument[]}
43
+ */
44
+ instruments = [];
45
+
46
+ /**
47
+ * Soundfont's default modulatorss
48
+ * @type {Modulator[]}
49
+ */
50
+ defaultModulators = defaultModulators.map(m => Modulator.copy(m));
51
+
52
+ /**
53
+ * Checks for XG drumsets and considers if this soundfont is XG.
54
+ * @type {boolean}
55
+ */
56
+ isXGBank = false;
57
+
21
58
  /**
22
59
  * Creates a new basic soundfont template
23
60
  * @param data {undefined|{presets: BasicPreset[], info: Object<string, string>}}
24
61
  */
25
62
  constructor(data = undefined)
26
63
  {
27
- /**
28
- * Soundfont's info stored as name: value. ifil and iver are stored as string representation of float (e.g., 2.1)
29
- * @type {Object<string, string|IndexedByteArray>}
30
- */
31
- this.soundFontInfo = {};
32
-
33
- /**
34
- * The soundfont's presets
35
- * @type {BasicPreset[]}
36
- */
37
- this.presets = [];
38
-
39
- /**
40
- * The soundfont's samples
41
- * @type {BasicSample[]}
42
- */
43
- this.samples = [];
44
-
45
- /**
46
- * The soundfont's instruments
47
- * @type {BasicInstrument[]}
48
- */
49
- this.instruments = [];
50
-
51
- /**
52
- * Soundfont's default modulatorss
53
- * @type {Modulator[]}
54
- */
55
- this.defaultModulators = defaultModulators.map(m => Modulator.copy(m));
56
-
57
64
  if (data?.presets)
58
65
  {
59
66
  this.presets.push(...data.presets);
@@ -135,7 +142,7 @@ class BasicSoundBank
135
142
  const pZone = new BasicPresetZone();
136
143
  pZone.instrument = inst;
137
144
 
138
- const preset = new BasicPreset(font.defaultModulators);
145
+ const preset = new BasicPreset(font);
139
146
  preset.presetName = "Saw Wave";
140
147
  preset.presetZones.push(pZone);
141
148
  font.presets.push(preset);
@@ -143,9 +150,45 @@ class BasicSoundBank
143
150
  font.soundFontInfo["ifil"] = "2.1";
144
151
  font.soundFontInfo["isng"] = "EMU8000";
145
152
  font.soundFontInfo["INAM"] = "Dummy";
153
+ font._parseInternal();
146
154
  return font.write().buffer;
147
155
  }
148
156
 
157
+ /**
158
+ * parses the bank after loading is done
159
+ * @protected
160
+ */
161
+ _parseInternal()
162
+ {
163
+ this.isXGBank = false;
164
+ // definitions for XG:
165
+ // at least one preset with bank 127, 126 or 120
166
+ // MUST be a valid XG bank.
167
+ // allowed banks: (see XG specification)
168
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 24,
169
+ // 25, 27, 28, 29, 30, 31, 32, 33, 40, 41, 48,
170
+ // 64, 65, 66, 126, 127
171
+ const allowedPrograms = new Set([
172
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 24,
173
+ 25, 27, 28, 29, 30, 31, 32, 33, 40, 41, 48,
174
+ 64, 65, 66, 126, 127
175
+ ]);
176
+ for (const preset of this.presets)
177
+ {
178
+ if (isXGDrums(preset.bank))
179
+ {
180
+ this.isXGBank = true;
181
+ if (!allowedPrograms.has(preset.program))
182
+ {
183
+ // not valid!
184
+ console.log("not XG!!");
185
+ this.isXGBank = false;
186
+ break;
187
+ }
188
+ }
189
+ }
190
+ }
191
+
149
192
  /**
150
193
  * Trims a sound bank to only contain samples in a given MIDI file
151
194
  * @param mid {BasicMIDI} - the MIDI file
@@ -381,16 +424,7 @@ class BasicSoundBank
381
424
  }
382
425
 
383
426
  /**
384
- * To avoid overlapping on multiple desfonts
385
- * @param offset {number}
386
- */
387
- setSampleIDOffset(offset)
388
- {
389
- this.presets.forEach(p => p.sampleIDOffset = offset);
390
- }
391
-
392
- /**
393
- * Get the appropriate preset, undefined if not foun d
427
+ * Get the appropriate preset, undefined if not found
394
428
  * @param bankNr {number}
395
429
  * @param programNr {number}
396
430
  * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset
@@ -398,14 +432,22 @@ class BasicSoundBank
398
432
  */
399
433
  getPresetNoFallback(bankNr, programNr, allowXGDrums = false)
400
434
  {
435
+ const isDrum = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr));
401
436
  // check for exact match
402
- const p = this.presets.find(p => p.bank === bankNr && p.program === programNr);
437
+ let p;
438
+ if (isDrum)
439
+ {
440
+ p = this.presets.find(p => p.bank === bankNr && p.isDrumPreset(allowXGDrums) && p.program === programNr);
441
+ }
442
+ else
443
+ {
444
+ p = this.presets.find(p => p.bank === bankNr && p.program === programNr);
445
+ }
403
446
  if (p)
404
447
  {
405
448
  return p;
406
449
  }
407
450
  // no match...
408
- const isDrum = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr));
409
451
  if (isDrum)
410
452
  {
411
453
  if (allowXGDrums)
@@ -430,36 +472,47 @@ class BasicSoundBank
430
472
  */
431
473
  getPreset(bankNr, programNr, allowXGDrums = false)
432
474
  {
433
- // check for exact match
434
- let preset = this.presets.find(p => p.bank === bankNr && p.program === programNr);
435
475
  const isDrums = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr));
436
- if (!preset)
476
+ // check for exact match
477
+ let preset;
478
+ // only allow drums if the preset is considered to be a drum preset
479
+ if (isDrums)
437
480
  {
438
- // no match...
439
- if (isDrums)
440
- {
441
- // drum preset: find any preset with bank 128
442
- preset = this.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNr);
443
- if (!preset)
444
- {
445
- // only allow 128, otherwise it would default to XG SFX
446
- preset = this.presets.find(p => p.isDrumPreset(allowXGDrums));
447
- }
448
- }
449
- else
450
- {
451
- // non-drum preset: find any preset with the given program that is not a drum preset
452
- preset = this.presets.find(p => p.program === programNr && !p.isDrumPreset(allowXGDrums));
453
- }
454
- if (preset)
481
+ preset = this.presets.find(p => p.bank === bankNr && p.isDrumPreset(allowXGDrums) && p.program === programNr);
482
+ }
483
+ else
484
+ {
485
+ preset = this.presets.find(p => p.bank === bankNr && p.program === programNr);
486
+ }
487
+ if (preset)
488
+ {
489
+ return preset;
490
+ }
491
+ // no match...
492
+ if (isDrums)
493
+ {
494
+ // drum preset: find any preset with bank 128
495
+ preset = this.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNr);
496
+ if (!preset)
455
497
  {
456
- SpessaSynthWarn(
457
- `%cPreset ${bankNr}.${programNr} not found. Replaced with %c${preset.presetName} (${preset.bank}.${preset.program})`,
458
- consoleColors.warn,
459
- consoleColors.recognized
460
- );
498
+ // only allow 128, otherwise it would default to XG SFX
499
+ preset = this.presets.find(p => p.isDrumPreset(allowXGDrums));
461
500
  }
462
501
  }
502
+ else
503
+ {
504
+ // non-drum preset: find any preset with the given program that is not a drum preset
505
+ preset = this.presets.find(p => p.program === programNr && !p.isDrumPreset(allowXGDrums));
506
+ }
507
+ if (preset)
508
+ {
509
+ SpessaSynthWarn(
510
+ `%cPreset ${bankNr}.${programNr} not found. Replaced with %c${preset.presetName} (${preset.bank}.${preset.program})`,
511
+ consoleColors.warn,
512
+ consoleColors.recognized
513
+ );
514
+ }
515
+
463
516
  // no preset, use the first one available
464
517
  if (!preset)
465
518
  {
@@ -1,19 +1,19 @@
1
1
  import { BasicPreset } from "../basic_soundfont/basic_preset.js";
2
2
  import { BasicPresetZone } from "../basic_soundfont/basic_zones.js";
3
3
  import { BasicInstrument } from "../basic_soundfont/basic_instrument.js";
4
- import { defaultModulators } from "../basic_soundfont/modulator.js";
5
4
 
6
5
  export class DLSPreset extends BasicPreset
7
6
  {
8
7
  /**
9
8
  * Creates a new DLS preset
9
+ * @param dls {BasicSoundBank}
10
10
  * @param ulBank {number}
11
11
  * @param ulInstrument {number}
12
12
  */
13
- constructor(ulBank, ulInstrument)
13
+ constructor(dls, ulBank, ulInstrument)
14
14
  {
15
15
  // use stock default modulators, dls won't ever have DMOD chunk
16
- super(defaultModulators);
16
+ super(dls);
17
17
  this.program = ulInstrument & 127;
18
18
  const bankMSB = (ulBank >> 8) & 127;
19
19
  const bankLSB = ulBank & 127;
@@ -115,7 +115,7 @@ class DLSSoundFont extends BasicSoundBank
115
115
 
116
116
  // sort presets
117
117
  this.presets.sort((a, b) => (a.program - b.program) + (a.bank - b.bank));
118
-
118
+ this._parseInternal();
119
119
  SpessaSynthInfo(
120
120
  `%cParsing finished! %c"${this.soundFontInfo["INAM"] || "UNNAMED"}"%c has %c${this.presets.length} %cpresets,
121
121
  %c${this.instruments.length}%c instruments and %c${this.samples.length}%c samples.`,
@@ -20,8 +20,9 @@ export const DLSSources = {
20
20
  volume: 0x87,
21
21
  pan: 0x8a,
22
22
  expression: 0x8b,
23
- chorus: 0xdb,
24
- reverb: 0xdd,
23
+ // note: these are flipped unintentionally in DLS2 table 9. Argh!
24
+ chorus: 0xdd,
25
+ reverb: 0xdb,
25
26
 
26
27
  pitchWheelRange: 0x100,
27
28
  fineTune: 0x101,