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.
- package/midi_parser/midi_editor.js +1 -1
- package/package.json +1 -1
- package/sequencer/worklet_sequencer/song_control.js +1 -5
- package/soundfont/read_sf2/generators.js +1 -1
- package/synthetizer/worklet_processor.min.js +10 -10
- package/synthetizer/worklet_system/main_processor.js +18 -12
- package/synthetizer/worklet_system/worklet_methods/note_on.js +2 -10
- package/synthetizer/worklet_system/worklet_methods/program_control.js +15 -41
- package/synthetizer/worklet_system/worklet_methods/voice_control.js +4 -4
- package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +156 -112
- package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +7 -17
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +306 -158
|
@@ -4,138 +4,310 @@
|
|
|
4
4
|
* note: sample dumping means sending it over to the AudioWorkletGlobalScope
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 {
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
109
|
+
class WorkletVoice
|
|
72
110
|
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Lowpass filter applied to the voice.
|
|
155
|
+
* @type {WorkletLowpassFilter}
|
|
156
|
+
*/
|
|
157
|
+
filter = new WorkletLowpassFilter();
|
|
102
158
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
299
|
+
* @param channelObject {WorkletProcessorChannel}
|
|
124
300
|
* @param currentTime {number}
|
|
125
|
-
*
|
|
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
|
-
|
|
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(
|
|
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)
|
|
160
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
rootKey
|
|
218
|
-
loopStart
|
|
219
|
-
loopEnd
|
|
220
|
-
|
|
221
|
-
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
}
|