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.
- package/@types/synthetizer/key_modifier_manager.d.ts +42 -0
- package/@types/synthetizer/synthetizer.d.ts +6 -0
- package/@types/synthetizer/worklet_system/message_protocol/worklet_message.d.ts +4 -0
- package/@types/synthetizer/worklet_system/worklet_methods/worklet_key_modifier.d.ts +69 -0
- package/package.json +1 -1
- package/sequencer/worklet_sequencer/song_control.js +0 -0
- package/soundfont/basic_soundfont/write_sf2/write.js +1 -1
- package/soundfont/dls/dls_soundfont.js +0 -0
- package/soundfont/dls/read_instrument.js +0 -0
- package/soundfont/read_sf2/samples.js +0 -0
- package/soundfont/read_sf2/soundfont.js +0 -0
- package/synthetizer/key_modifier_manager.js +73 -0
- package/synthetizer/synthetizer.js +7 -0
- package/synthetizer/worklet_processor.min.js +10 -10
- package/synthetizer/worklet_system/main_processor.js +8 -1
- package/synthetizer/worklet_system/message_protocol/handle_message.js +4 -0
- package/synthetizer/worklet_system/message_protocol/worklet_message.js +4 -1
- package/synthetizer/worklet_system/worklet_methods/note_on.js +7 -0
- package/synthetizer/worklet_system/worklet_methods/worklet_key_modifier.js +141 -0
- package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +0 -1
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +114 -100
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
345
|
+
|
|
346
|
+
// not cached...
|
|
347
|
+
let canCache = true;
|
|
348
|
+
let preset = channelObject.preset;
|
|
349
|
+
if (overridePatch)
|
|
341
350
|
{
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|