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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_core",
3
- "version": "3.27.2",
3
+ "version": "3.27.4",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -28,7 +28,7 @@ export const sampleTypes = {
28
28
  * @async
29
29
  * @param audioData {Float32Array}
30
30
  * @param sampleRate {number}
31
- * @returns {Uint8Array}
31
+ * @returns {Promise<Uint8Array>}
32
32
  */
33
33
 
34
34
  export class BasicSample
@@ -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
- return new Modulator(
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
- return new Modulator(
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 += sample.playbackStep * voice.currentTuningCalculated;
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 += sample.playbackStep * voice.currentTuningCalculated;
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
- if (voice.sample.isLooping)
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 += sample.playbackStep * voice.currentTuningCalculated;
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 += sample.playbackStep * voice.currentTuningCalculated;
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 cubic interpolation
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 getSampleCubic(voice, outputBuffer)
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 not is just a faster Math.floor
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 = y1 + 1; // point 1 after the cursor
174
- let y3 = y2 + 1; // point 2 after the cursor
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 x0 = sampleData[y0];
193
- const x1 = sampleData[y1];
194
- const x2 = sampleData[y2];
195
- const x3 = sampleData[y3];
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
- // const c0 = x1
199
- const c1 = 0.5 * (x2 - x0);
200
- const c2 = x0 - (2.5 * x1) + (2 * x2) - (0.5 * x3);
201
- const c3 = (0.5 * (x3 - x0)) + (1.5 * (x1 - x2));
202
- outputBuffer[i] = (((((c3 * t) + c2) * t) + c1) * t) + x1;
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 += sample.playbackStep * voice.currentTuningCalculated;
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 not is just a faster Math.floor
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 = y1 + 1; // point 1 after the cursor
224
- let y3 = y2 + 1; // point 2 after the cursor
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 x0 = sampleData[y0];
238
- const x1 = sampleData[y1];
239
- const x2 = sampleData[y2];
240
- const x3 = sampleData[y3];
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
- const c1 = 0.5 * (x2 - x0);
244
- const c2 = x0 - (2.5 * x1) + (2 * x2) - (0.5 * x3);
245
- const c3 = (0.5 * (x3 - x0)) + (1.5 * (x1 - x2));
246
- outputBuffer[i] = (((((c3 * t) + c2) * t) + c1) * t) + x1;
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 += sample.playbackStep * voice.currentTuningCalculated;
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
- // wave table oscillator
177
- switch (this.synth.interpolationType)
178
+
179
+ // looping mode 2: start on release. process only volEnv
180
+ if (voice.sample.loopingMode === 2 && !voice.isInRelease)
178
181
  {
179
- case interpolationTypes.fourthOrder:
180
- WavetableOscillator.getSampleCubic(voice, bufferOut);
181
- break;
182
-
183
- case interpolationTypes.linear:
184
- default:
185
- WavetableOscillator.getSampleLinear(voice, bufferOut);
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