spessasynth_lib 3.20.35 → 3.20.40
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/dls/dls_sample.d.ts +7 -1
- package/@types/soundfont/read_sf2/modulators.d.ts +5 -0
- package/@types/synthetizer/synthetizer.d.ts +8 -1
- package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +12 -0
- package/package.json +1 -1
- package/soundfont/dls/dls_sample.js +10 -1
- package/soundfont/dls/read_region.js +10 -2
- package/soundfont/dls/read_samples.js +7 -3
- package/soundfont/read_sf2/generators.js +4 -4
- package/soundfont/read_sf2/modulators.js +15 -0
- package/synthetizer/synthetizer.js +18 -1
- package/synthetizer/worklet_processor.min.js +11 -11
- package/synthetizer/worklet_system/main_processor.js +4 -2
- package/synthetizer/worklet_system/worklet_methods/controller_control.js +15 -2
- package/synthetizer/worklet_system/worklet_methods/note_on.js +34 -1
- package/synthetizer/worklet_system/worklet_methods/voice_control.js +10 -6
- package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +1 -4
- package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +3 -17
- package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +94 -10
- package/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +15 -7
- package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +12 -0
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +10 -8
|
@@ -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
|
|
|
@@ -84,7 +86,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
84
86
|
* Interpolation type used
|
|
85
87
|
* @type {interpolationTypes}
|
|
86
88
|
*/
|
|
87
|
-
this.interpolationType = interpolationTypes.
|
|
89
|
+
this.interpolationType = interpolationTypes.fourthOrder;
|
|
88
90
|
|
|
89
91
|
/**
|
|
90
92
|
* @type {function}
|
|
@@ -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,
|
|
@@ -59,6 +65,7 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
|
|
|
59
65
|
const exclusive = voice.generators[generatorTypes.exclusiveClass];
|
|
60
66
|
if(exclusive !== 0)
|
|
61
67
|
{
|
|
68
|
+
// kill all voices with the same exclusive class
|
|
62
69
|
channelVoices.forEach(v => {
|
|
63
70
|
if(v.generators[generatorTypes.exclusiveClass] === exclusive)
|
|
64
71
|
{
|
|
@@ -72,7 +79,33 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
|
|
|
72
79
|
}
|
|
73
80
|
// compute all modulators
|
|
74
81
|
computeModulators(voice, channelObject.midiControllers);
|
|
75
|
-
|
|
82
|
+
// modulate sample offsets (these are not real time)
|
|
83
|
+
const cursorStartOffset = voice.modulatedGenerators[generatorTypes.startAddrsOffset] + voice.modulatedGenerators[generatorTypes.startAddrsCoarseOffset] * 32768;
|
|
84
|
+
const endOffset = voice.modulatedGenerators[generatorTypes.endAddrOffset] + voice.modulatedGenerators[generatorTypes.endAddrsCoarseOffset] * 32768;
|
|
85
|
+
const loopStartOffset = voice.modulatedGenerators[generatorTypes.startloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.startloopAddrsCoarseOffset] * 32768;
|
|
86
|
+
const loopEndOffset = voice.modulatedGenerators[generatorTypes.endloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.endloopAddrsCoarseOffset] * 32768;
|
|
87
|
+
const sm = voice.sample;
|
|
88
|
+
// apply them
|
|
89
|
+
const clamp = num => Math.max(0, Math.min(sm.sampleData.length - 1, num));
|
|
90
|
+
sm.cursor = clamp( sm.cursor + cursorStartOffset);
|
|
91
|
+
sm.end = clamp(sm.end + endOffset);
|
|
92
|
+
sm.loopStart = clamp(sm.loopStart + loopStartOffset);
|
|
93
|
+
sm.loopEnd = clamp(sm.loopEnd + loopEndOffset);
|
|
94
|
+
// swap loops if needed
|
|
95
|
+
if(sm.loopEnd < sm.loopStart)
|
|
96
|
+
{
|
|
97
|
+
const temp = sm.loopStart;
|
|
98
|
+
sm.loopStart = sm.loopEnd;
|
|
99
|
+
sm.loopEnd = temp;
|
|
100
|
+
}
|
|
101
|
+
if (sm.loopEnd - sm.loopStart < 1)
|
|
102
|
+
{
|
|
103
|
+
sm.loopingMode = 0;
|
|
104
|
+
sm.isLooping = false;
|
|
105
|
+
}
|
|
106
|
+
// set the current attenuation to target,
|
|
107
|
+
// as it's interpolated (we don't want 0 attenuation for even a split second)
|
|
108
|
+
voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTarget;
|
|
76
109
|
// set initial pan to avoid split second changing from middle to the correct value
|
|
77
110
|
voice.currentPan = ((Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) // 0 to 1
|
|
78
111
|
});
|
|
@@ -4,6 +4,7 @@ import { getLFOValue } from '../worklet_utilities/lfo.js'
|
|
|
4
4
|
import { customControllers } from '../worklet_utilities/worklet_processor_channel.js'
|
|
5
5
|
import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js'
|
|
6
6
|
import {
|
|
7
|
+
getSampleCubic,
|
|
7
8
|
getSampleLinear,
|
|
8
9
|
getSampleNearest,
|
|
9
10
|
interpolationTypes,
|
|
@@ -15,7 +16,7 @@ import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
const HALF_PI = Math.PI / 2;
|
|
18
|
-
export const PAN_SMOOTHING_FACTOR = 0.
|
|
19
|
+
export const PAN_SMOOTHING_FACTOR = 0.05;
|
|
19
20
|
/**
|
|
20
21
|
* Renders a voice to the stereo output buffer
|
|
21
22
|
* @param channel {WorkletProcessorChannel} the voice's channel
|
|
@@ -40,10 +41,14 @@ export function renderVoice(
|
|
|
40
41
|
// if not in release, check if the release time is
|
|
41
42
|
if (currentTime >= voice.releaseStartTime)
|
|
42
43
|
{
|
|
43
|
-
|
|
44
|
+
// release the voice here
|
|
44
45
|
voice.isInRelease = true;
|
|
45
46
|
WorkletVolumeEnvelope.startRelease(voice);
|
|
46
47
|
WorkletModulationEnvelope.startRelease(voice);
|
|
48
|
+
if(voice.sample.loopingMode === 3)
|
|
49
|
+
{
|
|
50
|
+
voice.sample.isLooping = false;
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
@@ -163,6 +168,9 @@ export function renderVoice(
|
|
|
163
168
|
case interpolationTypes.nearestNeighbor:
|
|
164
169
|
getSampleNearest(voice, bufferOut);
|
|
165
170
|
break;
|
|
171
|
+
|
|
172
|
+
case interpolationTypes.fourthOrder:
|
|
173
|
+
getSampleCubic(voice, bufferOut);
|
|
166
174
|
}
|
|
167
175
|
|
|
168
176
|
// lowpass filter
|
|
@@ -264,8 +272,4 @@ export function releaseVoice(voice)
|
|
|
264
272
|
{
|
|
265
273
|
voice.releaseStartTime = voice.startTime + MIN_NOTE_LENGTH;
|
|
266
274
|
}
|
|
267
|
-
if(voice.sample.loopingMode === 3)
|
|
268
|
-
{
|
|
269
|
-
voice.sample.isLooping = false;
|
|
270
|
-
}
|
|
271
275
|
}
|
|
@@ -140,10 +140,7 @@ export class WorkletLowpassFilter
|
|
|
140
140
|
filter.cutoffHz = absCentsToHz(filter.cutoffCents);
|
|
141
141
|
|
|
142
142
|
// fix cutoff on low frequencies (fluid_iir_filter.c line 392)
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
filter.cutoffHz = 0.45 * sampleRate;
|
|
146
|
-
}
|
|
143
|
+
filter.cutoffHz = Math.min(filter.cutoffHz, 0.45 * sampleRate);
|
|
147
144
|
|
|
148
145
|
// adjust the filterQ (fluid_iir_filter.c line 204)
|
|
149
146
|
const qDb = (filter.reasonanceCb / 10) - 3.01;
|
|
@@ -10,7 +10,8 @@ export const VOLUME_ENVELOPE_SMOOTHING_FACTOR = 0.001;
|
|
|
10
10
|
|
|
11
11
|
const DB_SILENCE = 100;
|
|
12
12
|
const PERCEIVED_DB_SILENCE = 90;
|
|
13
|
-
|
|
13
|
+
// around 96 dB of attenuation
|
|
14
|
+
const PERCEIVED_GAIN_SILENCE = 0.000015; // can't go lower than that (see #50)
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* VOL ENV STATES:
|
|
@@ -147,22 +148,11 @@ export class WorkletVolumeEnvelope
|
|
|
147
148
|
WorkletVolumeEnvelope.recalculate(voice);
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
/**
|
|
151
|
-
* Initializes a volume envelope
|
|
152
|
-
* @param voice {WorkletVoice}
|
|
153
|
-
*/
|
|
154
|
-
static intialize(voice)
|
|
155
|
-
{
|
|
156
|
-
WorkletVolumeEnvelope.recalculate(voice, true);
|
|
157
|
-
voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTarget;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
151
|
/**
|
|
161
152
|
* Recalculates the envelope
|
|
162
153
|
* @param voice {WorkletVoice} the voice this envelope belongs to
|
|
163
|
-
* @param setupInterpolated {boolean} if we should initialize the interpolated values (attenuation and sustain)
|
|
164
154
|
*/
|
|
165
|
-
static recalculate(voice
|
|
155
|
+
static recalculate(voice)
|
|
166
156
|
{
|
|
167
157
|
const env = voice.volumeEnvelope;
|
|
168
158
|
const timecentsToSamples = tc =>
|
|
@@ -172,10 +162,6 @@ export class WorkletVolumeEnvelope
|
|
|
172
162
|
// calculate absolute times (they can change so we have to recalculate every time
|
|
173
163
|
env.attenuationTarget = Math.max(0, Math.min(voice.modulatedGenerators[generatorTypes.initialAttenuation], 1440)) / 10; // divide by ten to get decibels
|
|
174
164
|
env.sustainDbRelative = Math.min(DB_SILENCE, voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10);
|
|
175
|
-
if(setupInterpolated)
|
|
176
|
-
{
|
|
177
|
-
env.attenuation = env.attenuationTarget;
|
|
178
|
-
}
|
|
179
165
|
const sustainDb = Math.min(DB_SILENCE, env.sustainDbRelative);
|
|
180
166
|
|
|
181
167
|
// calculate durations
|
|
@@ -57,11 +57,6 @@ export function getSampleLinear(voice, outputBuffer)
|
|
|
57
57
|
}
|
|
58
58
|
else
|
|
59
59
|
{
|
|
60
|
-
// check and correct end errors
|
|
61
|
-
if(sample.end >= sampleData.length)
|
|
62
|
-
{
|
|
63
|
-
sample.end = sampleData.length - 1;
|
|
64
|
-
}
|
|
65
60
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
66
61
|
{
|
|
67
62
|
|
|
@@ -124,11 +119,6 @@ export function getSampleNearest(voice, outputBuffer)
|
|
|
124
119
|
}
|
|
125
120
|
else
|
|
126
121
|
{
|
|
127
|
-
// check and correct end errors
|
|
128
|
-
if(sample.end >= sampleData.length)
|
|
129
|
-
{
|
|
130
|
-
sample.end = sampleData.length - 1;
|
|
131
|
-
}
|
|
132
122
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
133
123
|
{
|
|
134
124
|
|
|
@@ -148,4 +138,98 @@ export function getSampleNearest(voice, outputBuffer)
|
|
|
148
138
|
}
|
|
149
139
|
}
|
|
150
140
|
sample.cursor = cur;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fills the output buffer with raw sample data using cubic interpolation
|
|
147
|
+
* @param voice {WorkletVoice} the voice we're working on
|
|
148
|
+
* @param outputBuffer {Float32Array} the output buffer to write to
|
|
149
|
+
*/
|
|
150
|
+
export function getSampleCubic(voice, outputBuffer)
|
|
151
|
+
{
|
|
152
|
+
const sample = voice.sample;
|
|
153
|
+
let cur = sample.cursor;
|
|
154
|
+
const sampleData = sample.sampleData;
|
|
155
|
+
|
|
156
|
+
if(sample.isLooping)
|
|
157
|
+
{
|
|
158
|
+
const loopLength = sample.loopEnd - sample.loopStart;
|
|
159
|
+
for (let i = 0; i < outputBuffer.length; i++)
|
|
160
|
+
{
|
|
161
|
+
// check for loop
|
|
162
|
+
while(cur >= sample.loopEnd)
|
|
163
|
+
{
|
|
164
|
+
cur -= loopLength;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// math comes from
|
|
168
|
+
// https://stackoverflow.com/questions/1125666/how-do-you-do-bicubic-or-other-non-linear-interpolation-of-re-sampled-audio-da
|
|
169
|
+
|
|
170
|
+
// grab the 4 points
|
|
171
|
+
const y0 = ~~cur; // point before the cursor. twice bitwise not is just a faster Math.floor
|
|
172
|
+
let y1 = y0 + 1; // point after the cursor
|
|
173
|
+
let y2 = y1 + 1; // point 1 after the cursor
|
|
174
|
+
let y3 = y2 + 1; // point 2 after the cursor
|
|
175
|
+
const t = cur - y0; // distance from y0 to cursor
|
|
176
|
+
// y0 is not handled here
|
|
177
|
+
// as it's math.floor of cur which is handled above
|
|
178
|
+
if(y1 >= sample.loopEnd) y1 -= loopLength;
|
|
179
|
+
if(y2 >= sample.loopEnd) y2 -= loopLength;
|
|
180
|
+
if(y3 >= sample.loopEnd) y3 -= loopLength;
|
|
181
|
+
|
|
182
|
+
// grab the samples
|
|
183
|
+
const x0 = sampleData[y0];
|
|
184
|
+
const x1 = sampleData[y1];
|
|
185
|
+
const x2 = sampleData[y2];
|
|
186
|
+
const x3 = sampleData[y3];
|
|
187
|
+
|
|
188
|
+
// interpolate
|
|
189
|
+
// const c0 = x1
|
|
190
|
+
const c1 = 0.5 * (x2 - x0);
|
|
191
|
+
const c2 = x0 - (2.5 * x1) + (2 * x2) - (0.5 * x3);
|
|
192
|
+
const c3 = (0.5 * (x3 - x0)) + (1.5 * (x1 - x2));
|
|
193
|
+
outputBuffer[i] = (((((c3 * t) + c2) * t) + c1) * t) + x1;
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
cur += sample.playbackStep * voice.currentTuningCalculated;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else
|
|
200
|
+
{
|
|
201
|
+
for (let i = 0; i < outputBuffer.length; i++)
|
|
202
|
+
{
|
|
203
|
+
|
|
204
|
+
// math comes from
|
|
205
|
+
// https://stackoverflow.com/questions/1125666/how-do-you-do-bicubic-or-other-non-linear-interpolation-of-re-sampled-audio-da
|
|
206
|
+
|
|
207
|
+
// grab the 4 points
|
|
208
|
+
const y0 = ~~cur; // point before the cursor. twice bitwise not is just a faster Math.floor
|
|
209
|
+
let y1 = y0 + 1; // point after the cursor
|
|
210
|
+
let y2 = y1 + 1; // point 1 after the cursor
|
|
211
|
+
let y3 = y2 + 1; // point 2 after the cursor
|
|
212
|
+
const t = cur - y0; // distance from y0 to cursor
|
|
213
|
+
|
|
214
|
+
// flag as finished if needed
|
|
215
|
+
if(y1 >= sample.end ||
|
|
216
|
+
y2 >= sample.end ||
|
|
217
|
+
y3 >= sample.end) {voice.finished = true; return;}
|
|
218
|
+
|
|
219
|
+
// grab the samples
|
|
220
|
+
const x0 = sampleData[y0];
|
|
221
|
+
const x1 = sampleData[y1];
|
|
222
|
+
const x2 = sampleData[y2];
|
|
223
|
+
const x3 = sampleData[y3];
|
|
224
|
+
|
|
225
|
+
// interpolate
|
|
226
|
+
const c1 = 0.5 * (x2 - x0);
|
|
227
|
+
const c2 = x0 - (2.5 * x1) + (2 * x2) - (0.5 * x3);
|
|
228
|
+
const c3 = (0.5 * (x3 - x0)) + (1.5 * (x1 - x2));
|
|
229
|
+
outputBuffer[i] = (((((c3 * t) + c2) * t) + c1) * t) + x1;
|
|
230
|
+
|
|
231
|
+
cur += sample.playbackStep * voice.currentTuningCalculated;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
voice.sample.cursor = cur;
|
|
151
235
|
}
|
|
@@ -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,9 +116,13 @@ 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
|
|
124
|
+
// why here and not at the bottom?
|
|
125
|
+
// I dunno, seems to work fine
|
|
119
126
|
WorkletModulationEnvelope.recalculate(voice);
|
|
120
127
|
|
|
121
128
|
if (sourceUsesCC === -1)
|
|
@@ -156,13 +163,14 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou
|
|
|
156
163
|
{
|
|
157
164
|
// Reset this destination
|
|
158
165
|
modulatedGenerators[destination] = generators[destination];
|
|
159
|
-
//
|
|
166
|
+
// compute our modulator
|
|
167
|
+
computeWorkletModulator(controllerTable, mod, voice);
|
|
168
|
+
// sum the values of all modulators for this destination
|
|
160
169
|
modulators.forEach(m => {
|
|
161
170
|
if (m.modulatorDestination === destination)
|
|
162
171
|
{
|
|
163
172
|
const limits = generatorLimits[mod.modulatorDestination];
|
|
164
|
-
const
|
|
165
|
-
const newValue = current + computeWorkletModulator(controllerTable, m, voice);
|
|
173
|
+
const newValue = modulatedGenerators[mod.modulatorDestination] + m.currentValue;
|
|
166
174
|
modulatedGenerators[mod.modulatorDestination] = Math.max(limits.min, Math.min(newValue, limits.max));
|
|
167
175
|
}
|
|
168
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
|
{
|
|
@@ -298,7 +299,7 @@ class WorkletVoice
|
|
|
298
299
|
currentTime,
|
|
299
300
|
voice.targetKey,
|
|
300
301
|
voice.generators,
|
|
301
|
-
voice.modulators.
|
|
302
|
+
voice.modulators.map(m => Modulator.copy(m))
|
|
302
303
|
);
|
|
303
304
|
}
|
|
304
305
|
}
|
|
@@ -329,7 +330,7 @@ export function getWorkletVoices(channel,
|
|
|
329
330
|
const cached = channelObject.cachedVoices[midiNote][velocity];
|
|
330
331
|
if(cached !== undefined)
|
|
331
332
|
{
|
|
332
|
-
|
|
333
|
+
return cached.map(v => WorkletVoice.copy(v, currentTime));
|
|
333
334
|
}
|
|
334
335
|
else
|
|
335
336
|
{
|
|
@@ -368,8 +369,8 @@ export function getWorkletVoices(channel,
|
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
// determine looping mode now. if the loop is too small, disable
|
|
371
|
-
let loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2)
|
|
372
|
-
let loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2)
|
|
372
|
+
let loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2);
|
|
373
|
+
let loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2);
|
|
373
374
|
let loopingMode = generators[generatorTypes.sampleModes];
|
|
374
375
|
const sampleLength = sampleAndGenerators.sample.getAudioData().length;
|
|
375
376
|
// clamp loop
|
|
@@ -381,17 +382,18 @@ export function getWorkletVoices(channel,
|
|
|
381
382
|
loopingMode = 0;
|
|
382
383
|
}
|
|
383
384
|
/**
|
|
384
|
-
* create the worklet sample
|
|
385
|
+
* create the worklet sample
|
|
386
|
+
* offsets are calculated at note on time (to allow for modulation of them)
|
|
385
387
|
* @type {WorkletSample}
|
|
386
388
|
*/
|
|
387
389
|
const workletSample = new WorkletSample(
|
|
388
390
|
sampleAndGenerators.sample.getAudioData(),
|
|
389
391
|
(sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(2, sampleAndGenerators.sample.samplePitchCorrection / 1200), // cent tuning
|
|
390
|
-
|
|
392
|
+
0,
|
|
391
393
|
rootKey,
|
|
392
394
|
loopStart,
|
|
393
395
|
loopEnd,
|
|
394
|
-
Math.floor( sampleAndGenerators.sample.sampleData.length) - 1
|
|
396
|
+
Math.floor( sampleAndGenerators.sample.sampleData.length) - 1,
|
|
395
397
|
loopingMode
|
|
396
398
|
)
|
|
397
399
|
// velocity override
|
|
@@ -424,7 +426,7 @@ export function getWorkletVoices(channel,
|
|
|
424
426
|
currentTime,
|
|
425
427
|
targetKey,
|
|
426
428
|
generators,
|
|
427
|
-
sampleAndGenerators.modulators
|
|
429
|
+
sampleAndGenerators.modulators.map(m => Modulator.copy(m))
|
|
428
430
|
)
|
|
429
431
|
);
|
|
430
432
|
return voices;
|