spessasynth_core 3.27.2 → 3.27.4
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/package.json +1 -1
- package/src/soundfont/basic_soundfont/basic_sample.js +1 -1
- package/src/soundfont/basic_soundfont/modulator.js +31 -17
- package/src/synthetizer/audio_engine/engine_components/compute_modulator.js +7 -0
- package/src/synthetizer/audio_engine/engine_components/soundfont_manager.js +0 -3
- package/src/synthetizer/audio_engine/engine_components/voice.js +6 -3
- package/src/synthetizer/audio_engine/engine_components/wavetable_oscillator.js +80 -59
- package/src/synthetizer/audio_engine/engine_methods/render_voice.js +16 -15
package/package.json
CHANGED
|
@@ -73,6 +73,13 @@ export class Modulator
|
|
|
73
73
|
*/
|
|
74
74
|
isEffectModulator = false;
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The default resonant modulator does not affect the filter gain.
|
|
78
|
+
* Neither XG nor GS responded to cc #74 in that way.
|
|
79
|
+
* @type {boolean}
|
|
80
|
+
*/
|
|
81
|
+
isDefaultResonanceModulator = false;
|
|
82
|
+
|
|
76
83
|
/**
|
|
77
84
|
* 1 if the source is bipolar (min is -1, max is 1)
|
|
78
85
|
* otherwise min is 0 and max is 1
|
|
@@ -198,7 +205,7 @@ export class Modulator
|
|
|
198
205
|
*/
|
|
199
206
|
static copy(modulator)
|
|
200
207
|
{
|
|
201
|
-
|
|
208
|
+
const m = new Modulator(
|
|
202
209
|
modulator.sourceIndex,
|
|
203
210
|
modulator.sourceCurveType,
|
|
204
211
|
modulator.sourceUsesCC,
|
|
@@ -214,6 +221,8 @@ export class Modulator
|
|
|
214
221
|
modulator.transformType,
|
|
215
222
|
modulator.isEffectModulator
|
|
216
223
|
);
|
|
224
|
+
m.isDefaultResonanceModulator = modulator.isDefaultResonanceModulator;
|
|
225
|
+
return m;
|
|
217
226
|
}
|
|
218
227
|
|
|
219
228
|
/**
|
|
@@ -313,7 +322,7 @@ export class Modulator
|
|
|
313
322
|
*/
|
|
314
323
|
sumTransform(modulator)
|
|
315
324
|
{
|
|
316
|
-
|
|
325
|
+
const m = new Modulator(
|
|
317
326
|
this.sourceIndex,
|
|
318
327
|
this.sourceCurveType,
|
|
319
328
|
this.sourceUsesCC,
|
|
@@ -329,6 +338,8 @@ export class Modulator
|
|
|
329
338
|
this.transformType,
|
|
330
339
|
this.isEffectModulator
|
|
331
340
|
);
|
|
341
|
+
m.isDefaultResonanceModulator = modulator.isDefaultResonanceModulator;
|
|
342
|
+
return m;
|
|
332
343
|
}
|
|
333
344
|
}
|
|
334
345
|
|
|
@@ -531,23 +542,26 @@ const customModulators = [
|
|
|
531
542
|
generatorTypes.initialFilterFc,
|
|
532
543
|
6000,
|
|
533
544
|
0
|
|
534
|
-
),
|
|
535
|
-
|
|
536
|
-
// cc 71 (filter Q) to filter Q
|
|
537
|
-
new DecodedModulator(
|
|
538
|
-
getModSourceEnum(
|
|
539
|
-
modulatorCurveTypes.linear,
|
|
540
|
-
1,
|
|
541
|
-
0,
|
|
542
|
-
1,
|
|
543
|
-
midiControllers.filterResonance
|
|
544
|
-
), // linear forwards bipolar cc 74
|
|
545
|
-
0x0, // no controller
|
|
546
|
-
generatorTypes.initialFilterQ,
|
|
547
|
-
250,
|
|
548
|
-
0
|
|
549
545
|
)
|
|
546
|
+
|
|
550
547
|
];
|
|
548
|
+
// cc 71 (filter Q) to filter Q
|
|
549
|
+
const resonanceModulator = new DecodedModulator(
|
|
550
|
+
getModSourceEnum(
|
|
551
|
+
modulatorCurveTypes.linear,
|
|
552
|
+
1,
|
|
553
|
+
0,
|
|
554
|
+
1,
|
|
555
|
+
midiControllers.filterResonance
|
|
556
|
+
), // linear forwards bipolar cc 74
|
|
557
|
+
0x0, // no controller
|
|
558
|
+
generatorTypes.initialFilterQ,
|
|
559
|
+
250,
|
|
560
|
+
0
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
resonanceModulator.isDefaultResonanceModulator = true;
|
|
564
|
+
customModulators.push(resonanceModulator);
|
|
551
565
|
|
|
552
566
|
/**
|
|
553
567
|
* @type {Modulator[]}
|
|
@@ -113,6 +113,13 @@ export function computeModulator(controllerTable, modulator, voice)
|
|
|
113
113
|
computedValue = Math.abs(computedValue);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// resonant modulator: take its value and ensure that it won't change the final gain
|
|
117
|
+
if (modulator.isDefaultResonanceModulator)
|
|
118
|
+
{
|
|
119
|
+
// half the gain, negates the filter
|
|
120
|
+
voice.resonanceOffset = Math.max(0, computedValue / 2);
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
modulator.currentValue = computedValue;
|
|
117
124
|
return computedValue;
|
|
118
125
|
}
|
|
@@ -116,9 +116,6 @@ export class SoundFontManager
|
|
|
116
116
|
SpessaSynthInfo(`No soundfont with id of "${id}" found. Aborting!`);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
-
delete this.soundfontList[index].soundfont.presets;
|
|
120
|
-
delete this.soundfontList[index].soundfont.instruments;
|
|
121
|
-
delete this.soundfontList[index].soundfont.samples;
|
|
122
119
|
this.soundfontList.splice(index, 1);
|
|
123
120
|
this.generatePresetList();
|
|
124
121
|
}
|
|
@@ -143,6 +143,12 @@ class Voice
|
|
|
143
143
|
*/
|
|
144
144
|
modulators = [];
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Resonance offset, it is affected by the default resonant modulator
|
|
148
|
+
* @type {number}
|
|
149
|
+
*/
|
|
150
|
+
resonanceOffset = 0;
|
|
151
|
+
|
|
146
152
|
/**
|
|
147
153
|
* The generators in real-time, affected by modulators.
|
|
148
154
|
* This is used during rendering.
|
|
@@ -290,7 +296,6 @@ class Voice
|
|
|
290
296
|
this.modulatedGenerators = new Int16Array(generators);
|
|
291
297
|
this.modulators = modulators;
|
|
292
298
|
this.filter = new LowpassFilter(sampleRate);
|
|
293
|
-
|
|
294
299
|
this.velocity = velocity;
|
|
295
300
|
this.midiNote = midiNote;
|
|
296
301
|
this.startTime = currentTime;
|
|
@@ -452,8 +457,6 @@ export function getVoicesForPreset(preset, bank, program, midiNote, velocity, re
|
|
|
452
457
|
// MidiNote: midiNote,
|
|
453
458
|
// AudioSample: audioSample
|
|
454
459
|
// }]);
|
|
455
|
-
|
|
456
|
-
|
|
457
460
|
voices.push(
|
|
458
461
|
new Voice(
|
|
459
462
|
this.sampleRate,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { interpolationTypes } from "./enums.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* wavetable_oscillator.js
|
|
3
5
|
* purpose: plays back raw audio data at an arbitrary playback rate
|
|
@@ -6,13 +8,45 @@
|
|
|
6
8
|
|
|
7
9
|
export class WavetableOscillator
|
|
8
10
|
{
|
|
11
|
+
/**
|
|
12
|
+
* Fills the output buffer with raw sample data using a given interpolation
|
|
13
|
+
* @param voice {Voice} the voice we're working on
|
|
14
|
+
* @param outputBuffer {Float32Array} the output buffer to write to
|
|
15
|
+
* @param interpolation {interpolationTypes} the interpolation type
|
|
16
|
+
*/
|
|
17
|
+
static getSample(voice, outputBuffer, interpolation)
|
|
18
|
+
{
|
|
19
|
+
const step = voice.currentTuningCalculated * voice.sample.playbackStep;
|
|
20
|
+
// why not?
|
|
21
|
+
if (step === 1)
|
|
22
|
+
{
|
|
23
|
+
WavetableOscillator.getSampleNearest(voice, outputBuffer, step);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
switch (interpolation)
|
|
27
|
+
{
|
|
28
|
+
case interpolationTypes.fourthOrder:
|
|
29
|
+
this.getSampleHermite(voice, outputBuffer, step);
|
|
30
|
+
return;
|
|
31
|
+
|
|
32
|
+
case interpolationTypes.linear:
|
|
33
|
+
default:
|
|
34
|
+
this.getSampleLinear(voice, outputBuffer, step);
|
|
35
|
+
return;
|
|
36
|
+
|
|
37
|
+
case interpolationTypes.nearestNeighbor:
|
|
38
|
+
WavetableOscillator.getSampleNearest(voice, outputBuffer, step);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
9
42
|
|
|
10
43
|
/**
|
|
11
44
|
* Fills the output buffer with raw sample data using linear interpolation
|
|
12
45
|
* @param voice {Voice} the voice we're working on
|
|
13
46
|
* @param outputBuffer {Float32Array} the output buffer to write to
|
|
47
|
+
* @param step {number} the step to advance every sample
|
|
14
48
|
*/
|
|
15
|
-
static getSampleLinear(voice, outputBuffer)
|
|
49
|
+
static getSampleLinear(voice, outputBuffer, step)
|
|
16
50
|
{
|
|
17
51
|
const sample = voice.sample;
|
|
18
52
|
let cur = sample.cursor;
|
|
@@ -45,15 +79,11 @@ export class WavetableOscillator
|
|
|
45
79
|
const lower = sampleData[floor];
|
|
46
80
|
outputBuffer[i] = (lower + (upper - lower) * fraction);
|
|
47
81
|
|
|
48
|
-
cur +=
|
|
82
|
+
cur += step;
|
|
49
83
|
}
|
|
50
84
|
}
|
|
51
85
|
else
|
|
52
86
|
{
|
|
53
|
-
if (sample.loopingMode === 2 && !voice.isInRelease)
|
|
54
|
-
{
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
87
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
58
88
|
{
|
|
59
89
|
|
|
@@ -75,7 +105,7 @@ export class WavetableOscillator
|
|
|
75
105
|
const lower = sampleData[floor];
|
|
76
106
|
outputBuffer[i] = (lower + (upper - lower) * fraction);
|
|
77
107
|
|
|
78
|
-
cur +=
|
|
108
|
+
cur += step;
|
|
79
109
|
}
|
|
80
110
|
}
|
|
81
111
|
voice.sample.cursor = cur;
|
|
@@ -85,15 +115,17 @@ export class WavetableOscillator
|
|
|
85
115
|
* Fills the output buffer with raw sample data using no interpolation (nearest neighbor)
|
|
86
116
|
* @param voice {Voice} the voice we're working on
|
|
87
117
|
* @param outputBuffer {Float32Array} the output buffer to write to
|
|
118
|
+
* @param step {number} the step to advance every sample
|
|
88
119
|
*/
|
|
89
|
-
static getSampleNearest(voice, outputBuffer)
|
|
120
|
+
static getSampleNearest(voice, outputBuffer, step)
|
|
90
121
|
{
|
|
91
122
|
const sample = voice.sample;
|
|
92
123
|
let cur = sample.cursor;
|
|
93
|
-
const loopLength = sample.loopEnd - sample.loopStart;
|
|
94
124
|
const sampleData = sample.sampleData;
|
|
95
|
-
|
|
125
|
+
|
|
126
|
+
if (sample.isLooping)
|
|
96
127
|
{
|
|
128
|
+
const loopLength = sample.loopEnd - sample.loopStart;
|
|
97
129
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
98
130
|
{
|
|
99
131
|
// check for loop
|
|
@@ -111,15 +143,11 @@ export class WavetableOscillator
|
|
|
111
143
|
}
|
|
112
144
|
|
|
113
145
|
outputBuffer[i] = sampleData[ceil];
|
|
114
|
-
cur +=
|
|
146
|
+
cur += step;
|
|
115
147
|
}
|
|
116
148
|
}
|
|
117
149
|
else
|
|
118
150
|
{
|
|
119
|
-
if (sample.loopingMode === 2 && !voice.isInRelease)
|
|
120
|
-
{
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
151
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
124
152
|
{
|
|
125
153
|
|
|
@@ -133,9 +161,8 @@ export class WavetableOscillator
|
|
|
133
161
|
return;
|
|
134
162
|
}
|
|
135
163
|
|
|
136
|
-
//nearest neighbor (uncomment to use)
|
|
137
164
|
outputBuffer[i] = sampleData[ceil];
|
|
138
|
-
cur +=
|
|
165
|
+
cur += step;
|
|
139
166
|
}
|
|
140
167
|
}
|
|
141
168
|
sample.cursor = cur;
|
|
@@ -143,11 +170,12 @@ export class WavetableOscillator
|
|
|
143
170
|
|
|
144
171
|
|
|
145
172
|
/**
|
|
146
|
-
* Fills the output buffer with raw sample data using
|
|
173
|
+
* Fills the output buffer with raw sample data using Hermite interpolation
|
|
147
174
|
* @param voice {Voice} the voice we're working on
|
|
148
175
|
* @param outputBuffer {Float32Array} the output buffer to write to
|
|
176
|
+
* @param step {number} the step to advance every sample
|
|
149
177
|
*/
|
|
150
|
-
static
|
|
178
|
+
static getSampleHermite(voice, outputBuffer, step)
|
|
151
179
|
{
|
|
152
180
|
const sample = voice.sample;
|
|
153
181
|
let cur = sample.cursor;
|
|
@@ -158,21 +186,18 @@ export class WavetableOscillator
|
|
|
158
186
|
const loopLength = sample.loopEnd - sample.loopStart;
|
|
159
187
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
160
188
|
{
|
|
161
|
-
// check for loop
|
|
189
|
+
// check for loop (it can exceed the end point multiple times)
|
|
162
190
|
while (cur >= sample.loopEnd)
|
|
163
191
|
{
|
|
164
192
|
cur -= loopLength;
|
|
165
193
|
}
|
|
166
194
|
|
|
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
195
|
// grab the 4 points
|
|
171
|
-
const y0 = ~~cur; // point before the cursor. twice bitwise
|
|
196
|
+
const y0 = ~~cur; // point before the cursor. twice bitwise-not is just a faster Math.floor
|
|
172
197
|
let y1 = y0 + 1; // point after the cursor
|
|
173
|
-
let y2 =
|
|
174
|
-
let y3 =
|
|
175
|
-
const t = cur - y0; // the distance from y0 to cursor
|
|
198
|
+
let y2 = y0 + 2; // point 1 after the cursor
|
|
199
|
+
let y3 = y0 + 3; // point 2 after the cursor
|
|
200
|
+
const t = cur - y0; // the distance from y0 to cursor [0;1]
|
|
176
201
|
// y0 is not handled here
|
|
177
202
|
// as it's math.floor of cur which is handled above
|
|
178
203
|
if (y1 >= sample.loopEnd)
|
|
@@ -189,40 +214,33 @@ export class WavetableOscillator
|
|
|
189
214
|
}
|
|
190
215
|
|
|
191
216
|
// grab the samples
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const
|
|
217
|
+
const xm1 = sampleData[y0];
|
|
218
|
+
const x0 = sampleData[y1];
|
|
219
|
+
const x1 = sampleData[y2];
|
|
220
|
+
const x2 = sampleData[y3];
|
|
196
221
|
|
|
197
222
|
// interpolate
|
|
198
|
-
//
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
223
|
+
// https://www.musicdsp.org/en/latest/Other/93-hermite-interpollation.html
|
|
224
|
+
const c = (x1 - xm1) * 0.5;
|
|
225
|
+
const v = x0 - x1;
|
|
226
|
+
const w = c + v;
|
|
227
|
+
const a = w + v + (x2 - x0) * 0.5;
|
|
228
|
+
const b = w + a;
|
|
229
|
+
outputBuffer[i] = ((((a * t) - b) * t + c) * t + x0);
|
|
204
230
|
|
|
205
|
-
cur +=
|
|
231
|
+
cur += step;
|
|
206
232
|
}
|
|
207
233
|
}
|
|
208
234
|
else
|
|
209
235
|
{
|
|
210
|
-
if (sample.loopingMode === 2 && !voice.isInRelease)
|
|
211
|
-
{
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
236
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
215
237
|
{
|
|
216
|
-
|
|
217
|
-
// math comes from
|
|
218
|
-
// https://stackoverflow.com/questions/1125666/how-do-you-do-bicubic-or-other-non-linear-interpolation-of-re-sampled-audio-da
|
|
219
|
-
|
|
220
238
|
// grab the 4 points
|
|
221
|
-
const y0 = ~~cur; // point before the cursor. twice bitwise
|
|
239
|
+
const y0 = ~~cur; // point before the cursor. twice bitwise-not is just a faster Math.floor
|
|
222
240
|
let y1 = y0 + 1; // point after the cursor
|
|
223
|
-
let y2 =
|
|
224
|
-
let y3 =
|
|
225
|
-
const t = cur - y0; // distance from y0 to cursor
|
|
241
|
+
let y2 = y0 + 2; // point 1 after the cursor
|
|
242
|
+
let y3 = y0 + 3; // point 2 after the cursor
|
|
243
|
+
const t = cur - y0; // the distance from y0 to cursor [0;1]
|
|
226
244
|
|
|
227
245
|
// flag as finished if needed
|
|
228
246
|
if (y1 >= sample.end ||
|
|
@@ -234,18 +252,21 @@ export class WavetableOscillator
|
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
// grab the samples
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
255
|
+
const xm1 = sampleData[y0];
|
|
256
|
+
const x0 = sampleData[y1];
|
|
257
|
+
const x1 = sampleData[y2];
|
|
258
|
+
const x2 = sampleData[y3];
|
|
241
259
|
|
|
242
260
|
// interpolate
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
261
|
+
// https://www.musicdsp.org/en/latest/Other/93-hermite-interpollation.html
|
|
262
|
+
const c = (x1 - xm1) * 0.5;
|
|
263
|
+
const v = x0 - x1;
|
|
264
|
+
const w = c + v;
|
|
265
|
+
const a = w + v + (x2 - x0) * 0.5;
|
|
266
|
+
const b = w + a;
|
|
267
|
+
outputBuffer[i] = ((((a * t) - b) * t + c) * t + x0);
|
|
247
268
|
|
|
248
|
-
cur +=
|
|
269
|
+
cur += step;
|
|
249
270
|
}
|
|
250
271
|
}
|
|
251
272
|
voice.sample.cursor = cur;
|
|
@@ -5,7 +5,6 @@ import { absCentsToHz, timecentsToSeconds } from "../engine_components/unit_conv
|
|
|
5
5
|
import { getLFOValue } from "../engine_components/lfo.js";
|
|
6
6
|
import { WavetableOscillator } from "../engine_components/wavetable_oscillator.js";
|
|
7
7
|
import { LowpassFilter } from "../engine_components/lowpass_filter.js";
|
|
8
|
-
import { interpolationTypes } from "../engine_components/enums.js";
|
|
9
8
|
import { generatorTypes } from "../../../soundfont/basic_soundfont/generator_types.js";
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -161,6 +160,9 @@ export function renderVoice(
|
|
|
161
160
|
cents += modEnv * modEnvPitchDepth;
|
|
162
161
|
}
|
|
163
162
|
|
|
163
|
+
// default resonant modulator: it does not affect the filter gain (neither XG nor GS did that)
|
|
164
|
+
volumeExcursionCentibels -= voice.resonanceOffset;
|
|
165
|
+
|
|
164
166
|
// finally, calculate the playback rate
|
|
165
167
|
const centsTotal = ~~(cents + semitones * 100);
|
|
166
168
|
if (centsTotal !== voice.currentTuningCents)
|
|
@@ -173,23 +175,22 @@ export function renderVoice(
|
|
|
173
175
|
// SYNTHESIS
|
|
174
176
|
const bufferOut = new Float32Array(sampleCount);
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
|
|
179
|
+
// looping mode 2: start on release. process only volEnv
|
|
180
|
+
if (voice.sample.loopingMode === 2 && !voice.isInRelease)
|
|
178
181
|
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
break;
|
|
187
|
-
|
|
188
|
-
case interpolationTypes.nearestNeighbor:
|
|
189
|
-
WavetableOscillator.getSampleNearest(voice, bufferOut);
|
|
190
|
-
break;
|
|
182
|
+
VolumeEnvelope.apply(
|
|
183
|
+
voice,
|
|
184
|
+
bufferOut,
|
|
185
|
+
volumeExcursionCentibels,
|
|
186
|
+
this.synth.volumeEnvelopeSmoothingFactor
|
|
187
|
+
);
|
|
188
|
+
return voice.finished;
|
|
191
189
|
}
|
|
192
190
|
|
|
191
|
+
// wave table oscillator
|
|
192
|
+
WavetableOscillator.getSample(voice, bufferOut, this.synth.interpolationType);
|
|
193
|
+
|
|
193
194
|
// low pass filter
|
|
194
195
|
LowpassFilter.apply(voice, bufferOut, lowpassExcursion, this.synth.filterSmoothingFactor);
|
|
195
196
|
|