spessasynth_lib 3.22.1 → 3.22.2

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.
@@ -49,6 +49,7 @@ import {
49
49
  import { applySynthesizerSnapshot, sendSynthesizerSnapshot } from "./worklet_methods/snapshot.js";
50
50
  import { WorkletSoundfontManager } from "./worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js";
51
51
  import { interpolationTypes } from "./worklet_utilities/wavetable_oscillator.js";
52
+ import { WorkletKeyModifierManager } from "./worklet_methods/worklet_key_modifier.js";
52
53
  import { getWorkletVoices } from "./worklet_utilities/worklet_voice.js";
53
54
  import { panVoice } from "./worklet_utilities/stereo_panner.js";
54
55
 
@@ -150,7 +151,13 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
150
151
  this.highPerformanceMode = false;
151
152
 
152
153
  /**
153
- * Overrides the main soundfont (embedded for example
154
+ * Handlese custom key overrides: velocity and preset
155
+ * @type {WorkletKeyModifierManager}
156
+ */
157
+ this.keyModifierManager = new WorkletKeyModifierManager();
158
+
159
+ /**
160
+ * Overrides the main soundfont (embedded for example)
154
161
  * @type {BasicSoundFont}
155
162
  */
156
163
  this.overrideSoundfont = undefined;
@@ -203,6 +203,10 @@ export function handleMessage(message)
203
203
  this.clearSoundFont(true, false);
204
204
  break;
205
205
 
206
+ case workletMessageType.keyModifierManager:
207
+ this.keyModifierManager.handleMessage(data[0], data[1]);
208
+ break;
209
+
206
210
  case workletMessageType.requestSynthesizerSnapshot:
207
211
  this.sendSynthesizerSnapshot();
208
212
  break;
@@ -27,6 +27,7 @@
27
27
  * @property {number} sequencerSpecific - 23 -> [messageType<WorkletSequencerMessageType> messageData<any>] note: refer to sequencer_message.js
28
28
  * @property {number} requestSynthesizerSnapshot - 24 -> (no data)
29
29
  * @property {number} setLogLevel - 25 -> [enableInfo<boolean>, enableWarning<boolean>, enableGroup<boolean>, enableTable<boolean>]
30
+ * @property {number} keyModifier - 26 -> [messageType<workletKeyModifierMessageType> messageData<any>]
30
31
  */
31
32
  export const workletMessageType = {
32
33
  noteOff: 0,
@@ -54,7 +55,8 @@ export const workletMessageType = {
54
55
  lockController: 22,
55
56
  sequencerSpecific: 23,
56
57
  requestSynthesizerSnapshot: 24,
57
- setLogLevel: 25
58
+ setLogLevel: 25,
59
+ keyModifierManager: 26
58
60
  };
59
61
 
60
62
  /**
@@ -83,6 +85,7 @@ export const ALL_CHANNELS_OR_DIFFERENT_ACTION = -1;
83
85
  * |boolean
84
86
  * |ArrayBuffer
85
87
  * |{messageType: WorkletSequencerMessageType, messageData: any}
88
+ * |{messageType: workletKeyModifierMessageType, messageData: any}
86
89
  * )
87
90
  * }} WorkletMessage
88
91
  */
@@ -50,6 +50,13 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
50
50
  velocity = channelObject.velocityOverride;
51
51
  }
52
52
 
53
+ // key velocity override
54
+ const keyVel = this.keyModifierManager.getVelocity(channel, midiNote);
55
+ if (keyVel > -1)
56
+ {
57
+ velocity = keyVel;
58
+ }
59
+
53
60
  // get voices
54
61
  const voices = this.getWorkletVoices(
55
62
  channel,
@@ -0,0 +1,141 @@
1
+ export class KeyModifier
2
+ {
3
+
4
+ /**
5
+ * The new override velocity. -1 means unchanged
6
+ * @type {number}
7
+ */
8
+ velocity = -1;
9
+ /**
10
+ * The patch this key uses. -1 on either means default
11
+ * @type {{bank: number, program: number}}
12
+ */
13
+ patch = { bank: -1, program: -1 };
14
+
15
+ /**
16
+ * @param velocity {number}
17
+ * @param bank {number}
18
+ * @param program {number}
19
+ */
20
+ constructor(velocity = -1, bank = -1, program = -1)
21
+ {
22
+ this.velocity = velocity;
23
+ this.patch = {
24
+ bank: bank,
25
+ program: program
26
+ };
27
+ }
28
+ }
29
+
30
+ /**
31
+ * @enum {number}
32
+ */
33
+ export const workletKeyModifierMessageType = {
34
+ addMapping: 0, // [channel<number, midiNote<number>, mapping<KeyModifier>]
35
+ deleteMapping: 1, // [channel<number, midiNote<number>]
36
+ clearMappings: 2 // <no data>
37
+ };
38
+
39
+ export class WorkletKeyModifierManager
40
+ {
41
+ /**
42
+ * The velocity override mappings for MIDI keys
43
+ * @type {KeyModifier[][]}
44
+ * @private
45
+ */
46
+ _keyMappings = [];
47
+
48
+ /**
49
+ * @param type {workletKeyModifierMessageType}
50
+ * @param data {any}
51
+ */
52
+ handleMessage(type, data)
53
+ {
54
+ switch (type)
55
+ {
56
+ default:
57
+ return;
58
+
59
+ case workletKeyModifierMessageType.addMapping:
60
+ this.addMapping(...data);
61
+ break;
62
+
63
+ case workletKeyModifierMessageType.clearMappings:
64
+ this.clearMappings();
65
+ break;
66
+
67
+ case workletKeyModifierMessageType.deleteMapping:
68
+ this.deleteMapping(...data);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @param channel {number}
74
+ * @param midiNote {number}
75
+ * @param mapping {KeyModifier}
76
+ */
77
+ addMapping(channel, midiNote, mapping)
78
+ {
79
+ if (this._keyMappings[channel] === undefined)
80
+ {
81
+ this._keyMappings[channel] = [];
82
+ }
83
+ this._keyMappings[channel][midiNote] = mapping;
84
+ }
85
+
86
+ deleteMapping(channel, midiNote)
87
+ {
88
+ if (this._keyMappings[channel]?.[midiNote] === undefined)
89
+ {
90
+ return;
91
+ }
92
+ this._keyMappings[channel][midiNote] = undefined;
93
+ }
94
+
95
+ clearMappings()
96
+ {
97
+ this._keyMappings = [];
98
+ }
99
+
100
+ /**
101
+ * @param channel {number}
102
+ * @param midiNote {number}
103
+ * @returns {number} velocity, -1 if unchanged
104
+ */
105
+ getVelocity(channel, midiNote)
106
+ {
107
+ const modifier = this._keyMappings[channel]?.[midiNote];
108
+ if (modifier)
109
+ {
110
+ return modifier.velocity;
111
+ }
112
+ return -1;
113
+ }
114
+
115
+ /**
116
+ * @param channel {number}
117
+ * @param midiNote {number}
118
+ * @returns {boolean}
119
+ */
120
+ hasOverridePatch(channel, midiNote)
121
+ {
122
+ const bank = this._keyMappings[channel]?.[midiNote]?.patch?.bank;
123
+ return bank !== undefined && bank > 0;
124
+ }
125
+
126
+ /**
127
+ * @param channel {number}
128
+ * @param midiNote {number}
129
+ * @returns {{bank: number, program: number}} -1 if unchanged
130
+ */
131
+ getPatch(channel, midiNote)
132
+ {
133
+ const modifier = this._keyMappings[channel]?.[midiNote];
134
+ if (modifier)
135
+ {
136
+ return modifier.patch;
137
+ }
138
+ throw new Error("No modifier.");
139
+ }
140
+
141
+ }
@@ -152,7 +152,6 @@ export class WorkletLowpassFilter
152
152
 
153
153
 
154
154
  // code is ported from https://github.com/sinshu/meltysynth/ to work with js.
155
- // I'm too dumb to understand the math behind this...
156
155
  let w = 2 * Math.PI * filter.cutoffHz / sampleRate; // we're in the audioworkletglobalscope so we can use sampleRate
157
156
  let cosw = Math.cos(w);
158
157
  let alpha = Math.sin(w) / (2 * filter.reasonanceGain);
@@ -333,112 +333,126 @@ export function getWorkletVoices(channel,
333
333
  let workletVoices;
334
334
 
335
335
  const cached = channelObject.cachedVoices[midiNote][velocity];
336
- if (cached !== undefined)
336
+
337
+ // override patch
338
+ const overridePatch = this.keyModifierManager.hasOverridePatch(channel, midiNote);
339
+
340
+ // overriden patch is not cached
341
+ if (cached !== undefined && !overridePatch)
337
342
  {
338
343
  return cached.map(v => WorkletVoice.copy(v, currentTime));
339
344
  }
340
- else
345
+
346
+ // not cached...
347
+ let canCache = true;
348
+ let preset = channelObject.preset;
349
+ if (overridePatch)
341
350
  {
342
- const preset = channelObject.preset;
343
- /**
344
- * @returns {WorkletVoice[]}
345
- */
346
- workletVoices = preset.getSamplesAndGenerators(midiNote, velocity)
347
- .reduce((voices, sampleAndGenerators) =>
351
+ canCache = false;
352
+ const patchNum = this.keyModifierManager.getPatch(channel, midiNote);
353
+ preset = this.soundfontManager.getPreset(patchNum.bank, patchNum.program);
354
+ }
355
+ /**
356
+ * @returns {WorkletVoice[]}
357
+ */
358
+ workletVoices = preset.getSamplesAndGenerators(midiNote, velocity)
359
+ .reduce((voices, sampleAndGenerators) =>
360
+ {
361
+ if (sampleAndGenerators.sample.sampleData === undefined)
348
362
  {
349
- if (sampleAndGenerators.sample.sampleData === undefined)
350
- {
351
- SpessaSynthWarn(`Discarding invalid sample: ${sampleAndGenerators.sample.sampleName}`);
352
- return voices;
353
- }
354
-
355
- // create the generator list
356
- const generators = new Int16Array(60);
357
- // apply and sum the gens
358
- for (let i = 0; i < 60; i++)
359
- {
360
- generators[i] = addAndClampGenerator(
361
- i,
362
- sampleAndGenerators.presetGenerators,
363
- sampleAndGenerators.instrumentGenerators
364
- );
365
- }
366
-
367
- // !! EMU initial attenuation correction, multiply initial attenuation by 0.4
368
- generators[generatorTypes.initialAttenuation] = Math.floor(generators[generatorTypes.initialAttenuation] * 0.4);
369
-
370
- // key override
371
- let rootKey = sampleAndGenerators.sample.samplePitch;
372
- if (generators[generatorTypes.overridingRootKey] > -1)
373
- {
374
- rootKey = generators[generatorTypes.overridingRootKey];
375
- }
376
-
377
- let targetKey = midiNote;
378
- if (generators[generatorTypes.keyNum] > -1)
379
- {
380
- targetKey = generators[generatorTypes.keyNum];
381
- }
382
-
383
- // determine looping mode now. if the loop is too small, disable
384
- let loopStart = sampleAndGenerators.sample.sampleLoopStartIndex;
385
- let loopEnd = sampleAndGenerators.sample.sampleLoopEndIndex;
386
- let loopingMode = generators[generatorTypes.sampleModes];
387
- /**
388
- * create the worklet sample
389
- * offsets are calculated at note on time (to allow for modulation of them)
390
- * @type {WorkletSample}
391
- */
392
- const workletSample = new WorkletSample(
393
- sampleAndGenerators.sample.getAudioData(),
394
- (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(
395
- 2,
396
- sampleAndGenerators.sample.samplePitchCorrection / 1200
397
- ), // cent tuning
398
- 0,
399
- rootKey,
400
- loopStart,
401
- loopEnd,
402
- Math.floor(sampleAndGenerators.sample.sampleData.length) - 1,
403
- loopingMode
404
- );
405
- // velocity override
406
- if (generators[generatorTypes.velocity] > -1)
407
- {
408
- velocity = generators[generatorTypes.velocity];
409
- }
410
-
411
- if (debug)
412
- {
413
- SpessaSynthTable([{
414
- Sample: sampleAndGenerators.sample.sampleName,
415
- Generators: generators,
416
- Modulators: sampleAndGenerators.modulators.map(m => m.debugString()),
417
- Velocity: velocity,
418
- TargetKey: targetKey,
419
- MidiNote: midiNote,
420
- WorkletSample: workletSample
421
- }]);
422
- }
423
-
424
-
425
- voices.push(
426
- new WorkletVoice(
427
- sampleRate,
428
- workletSample,
429
- midiNote,
430
- velocity,
431
- channel,
432
- currentTime,
433
- targetKey,
434
- realKey,
435
- generators,
436
- sampleAndGenerators.modulators.map(m => Modulator.copy(m))
437
- )
438
- );
363
+ SpessaSynthWarn(`Discarding invalid sample: ${sampleAndGenerators.sample.sampleName}`);
439
364
  return voices;
440
- }, []);
441
- // cache the voice
365
+ }
366
+
367
+ // create the generator list
368
+ const generators = new Int16Array(60);
369
+ // apply and sum the gens
370
+ for (let i = 0; i < 60; i++)
371
+ {
372
+ generators[i] = addAndClampGenerator(
373
+ i,
374
+ sampleAndGenerators.presetGenerators,
375
+ sampleAndGenerators.instrumentGenerators
376
+ );
377
+ }
378
+
379
+ // !! EMU initial attenuation correction, multiply initial attenuation by 0.4
380
+ generators[generatorTypes.initialAttenuation] = Math.floor(generators[generatorTypes.initialAttenuation] * 0.4);
381
+
382
+ // key override
383
+ let rootKey = sampleAndGenerators.sample.samplePitch;
384
+ if (generators[generatorTypes.overridingRootKey] > -1)
385
+ {
386
+ rootKey = generators[generatorTypes.overridingRootKey];
387
+ }
388
+
389
+ let targetKey = midiNote;
390
+ if (generators[generatorTypes.keyNum] > -1)
391
+ {
392
+ targetKey = generators[generatorTypes.keyNum];
393
+ }
394
+
395
+ // determine looping mode now. if the loop is too small, disable
396
+ let loopStart = sampleAndGenerators.sample.sampleLoopStartIndex;
397
+ let loopEnd = sampleAndGenerators.sample.sampleLoopEndIndex;
398
+ let loopingMode = generators[generatorTypes.sampleModes];
399
+ /**
400
+ * create the worklet sample
401
+ * offsets are calculated at note on time (to allow for modulation of them)
402
+ * @type {WorkletSample}
403
+ */
404
+ const workletSample = new WorkletSample(
405
+ sampleAndGenerators.sample.getAudioData(),
406
+ (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(
407
+ 2,
408
+ sampleAndGenerators.sample.samplePitchCorrection / 1200
409
+ ), // cent tuning
410
+ 0,
411
+ rootKey,
412
+ loopStart,
413
+ loopEnd,
414
+ Math.floor(sampleAndGenerators.sample.sampleData.length) - 1,
415
+ loopingMode
416
+ );
417
+ // velocity override
418
+ if (generators[generatorTypes.velocity] > -1)
419
+ {
420
+ velocity = generators[generatorTypes.velocity];
421
+ }
422
+
423
+ if (debug)
424
+ {
425
+ SpessaSynthTable([{
426
+ Sample: sampleAndGenerators.sample.sampleName,
427
+ Generators: generators,
428
+ Modulators: sampleAndGenerators.modulators.map(m => m.debugString()),
429
+ Velocity: velocity,
430
+ TargetKey: targetKey,
431
+ MidiNote: midiNote,
432
+ WorkletSample: workletSample
433
+ }]);
434
+ }
435
+
436
+
437
+ voices.push(
438
+ new WorkletVoice(
439
+ sampleRate,
440
+ workletSample,
441
+ midiNote,
442
+ velocity,
443
+ channel,
444
+ currentTime,
445
+ targetKey,
446
+ realKey,
447
+ generators,
448
+ sampleAndGenerators.modulators.map(m => Modulator.copy(m))
449
+ )
450
+ );
451
+ return voices;
452
+ }, []);
453
+ // cache the voice
454
+ if (canCache)
455
+ {
442
456
  channelObject.cachedVoices[midiNote][velocity] = workletVoices.map(v => WorkletVoice.copy(v, currentTime));
443
457
  }
444
458
  return workletVoices;