spessasynth_lib 3.20.35 → 3.20.37
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/synthetizer/synthetizer.d.ts +1 -1
- 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/synthetizer/synthetizer.js +1 -1
- package/synthetizer/worklet_processor.min.js +11 -11
- package/synthetizer/worklet_system/main_processor.js +1 -1
- package/synthetizer/worklet_system/worklet_methods/note_on.js +28 -1
- package/synthetizer/worklet_system/worklet_methods/voice_control.js +9 -5
- 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 +2 -0
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +6 -5
|
@@ -84,7 +84,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
84
84
|
* Interpolation type used
|
|
85
85
|
* @type {interpolationTypes}
|
|
86
86
|
*/
|
|
87
|
-
this.interpolationType = interpolationTypes.
|
|
87
|
+
this.interpolationType = interpolationTypes.fourthOrder;
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* @type {function}
|
|
@@ -59,6 +59,7 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
|
|
|
59
59
|
const exclusive = voice.generators[generatorTypes.exclusiveClass];
|
|
60
60
|
if(exclusive !== 0)
|
|
61
61
|
{
|
|
62
|
+
// kill all voices with the same exclusive class
|
|
62
63
|
channelVoices.forEach(v => {
|
|
63
64
|
if(v.generators[generatorTypes.exclusiveClass] === exclusive)
|
|
64
65
|
{
|
|
@@ -72,7 +73,33 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
|
|
|
72
73
|
}
|
|
73
74
|
// compute all modulators
|
|
74
75
|
computeModulators(voice, channelObject.midiControllers);
|
|
75
|
-
|
|
76
|
+
// modulate sample offsets (these are not real time)
|
|
77
|
+
const cursorStartOffset = voice.modulatedGenerators[generatorTypes.startAddrsOffset] + voice.modulatedGenerators[generatorTypes.startAddrsCoarseOffset] * 32768;
|
|
78
|
+
const endOffset = voice.modulatedGenerators[generatorTypes.endAddrOffset] + voice.modulatedGenerators[generatorTypes.endAddrsCoarseOffset] * 32768;
|
|
79
|
+
const loopStartOffset = voice.modulatedGenerators[generatorTypes.startloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.startloopAddrsCoarseOffset] * 32768;
|
|
80
|
+
const loopEndOffset = voice.modulatedGenerators[generatorTypes.endloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.endloopAddrsCoarseOffset] * 32768;
|
|
81
|
+
const sm = voice.sample;
|
|
82
|
+
// apply them
|
|
83
|
+
const clamp = num => Math.max(0, Math.min(sm.sampleData.length - 1, num));
|
|
84
|
+
sm.cursor = clamp( sm.cursor + cursorStartOffset);
|
|
85
|
+
sm.end = clamp(sm.end + endOffset);
|
|
86
|
+
sm.loopStart = clamp(sm.loopStart + loopStartOffset);
|
|
87
|
+
sm.loopEnd = clamp(sm.loopEnd + loopEndOffset);
|
|
88
|
+
// swap loops if needed
|
|
89
|
+
if(sm.loopEnd < sm.loopStart)
|
|
90
|
+
{
|
|
91
|
+
const temp = sm.loopStart;
|
|
92
|
+
sm.loopStart = sm.loopEnd;
|
|
93
|
+
sm.loopEnd = temp;
|
|
94
|
+
}
|
|
95
|
+
if (sm.loopEnd - sm.loopStart < 1)
|
|
96
|
+
{
|
|
97
|
+
sm.loopingMode = 0;
|
|
98
|
+
sm.isLooping = false;
|
|
99
|
+
}
|
|
100
|
+
// set the current attenuation to target,
|
|
101
|
+
// as it's interpolated (we don't want 0 attenuation for even a split second)
|
|
102
|
+
voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTarget;
|
|
76
103
|
// set initial pan to avoid split second changing from middle to the correct value
|
|
77
104
|
voice.currentPan = ((Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) // 0 to 1
|
|
78
105
|
});
|
|
@@ -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,
|
|
@@ -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
|
}
|
|
@@ -116,6 +116,8 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou
|
|
|
116
116
|
const { modulators, generators, modulatedGenerators } = voice;
|
|
117
117
|
|
|
118
118
|
// Modulation envelope is cheap to recalculate
|
|
119
|
+
// why here and not at the bottom?
|
|
120
|
+
// I dunno, seems to work fine
|
|
119
121
|
WorkletModulationEnvelope.recalculate(voice);
|
|
120
122
|
|
|
121
123
|
if (sourceUsesCC === -1)
|
|
@@ -368,8 +368,8 @@ export function getWorkletVoices(channel,
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
// 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)
|
|
371
|
+
let loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2);
|
|
372
|
+
let loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2);
|
|
373
373
|
let loopingMode = generators[generatorTypes.sampleModes];
|
|
374
374
|
const sampleLength = sampleAndGenerators.sample.getAudioData().length;
|
|
375
375
|
// clamp loop
|
|
@@ -381,17 +381,18 @@ export function getWorkletVoices(channel,
|
|
|
381
381
|
loopingMode = 0;
|
|
382
382
|
}
|
|
383
383
|
/**
|
|
384
|
-
* create the worklet sample
|
|
384
|
+
* create the worklet sample
|
|
385
|
+
* offsets are calculated at note on time (to allow for modulation of them)
|
|
385
386
|
* @type {WorkletSample}
|
|
386
387
|
*/
|
|
387
388
|
const workletSample = new WorkletSample(
|
|
388
389
|
sampleAndGenerators.sample.getAudioData(),
|
|
389
390
|
(sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(2, sampleAndGenerators.sample.samplePitchCorrection / 1200), // cent tuning
|
|
390
|
-
|
|
391
|
+
0,
|
|
391
392
|
rootKey,
|
|
392
393
|
loopStart,
|
|
393
394
|
loopEnd,
|
|
394
|
-
Math.floor( sampleAndGenerators.sample.sampleData.length) - 1
|
|
395
|
+
Math.floor( sampleAndGenerators.sample.sampleData.length) - 1,
|
|
395
396
|
loopingMode
|
|
396
397
|
)
|
|
397
398
|
// velocity override
|