spessasynth_lib 3.20.37 → 3.20.41
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/soundfont/read_sf2/modulators.d.ts +5 -0
- package/@types/synthetizer/synthetizer.d.ts +7 -0
- package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +12 -0
- package/package.json +1 -1
- package/soundfont/read_sf2/modulators.js +15 -0
- package/synthetizer/synthetizer.js +17 -0
- package/synthetizer/worklet_processor.min.js +9 -9
- package/synthetizer/worklet_system/main_processor.js +3 -1
- package/synthetizer/worklet_system/worklet_methods/controller_control.js +15 -2
- package/synthetizer/worklet_system/worklet_methods/note_on.js +6 -0
- package/synthetizer/worklet_system/worklet_methods/voice_control.js +1 -1
- package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +12 -0
- package/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +13 -7
- package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +12 -0
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +7 -14
|
@@ -49,7 +49,9 @@ import { getWorkletVoices } from './worklet_utilities/worklet_voice.js'
|
|
|
49
49
|
* purpose: manages the synthesizer (and worklet sequencer) from the AudioWorkletGlobalScope and renders the audio data
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// if the note is released faster than that, it forced to last that long
|
|
53
|
+
// this is used mostly for drum channels, where a lot of midis like to send instant note off after a note on
|
|
54
|
+
export const MIN_NOTE_LENGTH = 0.03;
|
|
53
55
|
|
|
54
56
|
export const SYNTHESIZER_GAIN = 1.0;
|
|
55
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { consoleColors } from '../../../utils/other.js'
|
|
2
2
|
import { midiControllers } from '../../../midi_parser/midi_message.js'
|
|
3
|
-
import { dataEntryStates } from '../worklet_utilities/worklet_processor_channel.js'
|
|
3
|
+
import { channelConfiguration, dataEntryStates } from '../worklet_utilities/worklet_processor_channel.js'
|
|
4
4
|
import { computeModulators } from '../worklet_utilities/worklet_modulator.js'
|
|
5
5
|
import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
6
6
|
import { SYNTHESIZER_GAIN } from '../main_processor.js'
|
|
@@ -8,7 +8,7 @@ import { DEFAULT_PERCUSSION } from '../../synthetizer.js'
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @param channel {number}
|
|
11
|
-
* @param controllerNumber {
|
|
11
|
+
* @param controllerNumber {number}
|
|
12
12
|
* @param controllerValue {number}
|
|
13
13
|
* @param force {boolean}
|
|
14
14
|
* @this {SpessaSynthProcessor}
|
|
@@ -24,6 +24,19 @@ export function controllerChange(channel, controllerNumber, controllerValue, for
|
|
|
24
24
|
SpessaSynthWarn(`Trying to access channel ${channel} which does not exist... ignoring!`);
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
+
if(controllerNumber > 127)
|
|
28
|
+
{
|
|
29
|
+
// channel configuration. force must be set to true
|
|
30
|
+
if(!force) return;
|
|
31
|
+
switch (controllerNumber)
|
|
32
|
+
{
|
|
33
|
+
default:
|
|
34
|
+
return;
|
|
35
|
+
|
|
36
|
+
case channelConfiguration.velocityOverride:
|
|
37
|
+
channelObject.velocityOverride = controllerValue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
27
40
|
// lsb controller values: append them as the lower nibble of the 14 bit value
|
|
28
41
|
// excluding bank select and data entry as it's handled separately
|
|
29
42
|
if(
|
|
@@ -43,6 +43,12 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
|
|
|
43
43
|
sentMidiNote = this.tunings[program]?.[midiNote].midiNote;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// velocity override
|
|
47
|
+
if(channelObject.velocityOverride > 0)
|
|
48
|
+
{
|
|
49
|
+
velocity = channelObject.velocityOverride;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
// get voices
|
|
47
53
|
const voices = this.getWorkletVoices(
|
|
48
54
|
channel,
|
|
@@ -16,7 +16,7 @@ import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
const HALF_PI = Math.PI / 2;
|
|
19
|
-
export const PAN_SMOOTHING_FACTOR = 0.
|
|
19
|
+
export const PAN_SMOOTHING_FACTOR = 0.05;
|
|
20
20
|
/**
|
|
21
21
|
* Renders a voice to the stereo output buffer
|
|
22
22
|
* @param channel {WorkletProcessorChannel} the voice's channel
|
|
@@ -57,6 +57,10 @@ export function getSampleLinear(voice, outputBuffer)
|
|
|
57
57
|
}
|
|
58
58
|
else
|
|
59
59
|
{
|
|
60
|
+
if(sample.loopingMode === 2 && !voice.isInRelease)
|
|
61
|
+
{
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
60
64
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
61
65
|
{
|
|
62
66
|
|
|
@@ -119,6 +123,10 @@ export function getSampleNearest(voice, outputBuffer)
|
|
|
119
123
|
}
|
|
120
124
|
else
|
|
121
125
|
{
|
|
126
|
+
if(sample.loopingMode === 2 && !voice.isInRelease)
|
|
127
|
+
{
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
122
130
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
123
131
|
{
|
|
124
132
|
|
|
@@ -198,6 +206,10 @@ export function getSampleCubic(voice, outputBuffer)
|
|
|
198
206
|
}
|
|
199
207
|
else
|
|
200
208
|
{
|
|
209
|
+
if(sample.loopingMode === 2 && !voice.isInRelease)
|
|
210
|
+
{
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
201
213
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
202
214
|
{
|
|
203
215
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { modulatorSources } from '../../../soundfont/read_sf2/modulators.js'
|
|
1
|
+
import { Modulator, modulatorSources } from '../../../soundfont/read_sf2/modulators.js'
|
|
2
2
|
import { getModulatorCurveValue, MOD_PRECOMPUTED_LENGTH } from './modulator_curves.js'
|
|
3
3
|
import { NON_CC_INDEX_OFFSET } from './worklet_processor_channel.js'
|
|
4
4
|
import { generatorLimits, generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
@@ -21,6 +21,7 @@ export function computeWorkletModulator(controllerTable, modulator, voice)
|
|
|
21
21
|
{
|
|
22
22
|
if(modulator.transformAmount === 0)
|
|
23
23
|
{
|
|
24
|
+
modulator.currentValue = 0;
|
|
24
25
|
return 0;
|
|
25
26
|
}
|
|
26
27
|
// mapped to 0-16384
|
|
@@ -95,13 +96,15 @@ export function computeWorkletModulator(controllerTable, modulator, voice)
|
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
// compute the modulator
|
|
98
|
-
|
|
99
|
+
let computedValue = sourceValue * secondSrcValue * modulator.transformAmount;
|
|
99
100
|
|
|
100
101
|
if(modulator.transformType === 2)
|
|
101
102
|
{
|
|
102
103
|
// abs value
|
|
103
|
-
|
|
104
|
+
computedValue = Math.abs(computedValue);
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
modulator.currentValue = computedValue;
|
|
105
108
|
return computedValue;
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -113,7 +116,9 @@ export function computeWorkletModulator(controllerTable, modulator, voice)
|
|
|
113
116
|
* @param sourceIndex {number} enum for the source
|
|
114
117
|
*/
|
|
115
118
|
export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sourceIndex = 0) {
|
|
116
|
-
const
|
|
119
|
+
const modulators = voice.modulators;
|
|
120
|
+
const generators = voice.generators;
|
|
121
|
+
const modulatedGenerators = voice.modulatedGenerators;
|
|
117
122
|
|
|
118
123
|
// Modulation envelope is cheap to recalculate
|
|
119
124
|
// why here and not at the bottom?
|
|
@@ -158,13 +163,14 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou
|
|
|
158
163
|
{
|
|
159
164
|
// Reset this destination
|
|
160
165
|
modulatedGenerators[destination] = generators[destination];
|
|
161
|
-
//
|
|
166
|
+
// compute our modulator
|
|
167
|
+
computeWorkletModulator(controllerTable, mod, voice);
|
|
168
|
+
// sum the values of all modulators for this destination
|
|
162
169
|
modulators.forEach(m => {
|
|
163
170
|
if (m.modulatorDestination === destination)
|
|
164
171
|
{
|
|
165
172
|
const limits = generatorLimits[mod.modulatorDestination];
|
|
166
|
-
const
|
|
167
|
-
const newValue = current + computeWorkletModulator(controllerTable, m, voice);
|
|
173
|
+
const newValue = modulatedGenerators[mod.modulatorDestination] + m.currentValue;
|
|
168
174
|
modulatedGenerators[mod.modulatorDestination] = Math.max(limits.min, Math.min(newValue, limits.max));
|
|
169
175
|
}
|
|
170
176
|
});
|
|
@@ -11,6 +11,7 @@ import { modulatorSources } from '../../../soundfont/read_sf2/modulators.js'
|
|
|
11
11
|
* @property {Int16Array} keyCentTuning - tuning of individual keys in cents
|
|
12
12
|
* @property {boolean} holdPedal - indicates whether the hold pedal is active
|
|
13
13
|
* @property {boolean} drumChannel - indicates whether the channel is a drum channel
|
|
14
|
+
* @property {number} velocityOverride - overrides velocity if > 0 otherwise disabled
|
|
14
15
|
*
|
|
15
16
|
* @property {dataEntryStates} dataEntryState - the current state of the data entry
|
|
16
17
|
* @property {number} NRPCoarse - the current coarse value of the Non-Registered Parameter
|
|
@@ -62,6 +63,8 @@ export function createWorkletChannel(sendEvent = false)
|
|
|
62
63
|
channelOctaveTuning: new Int8Array(12),
|
|
63
64
|
keyCentTuning: new Int16Array(128),
|
|
64
65
|
channelVibrato: {delay: 0, depth: 0, rate: 0},
|
|
66
|
+
velocityOverride: 0,
|
|
67
|
+
|
|
65
68
|
lockGSNRPNParams: false,
|
|
66
69
|
holdPedal: false,
|
|
67
70
|
isMuted: false,
|
|
@@ -92,6 +95,7 @@ resetArray[midiControllers.expressionController] = 127 << 7;
|
|
|
92
95
|
resetArray[midiControllers.pan] = 64 << 7;
|
|
93
96
|
resetArray[midiControllers.releaseTime] = 64 << 7;
|
|
94
97
|
resetArray[midiControllers.brightness] = 64 << 7;
|
|
98
|
+
resetArray[midiControllers.timbreHarmonicContent] = 64 << 7;
|
|
95
99
|
resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = 8192;
|
|
96
100
|
resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = 2 << 7;
|
|
97
101
|
|
|
@@ -119,3 +123,11 @@ export const customControllers = {
|
|
|
119
123
|
export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length;
|
|
120
124
|
export const customResetArray = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE);
|
|
121
125
|
customResetArray[customControllers.modulationMultiplier] = 1;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* This is a channel configuration enum, it is internally sent from Synthetizer via controller change
|
|
129
|
+
* @enum {number}
|
|
130
|
+
*/
|
|
131
|
+
export const channelConfiguration = {
|
|
132
|
+
velocityOverride: 128, // overrides velocity for the given channel
|
|
133
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* purpose: prepares workletvoices from sample and generator data and manages sample dumping
|
|
4
4
|
* note: sample dumping means sending it over to the AudioWorkletGlobalScope
|
|
5
5
|
*/
|
|
6
|
+
import { Modulator } from '../../../soundfont/read_sf2/modulators.js'
|
|
6
7
|
|
|
7
8
|
class WorkletSample
|
|
8
9
|
{
|
|
@@ -86,8 +87,9 @@ class WorkletSample
|
|
|
86
87
|
* Looping mode of the sample:
|
|
87
88
|
* 0 - no loop
|
|
88
89
|
* 1 - loop
|
|
89
|
-
* 2 -
|
|
90
|
-
*
|
|
90
|
+
* 2 - UNOFFICIAL: polyphone 2.4 added start on release
|
|
91
|
+
* 3 - loop then play when released
|
|
92
|
+
* @type {0|1|2|3}
|
|
91
93
|
*/
|
|
92
94
|
loopingMode = 0;
|
|
93
95
|
|
|
@@ -298,7 +300,7 @@ class WorkletVoice
|
|
|
298
300
|
currentTime,
|
|
299
301
|
voice.targetKey,
|
|
300
302
|
voice.generators,
|
|
301
|
-
voice.modulators.
|
|
303
|
+
voice.modulators.map(m => Modulator.copy(m))
|
|
302
304
|
);
|
|
303
305
|
}
|
|
304
306
|
}
|
|
@@ -329,7 +331,7 @@ export function getWorkletVoices(channel,
|
|
|
329
331
|
const cached = channelObject.cachedVoices[midiNote][velocity];
|
|
330
332
|
if(cached !== undefined)
|
|
331
333
|
{
|
|
332
|
-
|
|
334
|
+
return cached.map(v => WorkletVoice.copy(v, currentTime));
|
|
333
335
|
}
|
|
334
336
|
else
|
|
335
337
|
{
|
|
@@ -371,15 +373,6 @@ export function getWorkletVoices(channel,
|
|
|
371
373
|
let loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2);
|
|
372
374
|
let loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2);
|
|
373
375
|
let loopingMode = generators[generatorTypes.sampleModes];
|
|
374
|
-
const sampleLength = sampleAndGenerators.sample.getAudioData().length;
|
|
375
|
-
// clamp loop
|
|
376
|
-
loopStart = Math.min(Math.max(0, loopStart), sampleLength);
|
|
377
|
-
// clamp loop
|
|
378
|
-
loopEnd = Math.min(Math.max(0, loopEnd), sampleLength);
|
|
379
|
-
if (loopEnd - loopStart < 1)
|
|
380
|
-
{
|
|
381
|
-
loopingMode = 0;
|
|
382
|
-
}
|
|
383
376
|
/**
|
|
384
377
|
* create the worklet sample
|
|
385
378
|
* offsets are calculated at note on time (to allow for modulation of them)
|
|
@@ -425,7 +418,7 @@ export function getWorkletVoices(channel,
|
|
|
425
418
|
currentTime,
|
|
426
419
|
targetKey,
|
|
427
420
|
generators,
|
|
428
|
-
sampleAndGenerators.modulators
|
|
421
|
+
sampleAndGenerators.modulators.map(m => Modulator.copy(m))
|
|
429
422
|
)
|
|
430
423
|
);
|
|
431
424
|
return voices;
|