spessasynth_core 3.26.32 → 3.26.33

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.26.32",
3
+ "version": "3.26.33",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -42,16 +42,4 @@ export class BasicInstrumentZone extends BasicZone
42
42
  {
43
43
  this.sample.unlinkFrom(this.parentInstrument);
44
44
  }
45
-
46
- /**
47
- * @param zone {BasicZone}
48
- */
49
- copyFrom(zone)
50
- {
51
- super.copyFrom(zone);
52
- if (zone instanceof BasicInstrumentZone)
53
- {
54
- this.sample = zone.sample;
55
- }
56
- }
57
45
  }
@@ -36,16 +36,4 @@ export class BasicPresetZone extends BasicZone
36
36
  this.instrument = instrument;
37
37
  this.instrument.linkTo(this.parentPreset);
38
38
  }
39
-
40
- /**
41
- * @param zone {BasicZone}
42
- */
43
- copyFrom(zone)
44
- {
45
- super.copyFrom(zone);
46
- if (zone instanceof BasicPresetZone)
47
- {
48
- this.instrument = zone.instrument;
49
- }
50
- }
51
39
  }
@@ -1,10 +1,6 @@
1
- /**
2
- * samples.js
3
- * purpose: parses soundfont samples, resamples if needed.
4
- * loads sample data, handles async loading of sf3 compressed samples
5
- */
6
1
  import { SpessaSynthWarn } from "../../utils/loggin.js";
7
2
  import { IndexedByteArray } from "../../utils/indexed_array.js";
3
+ import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js";
8
4
 
9
5
  // should be reasonable for most cases
10
6
  const RESAMPLE_RATE = 48000;
@@ -87,7 +83,7 @@ export class BasicSample
87
83
  * Indicates if the sample is compressed using vorbis SF3
88
84
  * @type {boolean}
89
85
  */
90
- isCompressed;
86
+ isCompressed = false;
91
87
 
92
88
  /**
93
89
  * The compressed sample data if the sample has been compressed
@@ -102,7 +98,7 @@ export class BasicSample
102
98
  linkedInstruments = [];
103
99
  /**
104
100
  * The sample's audio data
105
- * @type {Float32Array}
101
+ * @type {Float32Array|undefined}
106
102
  */
107
103
  sampleData = undefined;
108
104
 
@@ -169,6 +165,10 @@ export class BasicSample
169
165
  */
170
166
  getRawData(allowVorbis = true)
171
167
  {
168
+ if (this.isCompressed && allowVorbis && !this.dataOverriden)
169
+ {
170
+ return this.compressedData;
171
+ }
172
172
  return this.encodeS16LE();
173
173
  }
174
174
 
@@ -210,11 +210,8 @@ export class BasicSample
210
210
  this.resampleData(RESAMPLE_RATE);
211
211
  audioData = this.getAudioData();
212
212
  }
213
- this.compressedData = encodeVorbis([audioData], 1, this.sampleRate, quality);
214
- // flag as compressed
215
- this.isCompressed = true;
216
- // allow the data to be copied from the compressedData chunk during the write operation
217
- this.dataOverriden = false;
213
+ const compressed = encodeVorbis([audioData], 1, this.sampleRate, quality);
214
+ this.setCompressedData(compressed);
218
215
  }
219
216
  catch (e)
220
217
  {
@@ -320,17 +317,69 @@ export class BasicSample
320
317
  this.linkedInstruments.splice(index, 1);
321
318
  }
322
319
 
320
+
323
321
  /**
322
+ * @private
323
+ * Decode binary vorbis into a float32 pcm
324
+ * @returns {Float32Array}
325
+ */
326
+ decodeVorbis()
327
+ {
328
+ if (this.sampleData)
329
+ {
330
+ return this.sampleData;
331
+ }
332
+ // get the compressed byte stream
333
+ // reset array and being decoding
334
+ try
335
+ {
336
+ /**
337
+ * @type {{data: Float32Array[], error: (string|null), sampleRate: number, eof: boolean}}
338
+ */
339
+ const vorbis = stbvorbis.decode(this.compressedData);
340
+ const decoded = vorbis.data[0];
341
+ if (decoded === undefined)
342
+ {
343
+ SpessaSynthWarn(`Error decoding sample ${this.sampleName}: Vorbis decode returned undefined.`);
344
+ return new Float32Array(0);
345
+ }
346
+ // clip
347
+ // because vorbis can go above 1 sometimes
348
+ for (let i = 0; i < decoded.length; i++)
349
+ {
350
+ // magic number is 32,767 / 32,768
351
+ decoded[i] = Math.max(-1, Math.min(decoded[i], 0.999969482421875));
352
+ }
353
+ return decoded;
354
+ }
355
+ catch (e)
356
+ {
357
+ // do not error out, fill with silence
358
+ SpessaSynthWarn(`Error decoding sample ${this.sampleName}: ${e}`);
359
+ return new Float32Array(this.sampleLoopEndIndex + 1);
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Get the float32 audio data.
365
+ * Note that this either decodes the compressed data or passes the ready sampleData.
366
+ * If neither are set then it will throw an error!
324
367
  * @returns {Float32Array}
325
- * @virtual
326
368
  */
327
369
  getAudioData()
328
370
  {
329
- if (!this.sampleData)
371
+ if (this.sampleData)
330
372
  {
331
- throw new Error("Error! Sample data is undefined. Is the method overriden properly?");
373
+ return this.sampleData;
332
374
  }
333
- return this.sampleData;
375
+ if (this.isCompressed)
376
+ {
377
+ // SF3
378
+ // if compressed, decode
379
+ this.sampleData = this.decodeVorbis();
380
+ return this.sampleData;
381
+ }
382
+ throw new Error("Sample data is undefined for a BasicSample instance.");
334
383
  }
335
384
 
336
385
  /**
@@ -372,4 +421,16 @@ export class BasicSample
372
421
  this.sampleData = audioData;
373
422
  this.dataOverriden = true;
374
423
  }
424
+
425
+ /**
426
+ * Replaces the audio with a compressed data sample
427
+ * @param data {Uint8Array}
428
+ */
429
+ setCompressedData(data)
430
+ {
431
+ this.sampleData = undefined;
432
+ this.compressedData = data;
433
+ this.isCompressed = true;
434
+ this.dataOverriden = false;
435
+ }
375
436
  }
@@ -39,18 +39,21 @@ class BasicSoundBank
39
39
  /**
40
40
  * The soundfont's presets
41
41
  * @type {BasicPreset[]}
42
+ * @readonly
42
43
  */
43
44
  presets = [];
44
45
 
45
46
  /**
46
47
  * The soundfont's samples
47
48
  * @type {BasicSample[]}
49
+ * @readonly
48
50
  */
49
51
  samples = [];
50
52
 
51
53
  /**
52
54
  * The soundfont's instruments
53
55
  * @type {BasicInstrument[]}
56
+ * @readonly
54
57
  */
55
58
  instruments = [];
56
59
 
@@ -202,40 +205,137 @@ class BasicSoundBank
202
205
  }
203
206
 
204
207
  /**
205
- * @param preset {BasicPreset}
208
+ * @param presets {BasicPreset}
206
209
  */
207
- addPresets(...preset)
210
+ addPresets(...presets)
208
211
  {
209
- this.presets.push(...preset);
212
+ this.presets.push(...presets);
210
213
  }
211
214
 
212
- flush()
215
+ /**
216
+ * @param instruments {BasicInstrument}
217
+ */
218
+ addInstruments(...instruments)
213
219
  {
214
- this.presets.sort((a, b) =>
220
+ this.instruments.push(...instruments);
221
+ }
222
+
223
+ /**
224
+ * @param samples {BasicSample}
225
+ */
226
+ addSamples(...samples)
227
+ {
228
+ this.samples.push(...samples);
229
+ }
230
+
231
+ /**
232
+ * Clones samples into this bank
233
+ * @param sample {BasicSample} samples to copy
234
+ * @returns {BasicSample} copied sample, if a sample exists with that name, it is returned instead
235
+ */
236
+ cloneSample(sample)
237
+ {
238
+ const duplicate = this.samples.find(s => s.sampleName === sample.sampleName);
239
+ if (duplicate)
215
240
  {
216
- if (a.bank !== b.bank)
217
- {
218
- return a.bank - b.bank;
219
- }
220
- return a.program - b.program;
221
- });
222
- this._parseInternal();
241
+ return duplicate;
242
+ }
243
+ const newSample = new BasicSample(
244
+ sample.sampleName,
245
+ sample.sampleRate,
246
+ sample.samplePitch,
247
+ sample.samplePitchCorrection,
248
+ sample.sampleType,
249
+ sample.sampleLoopStartIndex,
250
+ sample.sampleLoopEndIndex
251
+ );
252
+ if (sample.isCompressed)
253
+ {
254
+ newSample.setCompressedData(sample.compressedData.slice());
255
+ console.log(sample.compressedData.length);
256
+ }
257
+ else
258
+ {
259
+ newSample.setAudioData(sample.getAudioData());
260
+ }
261
+ this.addSamples(newSample);
262
+ if (sample.linkedSample)
263
+ {
264
+ const clonedLinked = this.cloneSample(sample.linkedSample);
265
+ newSample.setLinkedSample(clonedLinked, newSample.sampleType);
266
+ }
267
+ return newSample;
223
268
  }
224
269
 
270
+
225
271
  /**
272
+ * Clones an instruments into this bank
226
273
  * @param instrument {BasicInstrument}
274
+ * @returns {BasicInstrument} the copied instrument, if an instrument exists with that name, it is returned instead
227
275
  */
228
- addInstruments(...instrument)
276
+ cloneInstrument(instrument)
229
277
  {
230
- this.instruments.push(...instrument);
278
+ const duplicate = this.instruments.find(i => i.instrumentName === instrument.instrumentName);
279
+ if (duplicate)
280
+ {
281
+ return duplicate;
282
+ }
283
+ const newInstrument = new BasicInstrument();
284
+ newInstrument.instrumentName = instrument.instrumentName;
285
+ newInstrument.globalZone.copyFrom(instrument.globalZone);
286
+ for (const zone of instrument.instrumentZones)
287
+ {
288
+ const copiedZone = newInstrument.createZone();
289
+ copiedZone.copyFrom(zone);
290
+ copiedZone.setSample(this.cloneSample(zone.sample));
291
+ }
292
+ this.addInstruments(newInstrument);
293
+ return newInstrument;
231
294
  }
232
295
 
296
+ // noinspection JSUnusedGlobalSymbols
233
297
  /**
234
- * @param sample {BasicSample}
298
+ * Clones presets into this sound bank
299
+ * @param preset {BasicPreset}
300
+ * @returns {BasicPreset} the copied preset, if a preset exists with that name, it is returned instead
235
301
  */
236
- addSamples(...sample)
302
+ clonePreset(preset)
303
+ {
304
+ const duplicate = this.presets.find(p => p.presetName === preset.presetName);
305
+ if (duplicate)
306
+ {
307
+ return duplicate;
308
+ }
309
+ const newPreset = new BasicPreset(this);
310
+ newPreset.presetName = preset.presetName;
311
+ newPreset.bank = preset.bank;
312
+ newPreset.program = preset.program;
313
+ newPreset.library = preset.library;
314
+ newPreset.genre = preset.genre;
315
+ newPreset.morphology = preset.morphology;
316
+ newPreset.globalZone.copyFrom(preset.globalZone);
317
+ for (const zone of preset.presetZones)
318
+ {
319
+ const copiedZone = newPreset.createZone();
320
+ copiedZone.copyFrom(zone);
321
+ copiedZone.setInstrument(this.cloneInstrument(zone.instrument));
322
+ }
323
+
324
+ this.addPresets(newPreset);
325
+ return newPreset;
326
+ }
327
+
328
+ flush()
237
329
  {
238
- this.samples.push(...sample);
330
+ this.presets.sort((a, b) =>
331
+ {
332
+ if (a.bank !== b.bank)
333
+ {
334
+ return a.bank - b.bank;
335
+ }
336
+ return a.program - b.program;
337
+ });
338
+ this._parseInternal();
239
339
  }
240
340
 
241
341
  /**
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { generatorTypes } from "./generator_types.js";
7
7
  import { Generator } from "./generator.js";
8
+ import { Modulator } from "./modulator.js";
8
9
 
9
10
  export class BasicZone
10
11
  {
@@ -136,7 +137,7 @@ export class BasicZone
136
137
  copyFrom(zone)
137
138
  {
138
139
  this.generators = [...zone.generators];
139
- this.modulators = [...zone.modulators];
140
+ this.modulators = zone.modulators.map(m => Modulator.copy(m));
140
141
  this.velRange = { ...zone.velRange };
141
142
  this.keyRange = { ...zone.keyRange };
142
143
  }
@@ -84,7 +84,7 @@ export function write(options = DEFAULT_WRITE_OPTIONS)
84
84
  */
85
85
  const infoArrays = [];
86
86
  this.soundFontInfo["ISFT"] = "SpessaSynth"; // ( ͡° ͜ʖ ͡°)
87
- if (options?.compress)
87
+ if (options?.compress || this.samples.some(s => s.isCompressed))
88
88
  {
89
89
  this.soundFontInfo["ifil"] = "3.0"; // set version to 3
90
90
  }
@@ -113,7 +113,6 @@ export function write(options = DEFAULT_WRITE_OPTIONS)
113
113
  }
114
114
  else if (type === "DMOD")
115
115
  {
116
-
117
116
  const mods = this.defaultModulators;
118
117
  SpessaSynthInfo(
119
118
  `%cWriting %c${mods.length}%c default modulators...`,
@@ -223,21 +223,15 @@ export class DLSSample extends BasicSample
223
223
 
224
224
  getRawData(allowVorbis = true)
225
225
  {
226
- if (this.dataOverriden)
226
+ if (this.dataOverriden || this.isCompressed)
227
227
  {
228
- return this.encodeS16LE();
228
+ return super.getRawData();
229
229
  }
230
- else
230
+ if (this.wFormatTag === W_FORMAT_TAG.PCM && this.bytesPerSample === 2)
231
231
  {
232
- if (this.compressedData && allowVorbis)
233
- {
234
- return this.compressedData;
235
- }
236
- if (this.wFormatTag === W_FORMAT_TAG.PCM && this.bytesPerSample === 2)
237
- {
238
- return this.rawData;
239
- }
240
- return this.encodeS16LE();
232
+ // copy straight away
233
+ return this.rawData;
241
234
  }
235
+ return this.encodeS16LE();
242
236
  }
243
237
  }
@@ -1,12 +1,16 @@
1
1
  import { RiffChunk } from "../basic_soundfont/riff_chunk.js";
2
2
  import { IndexedByteArray } from "../../utils/indexed_array.js";
3
3
  import { readLittleEndian, signedInt8 } from "../../utils/byte_functions/little_endian.js";
4
- import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js";
5
4
  import { SpessaSynthInfo, SpessaSynthWarn } from "../../utils/loggin.js";
6
5
  import { readBytesAsString } from "../../utils/byte_functions/string.js";
7
6
  import { BasicSample, sampleTypes } from "../basic_soundfont/basic_sample.js";
8
7
  import { consoleColors } from "../../utils/other.js";
9
8
 
9
+ /**
10
+ * samples.js
11
+ * purpose: parses soundfont samples
12
+ */
13
+
10
14
  export const SF3_BIT_FLIT = 0x10;
11
15
 
12
16
  export class SoundFontSample extends BasicSample
@@ -82,11 +86,9 @@ export class SoundFontSample extends BasicSample
82
86
  this.isCompressed = compressed;
83
87
  this.sampleName = sampleName;
84
88
  // in bytes
85
- this.sampleStartIndex = sampleStartIndex;
86
- this.sampleEndIndex = sampleEndIndex;
89
+ this.startByteOffset = sampleStartIndex;
90
+ this.endByteOffset = sampleEndIndex;
87
91
  this.sampleID = sampleIndex;
88
- // in bytes
89
- this.sampleLength = this.sampleEndIndex - this.sampleStartIndex;
90
92
  const smplStart = sampleDataArray.currentIndex;
91
93
 
92
94
  // three data types in:
@@ -96,14 +98,13 @@ export class SoundFontSample extends BasicSample
96
98
  if (this.isCompressed)
97
99
  {
98
100
  // correct loop points
99
- this.sampleLoopStartIndex += this.sampleStartIndex / 2;
100
- this.sampleLoopEndIndex += this.sampleStartIndex / 2;
101
- this.sampleLength = 99999999; // set to 999,999 before we decode it
101
+ this.sampleLoopStartIndex += this.startByteOffset / 2;
102
+ this.sampleLoopEndIndex += this.startByteOffset / 2;
102
103
 
103
104
  // copy the compressed data, it can be preserved during writing
104
105
  this.compressedData = sampleDataArray.slice(
105
- this.sampleStartIndex / 2 + smplStart,
106
- this.sampleEndIndex / 2 + smplStart
106
+ this.startByteOffset / 2 + smplStart,
107
+ this.endByteOffset / 2 + smplStart
107
108
  );
108
109
  }
109
110
  else
@@ -112,15 +113,15 @@ export class SoundFontSample extends BasicSample
112
113
  {
113
114
  // float32 array from SF2pack, copy directly
114
115
  this.sampleData = sampleDataArray.slice(
115
- this.sampleStartIndex / 2,
116
- this.sampleEndIndex / 2
116
+ this.startByteOffset / 2,
117
+ this.endByteOffset / 2
117
118
  );
118
119
  }
119
120
  else
120
121
  {
121
122
  // regular sf2 s16le
122
- this.s16leStart = smplStart + this.sampleStartIndex;
123
- this.s16leEnd = smplStart + this.sampleEndIndex;
123
+ this.s16leStart = smplStart + this.startByteOffset;
124
+ this.s16leEnd = smplStart + this.endByteOffset;
124
125
  this.sf2FileArrayHandle = sampleDataArray;
125
126
  }
126
127
 
@@ -150,53 +151,6 @@ export class SoundFontSample extends BasicSample
150
151
  }
151
152
  }
152
153
 
153
- /**
154
- * @private
155
- * Decode binary vorbis into a float32 pcm
156
- * @returns {Float32Array}
157
- */
158
- decodeVorbis()
159
- {
160
- if (this.sampleData)
161
- {
162
- return this.sampleData;
163
- }
164
- if (this.sampleLength < 1)
165
- {
166
- // eos, do not do anything
167
- return new Float32Array(0);
168
- }
169
- // get the compressed byte stream
170
- // reset array and being decoding
171
- try
172
- {
173
- /**
174
- * @type {{data: Float32Array[], error: (string|null), sampleRate: number, eof: boolean}}
175
- */
176
- const vorbis = stbvorbis.decode(this.compressedData);
177
- const decoded = vorbis.data[0];
178
- if (decoded === undefined)
179
- {
180
- SpessaSynthWarn(`Error decoding sample ${this.sampleName}: Vorbis decode returned undefined.`);
181
- return new Float32Array(0);
182
- }
183
- // clip
184
- // because vorbis can go above 1 sometimes
185
- for (let i = 0; i < decoded.length; i++)
186
- {
187
- // magic number is 32,767 / 32,768
188
- decoded[i] = Math.max(-1, Math.min(decoded[i], 0.999969482421875));
189
- }
190
- return decoded;
191
- }
192
- catch (e)
193
- {
194
- // do not error out, fill with silence
195
- SpessaSynthWarn(`Error decoding sample ${this.sampleName}: ${e}`);
196
- return new Float32Array(this.sampleLoopEndIndex + 1);
197
- }
198
- }
199
-
200
154
  /**
201
155
  * @param audioData {Float32Array}
202
156
  */
@@ -216,24 +170,24 @@ export class SoundFontSample extends BasicSample
216
170
  return this.sampleData;
217
171
  }
218
172
  // SF2Pack is decoded during load time
173
+ // SF3 is decoded in BasicSample
174
+ if (this.isCompressed)
175
+ {
176
+ return super.getAudioData();
177
+ }
219
178
 
220
179
  // start loading data if it is not loaded
221
- if (this.sampleLength < 1)
180
+ const byteLength = this.endByteOffset - this.startByteOffset;
181
+ if (byteLength < 1)
222
182
  {
223
- SpessaSynthWarn(`Invalid sample ${this.sampleName}! Invalid length: ${this.sampleLength}`);
183
+ SpessaSynthWarn(`Invalid sample ${this.sampleName}! Invalid length: ${byteLength}`);
224
184
  return new Float32Array(1);
225
185
  }
226
186
 
227
- if (this.isCompressed)
228
- {
229
- // SF3
230
- // if compressed, decode
231
- this.sampleData = this.decodeVorbis();
232
- return this.sampleData;
233
- }
187
+
234
188
  // SF2
235
189
  // read the sample data
236
- let audioData = new Float32Array(this.sampleLength / 2);
190
+ let audioData = new Float32Array(byteLength / 2);
237
191
  let convertedSigned16 = new Int16Array(
238
192
  this.sf2FileArrayHandle.buffer.slice(this.s16leStart, this.s16leEnd)
239
193
  );
@@ -255,22 +209,13 @@ export class SoundFontSample extends BasicSample
255
209
  */
256
210
  getRawData(allowVorbis = true)
257
211
  {
258
- if (this.dataOverriden)
212
+ if (this.dataOverriden || this.compressedData)
259
213
  {
260
- return this.encodeS16LE();
261
- }
262
- else
263
- {
264
- if (this.compressedData)
265
- {
266
- if (allowVorbis)
267
- {
268
- return this.compressedData;
269
- }
270
- return this.encodeS16LE();
271
- }
272
- return this.sf2FileArrayHandle.slice(this.s16leStart, this.s16leEnd);
214
+ // return vorbis or encode manually
215
+ return super.getRawData();
273
216
  }
217
+ // copy the smpl directly
218
+ return this.sf2FileArrayHandle.slice(this.s16leStart, this.s16leEnd);
274
219
  }
275
220
  }
276
221