spessasynth_lib 3.20.11 → 3.20.15

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.
@@ -4,138 +4,310 @@
4
4
  * note: sample dumping means sending it over to the AudioWorkletGlobalScope
5
5
  */
6
6
 
7
- /**
8
- * @typedef {Object} WorkletSample
9
- * @property {number} sampleID - ID of the sample
10
- * @property {number} playbackStep - current playback step (rate)
11
- * @property {number} cursor - current position in the sample
12
- * @property {number} rootKey - root key of the sample
13
- * @property {number} loopStart - start position of the loop
14
- * @property {number} loopEnd - end position of the loop
15
- * @property {number} end - end position of the sample
16
- * @property {0|1|2} loopingMode - looping mode of the sample
17
- */
7
+ class WorkletSample
8
+ {
9
+ /**
10
+ * @param data {Float32Array}
11
+ * @param playbackStep {number} the playback step, a single increment
12
+ * @param cursorStart {number} the sample id which starts the playback
13
+ * @param rootKey {number} MIDI root key
14
+ * @param loopStart {number} loop start index
15
+ * @param loopEnd {number} loop end index
16
+ * @param endIndex {number} sample end index (for end offset)
17
+ * @param loopingMode {number} sample looping mode
18
+ */
19
+ constructor(
20
+ data,
21
+ playbackStep,
22
+ cursorStart,
23
+ rootKey,
24
+ loopStart,
25
+ loopEnd,
26
+ endIndex,
27
+ loopingMode
28
+ )
29
+ {
30
+ this.sampleData = data;
31
+ this.playbackStep = playbackStep;
32
+ this.cursor = cursorStart;
33
+ this.rootKey = rootKey;
34
+ this.loopStart = loopStart;
35
+ this.loopEnd = loopEnd;
36
+ this.end = endIndex;
37
+ this.loopingMode = loopingMode;
38
+ }
39
+ /**
40
+ * the sample's audio data
41
+ * @type {Float32Array}
42
+ */
43
+ sampleData;
18
44
 
19
- /**
20
- * @typedef {Object} WorkletVoice
21
- * @property {WorkletSample} sample - sample ID for voice.
22
- * @property {WorkletLowpassFilter} filter - lowpass filter applied to the voice
23
- * @property {Int16Array} generators - the unmodulated (constant) generators of the voice
24
- * @property {Modulator[]} modulators - the voice's modulators. Grouped by the destination
25
- * @property {Int16Array} modulatedGenerators - the generators modulated by the modulators
26
- *
27
- * @property {boolean} finished - indicates if the voice has finished
28
- * @property {boolean} isInRelease - indicates if the voice is in the release phase
29
- *
30
- * @property {number} channelNumber - MIDI channel number
31
- * @property {number} velocity - velocity of the note
32
- * @property {number} midiNote - MIDI note number
33
- * @property {number} pressure - the pressure of the note
34
- * @property {number} targetKey - target key for the note
35
- *
36
- * @property {WorkletModulationEnvelope} modulationEnvelope
37
- * @property {WorkletVolumeEnvelope} volumeEnvelope
38
- *
39
- * @property {number} startTime - start time of the voice absolute
40
- * @property {number} releaseStartTime - start time of the release phase absolute
41
- *
42
- * @property {number} currentTuningCents - current tuning adjustment in cents
43
- * @property {number} currentTuningCalculated - calculated tuning adjustment
44
- * @property {number} currentPan - from 0 to 1
45
- */
45
+ /**
46
+ * Current playback step (rate)
47
+ * @type {number}
48
+ */
49
+ playbackStep = 0;
50
+
51
+ /**
52
+ * Current position in the sample
53
+ * @type {number}
54
+ */
55
+ cursor = 0;
56
+
57
+ /**
58
+ * MIDI root key of the sample
59
+ * @type {number}
60
+ */
61
+ rootKey = 0;
62
+
63
+ /**
64
+ * Start position of the loop
65
+ * @type {number}
66
+ */
67
+ loopStart = 0;
68
+
69
+ /**
70
+ * End position of the loop
71
+ * @type {number}
72
+ */
73
+ loopEnd = 0;
74
+
75
+ /**
76
+ * End position of the sample
77
+ * @type {number}
78
+ */
79
+ end = 0;
80
+
81
+ /**
82
+ * Looping mode of the sample:
83
+ * 0 - no loop
84
+ * 1 - loop
85
+ * 2 - loop then play when released
86
+ * @type {0|1|2}
87
+ */
88
+ loopingMode = 0;
89
+ }
46
90
 
47
91
  import { addAndClampGenerator, generatorTypes } from '../../../soundfont/read_sf2/generators.js'
48
92
  import { SpessaSynthTable, SpessaSynthWarn } from '../../../utils/loggin.js'
49
- import { DEFAULT_WORKLET_LOWPASS_FILTER } from './lowpass_filter.js'
93
+ import { WorkletLowpassFilter } from './lowpass_filter.js'
50
94
  import { WorkletVolumeEnvelope } from './volume_envelope.js'
51
95
  import { WorkletModulationEnvelope } from './modulation_envelope.js'
52
96
 
53
97
 
54
98
  /**
55
- * the sampleID is the index
56
- * @type {boolean[]}
57
- */
58
- let globalDumpedSamplesList = [];
59
-
60
- export function clearSamplesList()
61
- {
62
- globalDumpedSamplesList = [];
63
- }
64
-
65
- function /**
66
- * @param channel {number} channel hint for the processor to recalculate cursor positions
67
- * @param sample {LoadedSample}
68
- * @param id {number}
69
- * @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
99
+ * WorkletVoice represents a single instance of the
100
+ * SoundFont2 synthesis model.
101
+ * That is:
102
+ * A wavetable oscillator (sample)
103
+ * A volume envelope (volumeEnvelope)
104
+ * A modulation envelope (modulationEnvelope)
105
+ * Generators (generators and modulatedGenerators)
106
+ * Modulators (modulators)
107
+ * And MIDI params such as channel, MIDI note, velocity
70
108
  */
71
- dumpSample(channel, sample, id, sampleDumpCallback)
109
+ class WorkletVoice
72
110
  {
73
- // flag as defined, so it's currently being dumped
74
- globalDumpedSamplesList[id] = false;
75
-
76
- // load the data
77
- sampleDumpCallback({
78
- channel: channel,
79
- sampleID: id,
80
- sampleData: sample.getAudioData()
81
- });
82
- globalDumpedSamplesList[id] = true;
83
- }
111
+ /**
112
+ * Creates a new voice
113
+ * @param sampleRate {number}
114
+ * @param workletSample {WorkletSample}
115
+ * @param midiNote {number}
116
+ * @param velocity {number}
117
+ * @param channel {number}
118
+ * @param currentTime {number}
119
+ * @param targetKey {number}
120
+ * @param generators {Int16Array}
121
+ * @param modulators {Modulator[]}
122
+ */
123
+ constructor(
124
+ sampleRate,
125
+ workletSample,
126
+ midiNote,
127
+ velocity,
128
+ channel,
129
+ currentTime,
130
+ targetKey,
131
+ generators,
132
+ modulators,
133
+ )
134
+ {
135
+ this.sample = workletSample;
136
+ this.generators = generators;
137
+ this.modulatedGenerators = new Int16Array(generators);
138
+ this.modulators = modulators;
84
139
 
85
- /**
86
- * Deep clone function for the WorkletVoice object and its nested structures.
87
- * This function handles Int16Array, objects, arrays, and primitives.
88
- * It does not handle circular references.
89
- * @template T
90
- * @param {T} obj - The object to clone.
91
- * @returns {T} - Cloned object.
92
- */
93
- function deepClone(obj) {
94
- if (obj === null || typeof obj !== 'object') {
95
- return obj;
140
+ this.velocity = velocity;
141
+ this.midiNote = midiNote;
142
+ this.channelNumber = channel;
143
+ this.startTime = currentTime;
144
+ this.targetKey = targetKey;
145
+ this.volumeEnvelope = new WorkletVolumeEnvelope(sampleRate);
96
146
  }
147
+ /**
148
+ * Sample ID for voice.
149
+ * @type {WorkletSample}
150
+ */
151
+ sample;
97
152
 
98
- // Handle Int16Array separately
99
- if (obj instanceof Int16Array) {
100
- return new Int16Array(obj);
101
- }
153
+ /**
154
+ * Lowpass filter applied to the voice.
155
+ * @type {WorkletLowpassFilter}
156
+ */
157
+ filter = new WorkletLowpassFilter();
102
158
 
103
- // Handle objects and arrays
104
- const clonedObj = Array.isArray(obj) ? [] : {};
105
- for (let key in obj) {
106
- if (obj.hasOwnProperty(key)) {
107
- if (typeof obj[key] === 'object' && obj[key] !== null) {
108
- clonedObj[key] = deepClone(obj[key]); // Recursively clone nested objects
109
- } else if (obj[key] instanceof Int16Array) {
110
- clonedObj[key] = new Int16Array(obj[key]); // Clone Int16Array
111
- } else {
112
- clonedObj[key] = obj[key]; // Copy primitives
113
- }
114
- }
159
+ /**
160
+ * The unmodulated (constant) generators of the voice.
161
+ * @type {Int16Array}
162
+ */
163
+ generators;
164
+
165
+ /**
166
+ * The voice's modulators.
167
+ * Grouped by the destination.
168
+ * @type {Modulator[]}
169
+ */
170
+ modulators = [];
171
+
172
+ /**
173
+ * The generators modulated by the modulators.
174
+ * @type {Int16Array}
175
+ */
176
+ modulatedGenerators;
177
+
178
+ /**
179
+ * Indicates if the voice has finished.
180
+ * @type {boolean}
181
+ */
182
+ finished = false;
183
+
184
+ /**
185
+ * Indicates if the voice is in the release phase.
186
+ * @type {boolean}
187
+ */
188
+ isInRelease = false;
189
+
190
+ /**
191
+ * MIDI channel number.
192
+ * @type {number}
193
+ */
194
+ channelNumber = 0;
195
+
196
+ /**
197
+ * Velocity of the note.
198
+ * @type {number}
199
+ */
200
+ velocity = 0;
201
+
202
+ /**
203
+ * MIDI note number.
204
+ * @type {number}
205
+ */
206
+ midiNote = 0;
207
+
208
+ /**
209
+ * The pressure of the note.
210
+ * @type {number}
211
+ */
212
+ pressure = 0;
213
+
214
+ /**
215
+ * Target key for the note.
216
+ * @type {number}
217
+ */
218
+ targetKey = 0;
219
+
220
+ /**
221
+ * Modulation envelope.
222
+ * @type {WorkletModulationEnvelope}
223
+ */
224
+ modulationEnvelope = new WorkletModulationEnvelope();
225
+
226
+ /**
227
+ * Volume envelope.
228
+ * @type {WorkletVolumeEnvelope}
229
+ */
230
+ volumeEnvelope;
231
+
232
+ /**
233
+ * Start time of the voice absolute.
234
+ * @type {number}
235
+ */
236
+ startTime = 0;
237
+
238
+ /**
239
+ * Start time of the release phase absolute.
240
+ * @type {number}
241
+ */
242
+ releaseStartTime = Infinity;
243
+
244
+ /**
245
+ * Current tuning adjustment in cents.
246
+ * @type {number}
247
+ */
248
+ currentTuningCents = 0;
249
+
250
+ /**
251
+ * Calculated tuning adjustment.
252
+ * @type {number}
253
+ */
254
+ currentTuningCalculated = 1;
255
+
256
+ /**
257
+ * From 0 to 1.
258
+ * @type {number}
259
+ */
260
+ currentPan = 0.5;
261
+
262
+ /**
263
+ * Copies a workletVoice instance
264
+ * @param voice {WorkletVoice}
265
+ * @param currentTime {number}
266
+ * @returns WorkletVoice
267
+ */
268
+ static copy(voice, currentTime)
269
+ {
270
+ const sampleToCopy = voice.sample;
271
+ const sample = new WorkletSample(
272
+ sampleToCopy.sampleData,
273
+ sampleToCopy.playbackStep,
274
+ sampleToCopy.cursor,
275
+ sampleToCopy.rootKey,
276
+ sampleToCopy.loopStart,
277
+ sampleToCopy.loopEnd,
278
+ sampleToCopy.end,
279
+ sampleToCopy.loopingMode
280
+ )
281
+ return new WorkletVoice(
282
+ voice.volumeEnvelope.sampleRate,
283
+ sample,
284
+ voice.midiNote,
285
+ voice.velocity,
286
+ voice.channelNumber,
287
+ currentTime,
288
+ voice.targetKey,
289
+ voice.generators,
290
+ voice.modulators.slice()
291
+ );
115
292
  }
116
- return clonedObj;
117
293
  }
118
294
 
119
295
  /**
120
296
  * @param channel {number} a hint for the processor to recalculate sample cursors when sample dumping
121
297
  * @param midiNote {number}
122
298
  * @param velocity {number}
123
- * @param preset {BasicPreset}
299
+ * @param channelObject {WorkletProcessorChannel}
124
300
  * @param currentTime {number}
125
- * @param sampleRate {number}
126
- * @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
127
- * @param cachedVoices {WorkletVoice[][][]} first is midi note, second is velocity. output is an array of WorkletVoices
301
+ * output is an array of WorkletVoices
128
302
  * @param debug {boolean}
303
+ * @this {SpessaSynthProcessor}
129
304
  * @returns {WorkletVoice[]}
130
305
  */
131
306
  export function getWorkletVoices(channel,
132
307
  midiNote,
133
308
  velocity,
134
- preset,
309
+ channelObject,
135
310
  currentTime,
136
- sampleRate,
137
- sampleDumpCallback,
138
- cachedVoices,
139
311
  debug=false)
140
312
  {
141
313
  /**
@@ -143,26 +315,19 @@ export function getWorkletVoices(channel,
143
315
  */
144
316
  let workletVoices;
145
317
 
146
- const cached = cachedVoices[midiNote][velocity];
318
+ const cached = channelObject.cachedVoices[midiNote][velocity];
147
319
  if(cached !== undefined)
148
320
  {
149
- workletVoices = cached.map(deepClone);
150
- workletVoices.forEach(v => {
151
- v.startTime = currentTime;
152
- });
321
+ workletVoices = cached.map(v => WorkletVoice.copy(v, currentTime));
153
322
  }
154
323
  else
155
324
  {
325
+ const preset = channelObject.preset;
156
326
  /**
157
327
  * @returns {WorkletVoice[]}
158
328
  */
159
- workletVoices = preset.getSamplesAndGenerators(midiNote, velocity).reduce((voices, sampleAndGenerators) => {
160
- // dump the sample if haven't already
161
- const sampleID = sampleAndGenerators.sampleID + preset.sampleIDOffset;
162
- if (globalDumpedSamplesList[sampleID] !== true)
163
- {
164
- dumpSample(channel, sampleAndGenerators.sample, sampleID, sampleDumpCallback);
165
- }
329
+ workletVoices = preset.getSamplesAndGenerators(midiNote, velocity)
330
+ .reduce((voices, sampleAndGenerators) => {
166
331
  if(sampleAndGenerators.sample.sampleData === undefined)
167
332
  {
168
333
  SpessaSynthWarn(`Discarding invalid sample: ${sampleAndGenerators.sample.sampleName}`);
@@ -204,25 +369,23 @@ export function getWorkletVoices(channel,
204
369
  {
205
370
  loopingMode = 0;
206
371
  }
207
-
208
- // determine end
209
372
  /**
210
- * create the worklet sample
373
+ * create the worklet sample and calculate offsets
211
374
  * @type {WorkletSample}
212
375
  */
213
- const workletSample = {
214
- sampleID: sampleID,
215
- playbackStep: (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(2, sampleAndGenerators.sample.samplePitchCorrection / 1200),// cent tuning
216
- cursor: generators[generatorTypes.startAddrsOffset] + (generators[generatorTypes.startAddrsCoarseOffset] * 32768),
217
- rootKey: rootKey,
218
- loopStart: loopStart,
219
- loopEnd: loopEnd,
220
- end: Math.floor( sampleAndGenerators.sample.sampleData.length) - 1 + (generators[generatorTypes.endAddrOffset] + (generators[generatorTypes.endAddrsCoarseOffset] * 32768)),
221
- loopingMode: loopingMode
222
- };
223
-
376
+ const workletSample = new WorkletSample(
377
+ sampleAndGenerators.sample.getAudioData(),
378
+ (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(2, sampleAndGenerators.sample.samplePitchCorrection / 1200), // cent tuning
379
+ generators[generatorTypes.startAddrsOffset] + (generators[generatorTypes.startAddrsCoarseOffset] * 32768),
380
+ rootKey,
381
+ loopStart,
382
+ loopEnd,
383
+ Math.floor( sampleAndGenerators.sample.sampleData.length) - 1 + (generators[generatorTypes.endAddrOffset] + (generators[generatorTypes.endAddrsCoarseOffset] * 32768)),
384
+ loopingMode
385
+ )
224
386
  // velocity override
225
- if (generators[generatorTypes.velocity] > -1) {
387
+ if (generators[generatorTypes.velocity] > -1)
388
+ {
226
389
  velocity = generators[generatorTypes.velocity];
227
390
  }
228
391
 
@@ -240,38 +403,23 @@ export function getWorkletVoices(channel,
240
403
  }
241
404
 
242
405
 
243
- voices.push({
244
- filter: deepClone(DEFAULT_WORKLET_LOWPASS_FILTER),
245
- // generators and modulators
246
- generators: generators,
247
- modulators: sampleAndGenerators.modulators,
248
- modulatedGenerators: new Int16Array(generators),
249
-
250
- // sample and playback data
251
- sample: workletSample,
252
- velocity: velocity,
253
- midiNote: midiNote,
254
- pressure: 0,
255
- channelNumber: channel,
256
- startTime: currentTime,
257
- targetKey: targetKey,
258
- currentTuningCalculated: 1,
259
- currentTuningCents: 0,
260
- releaseStartTime: Infinity,
261
-
262
- // envelope data
263
- finished: false,
264
- isInRelease: false,
265
- currentPan: 0.5,
266
-
267
- volumeEnvelope: new WorkletVolumeEnvelope(sampleRate),
268
- modulationEnvelope: new WorkletModulationEnvelope()
269
- });
406
+ voices.push(
407
+ new WorkletVoice(
408
+ sampleRate,
409
+ workletSample,
410
+ midiNote,
411
+ velocity,
412
+ channel,
413
+ currentTime,
414
+ targetKey,
415
+ generators,
416
+ sampleAndGenerators.modulators
417
+ )
418
+ );
270
419
  return voices;
271
420
  }, []);
272
421
  // cache the voice
273
- // clone it so the system won't mess with it!
274
- cachedVoices[midiNote][velocity] = workletVoices.map(deepClone);
422
+ channelObject.cachedVoices[midiNote][velocity] = workletVoices.map(v => WorkletVoice.copy(v, currentTime));
275
423
  }
276
424
  return workletVoices;
277
425
  }