spessasynth_lib 3.20.34 → 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.
@@ -84,7 +84,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
84
84
  * Interpolation type used
85
85
  * @type {interpolationTypes}
86
86
  */
87
- this.interpolationType = interpolationTypes.linear;
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
- WorkletVolumeEnvelope.intialize(voice);
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
- if(filter.cutoffHz > 0.45 * sampleRate)
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
- const PERCEIVED_GAIN_SILENCE = 0.005;
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, setupInterpolated = false)
155
+ static recalculate(voice)
166
156
  {
167
157
  const env = voice.volumeEnvelope;
168
158
  const timecentsToSamples = tc =>
@@ -172,11 +162,7 @@ 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
- const sustainDb = Math.min(DB_SILENCE, env.sustainDbRelative + env.attenuation);
165
+ const sustainDb = Math.min(DB_SILENCE, env.sustainDbRelative);
180
166
 
181
167
  // calculate durations
182
168
  env.attackDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.attackVolEnv]);
@@ -186,7 +172,7 @@ export class WorkletVolumeEnvelope
186
172
  // (changing from attenuation to sustain instead of -100dB)
187
173
  const fullChange = voice.modulatedGenerators[generatorTypes.decayVolEnv];
188
174
  const keyNumAddition = (60 - voice.targetKey) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay];
189
- const fraction = (sustainDb - env.attenuation) / DB_SILENCE;
175
+ const fraction = sustainDb / DB_SILENCE;
190
176
  env.decayDuration = timecentsToSamples(fullChange + keyNumAddition) * fraction;
191
177
 
192
178
  env.releaseDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
@@ -206,7 +192,7 @@ export class WorkletVolumeEnvelope
206
192
  // if this is the first recalculation and the voice has no attack or delay time, set current db to peak
207
193
  if(env.state === 0 && env.attackEnd === 0)
208
194
  {
209
- env.currentAttenuationDb = env.attenuationTarget;
195
+ // env.currentAttenuationDb = env.attenuationTarget;
210
196
  env.state = 2;
211
197
  }
212
198
 
@@ -214,9 +200,9 @@ export class WorkletVolumeEnvelope
214
200
  if(voice.isInRelease)
215
201
  {
216
202
  // no interpolation this time: force update to actual attenuation and calculate release start from there
217
- env.attenuation = Math.min(DB_SILENCE, env.attenuationTarget);
218
- const sustainDb = Math.max(0, Math.min(DB_SILENCE, env.sustainDbRelative + env.attenuation));
219
- const fraction = (sustainDb - env.attenuation) / DB_SILENCE;
203
+ //env.attenuation = Math.min(DB_SILENCE, env.attenuationTarget);
204
+ const sustainDb = Math.max(0, Math.min(DB_SILENCE, env.sustainDbRelative));
205
+ const fraction = sustainDb / DB_SILENCE;
220
206
  env.decayDuration = timecentsToSamples(fullChange + keyNumAddition) * fraction;
221
207
 
222
208
  switch (env.state)
@@ -232,26 +218,21 @@ export class WorkletVolumeEnvelope
232
218
  // attack is linear (in gain) so we need to do get db from that
233
219
  let elapsed = 1 - ((env.attackEnd - env.releaseStartTimeSamples) / env.attackDuration);
234
220
  // calculate the gain that the attack would have
235
- let attackGain = elapsed * decibelAttenuationToGain(env.attenuation);
236
-
237
221
  // turn that into db
238
- env.releaseStartDb = 20 * Math.log10(attackGain) * -1;
222
+ env.releaseStartDb = 20 * Math.log10(elapsed) * -1;
239
223
  break;
240
224
 
241
225
  case 2:
242
- env.releaseStartDb = env.attenuation;
226
+ env.releaseStartDb = 0;
243
227
  break;
244
228
 
245
229
  case 3:
246
- env.releaseStartDb = (1 - (env.decayEnd - env.releaseStartTimeSamples) / env.decayDuration) * (sustainDb - env.attenuation) + env.attenuation;
230
+ env.releaseStartDb = (1 - (env.decayEnd - env.releaseStartTimeSamples) / env.decayDuration) * sustainDb;
247
231
  break;
248
232
 
249
233
  case 4:
250
234
  env.releaseStartDb = sustainDb;
251
235
  break;
252
-
253
- default:
254
- env.releaseStartDb = env.currentAttenuationDb;
255
236
  }
256
237
  env.releaseStartDb = Math.max(0, Math.min(env.releaseStartDb, DB_SILENCE));
257
238
  if(env.releaseStartDb >= PERCEIVED_DB_SILENCE)
@@ -268,6 +249,7 @@ export class WorkletVolumeEnvelope
268
249
  * @param audioBuffer {Float32Array} the audio buffer to modify
269
250
  * @param centibelOffset {number} the centibel offset of volume, for modLFOtoVolume
270
251
  * @param smoothingFactor {number} the adjusted smoothing factor for the envelope
252
+ * @description essentially we use approach of 100dB is silence, 0dB is peak, and always add attenuation to that (which is interpolated)
271
253
  */
272
254
  static apply(voice, audioBuffer, centibelOffset, smoothingFactor)
273
255
  {
@@ -279,9 +261,6 @@ export class WorkletVolumeEnvelope
279
261
  // RELEASE PHASE
280
262
  if(voice.isInRelease)
281
263
  {
282
- // release needs a more aggressive smoothing factor
283
- // as the instant notes don't end instantly when they should
284
- const releaseSmoothingFactor = smoothingFactor * 10;
285
264
  let elapsedRelease = env.currentSampleTime - env.releaseStartTimeSamples;
286
265
  if(elapsedRelease >= env.releaseDuration)
287
266
  {
@@ -295,9 +274,10 @@ export class WorkletVolumeEnvelope
295
274
  let dbDifference = DB_SILENCE - env.releaseStartDb;
296
275
  for (let i = 0; i < audioBuffer.length; i++)
297
276
  {
277
+ // attenuation interpolation
278
+ env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing;
298
279
  let db = (elapsedRelease / env.releaseDuration) * dbDifference + env.releaseStartDb;
299
- let gain = decibelAttenuationToGain(db + decibelOffset);
300
- env.currentReleaseGain += (gain - env.currentReleaseGain) * releaseSmoothingFactor;
280
+ env.currentReleaseGain = decibelAttenuationToGain(db + decibelOffset + env.attenuation);
301
281
  audioBuffer[i] *= env.currentReleaseGain;
302
282
  env.currentSampleTime++;
303
283
  elapsedRelease++;
@@ -340,7 +320,7 @@ export class WorkletVolumeEnvelope
340
320
  let linearAttenuation = 1 - (env.attackEnd - env.currentSampleTime) / env.attackDuration; // 0 to 1
341
321
  audioBuffer[filledBuffer] *= linearAttenuation * decibelAttenuationToGain(env.attenuation + decibelOffset);
342
322
  // set current attenuation to peak as its invalid during this phase
343
- env.currentAttenuationDb = env.attenuation;
323
+ env.currentAttenuationDb = 0;
344
324
 
345
325
  env.currentSampleTime++;
346
326
  if(++filledBuffer >= audioBuffer.length)
@@ -359,7 +339,7 @@ export class WorkletVolumeEnvelope
359
339
  env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing;
360
340
 
361
341
  audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.attenuation + decibelOffset);
362
- env.currentAttenuationDb = env.attenuation;
342
+ env.currentAttenuationDb = 0;
363
343
 
364
344
  env.currentSampleTime++;
365
345
  if(++filledBuffer >= audioBuffer.length)
@@ -377,9 +357,8 @@ export class WorkletVolumeEnvelope
377
357
  // attenuation interpolation
378
358
  env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing;
379
359
 
380
- const sustainDb = Math.min(DB_SILENCE, env.sustainDbRelative + env.attenuation);
381
- env.currentAttenuationDb = (1 - (env.decayEnd - env.currentSampleTime) / env.decayDuration) * (sustainDb - env.attenuation) + env.attenuation;
382
- audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb + decibelOffset);
360
+ env.currentAttenuationDb = (1 - (env.decayEnd - env.currentSampleTime) / env.decayDuration) * env.sustainDbRelative;
361
+ audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb + decibelOffset + env.attenuation);
383
362
 
384
363
  env.currentSampleTime++;
385
364
  if(++filledBuffer >= audioBuffer.length)
@@ -400,10 +379,9 @@ export class WorkletVolumeEnvelope
400
379
  {
401
380
  // attenuation interpolation
402
381
  env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing;
403
- const sustainDb = Math.min(DB_SILENCE, env.sustainDbRelative + env.attenuation);
404
382
 
405
- audioBuffer[filledBuffer] *= decibelAttenuationToGain(sustainDb + decibelOffset);
406
- env.currentAttenuationDb = sustainDb;
383
+ audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.sustainDbRelative + decibelOffset + env.attenuation);
384
+ env.currentAttenuationDb = env.sustainDbRelative;
407
385
  env.currentSampleTime++;
408
386
  if(++filledBuffer >= audioBuffer.length)
409
387
  {
@@ -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) + (generators[generatorTypes.startloopAddrsOffset] + (generators[generatorTypes.startloopAddrsCoarseOffset] * 32768));
372
- let loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2) + (generators[generatorTypes.endloopAddrsOffset] + (generators[generatorTypes.endloopAddrsCoarseOffset] * 32768));
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 and calculate offsets
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
- generators[generatorTypes.startAddrsOffset] + (generators[generatorTypes.startAddrsCoarseOffset] * 32768),
391
+ 0,
391
392
  rootKey,
392
393
  loopStart,
393
394
  loopEnd,
394
- Math.floor( sampleAndGenerators.sample.sampleData.length) - 1 + (generators[generatorTypes.endAddrOffset] + (generators[generatorTypes.endAddrsCoarseOffset] * 32768)),
395
+ Math.floor( sampleAndGenerators.sample.sampleData.length) - 1,
395
396
  loopingMode
396
397
  )
397
398
  // velocity override