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.
@@ -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,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) + (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