spessasynth_lib 3.24.7 → 3.24.9

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.
@@ -1,10 +1,8 @@
1
- // noinspection JSUnresolvedReference
2
-
3
1
  import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, VOICE_CAP } from "../synthetizer.js";
4
2
  import { WorkletSequencer } from "../../sequencer/worklet_sequencer/worklet_sequencer.js";
5
3
  import { SpessaSynthInfo } from "../../utils/loggin.js";
6
4
  import { consoleColors } from "../../utils/other.js";
7
- import { PAN_SMOOTHING_FACTOR, releaseVoice, renderVoice, voiceKilling } from "./worklet_methods/voice_control.js";
5
+ import { releaseVoice, renderVoice, voiceKilling } from "./worklet_methods/voice_control.js";
8
6
  import { ALL_CHANNELS_OR_DIFFERENT_ACTION, returnMessageType } from "./message_protocol/worklet_message.js";
9
7
  import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js";
10
8
  import { VOLUME_ENVELOPE_SMOOTHING_FACTOR } from "./worklet_utilities/volume_envelope.js";
@@ -51,7 +49,7 @@ import { WorkletSoundfontManager } from "./worklet_methods/worklet_soundfont_man
51
49
  import { interpolationTypes } from "./worklet_utilities/wavetable_oscillator.js";
52
50
  import { WorkletKeyModifierManager } from "./worklet_methods/worklet_key_modifier.js";
53
51
  import { getWorkletVoices } from "./worklet_utilities/worklet_voice.js";
54
- import { panVoice } from "./worklet_utilities/stereo_panner.js";
52
+ import { PAN_SMOOTHING_FACTOR, panVoice } from "./worklet_utilities/stereo_panner.js";
55
53
 
56
54
 
57
55
  /**
@@ -68,6 +66,7 @@ export const MIN_EXCLUSIVE_LENGTH = 0.07;
68
66
  export const SYNTHESIZER_GAIN = 1.0;
69
67
 
70
68
 
69
+ // noinspection JSUnresolvedReference
71
70
  class SpessaSynthProcessor extends AudioWorkletProcessor
72
71
  {
73
72
  /**
@@ -106,11 +105,6 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
106
105
  */
107
106
  this.interpolationType = interpolationTypes.fourthOrder;
108
107
 
109
- /**
110
- * @type {function}
111
- */
112
- this.processTickCallback = undefined;
113
-
114
108
  this.sequencer = new WorkletSequencer(this);
115
109
 
116
110
  this.transposition = 0;
@@ -214,7 +208,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
214
208
  this.workletProcessorChannels[DEFAULT_PERCUSSION].preset = this.drumPreset;
215
209
  this.workletProcessorChannels[DEFAULT_PERCUSSION].drumChannel = true;
216
210
 
217
- // these smoothing factors were tested on 44,100 Hz, adjust them here
211
+ // these smoothing factors were tested on 44,100 Hz, adjust them to target sample rate here
218
212
  this.volumeEnvelopeSmoothingFactor = VOLUME_ENVELOPE_SMOOTHING_FACTOR * (44100 / sampleRate);
219
213
  this.panSmoothingFactor = PAN_SMOOTHING_FACTOR * (44100 / sampleRate);
220
214
 
@@ -327,10 +321,8 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
327
321
  {
328
322
  return false;
329
323
  }
330
- if (this.processTickCallback)
331
- {
332
- this.processTickCallback();
333
- }
324
+ // process the sequencer playback
325
+ this.sequencer.processTick();
334
326
 
335
327
  // for every channel
336
328
  let totalCurrentVoices = 0;
@@ -365,13 +357,8 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
365
357
  chorusChannels = outputs[1];
366
358
  }
367
359
 
368
- const tempV = channel.voices;
369
-
370
- // reset voices
371
- channel.voices = [];
372
-
373
- // for every voice
374
- tempV.forEach(v =>
360
+ // for every voice, render it
361
+ channel.voices = channel.voices.filter(v =>
375
362
  {
376
363
  // render voice
377
364
  this.renderVoice(
@@ -381,11 +368,8 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
381
368
  reverbChannels,
382
369
  chorusChannels
383
370
  );
384
- if (!v.finished)
385
- {
386
- // if not finished, add it back
387
- channel.voices.push(v);
388
- }
371
+
372
+ return !v.finished;
389
373
  });
390
374
 
391
375
  totalCurrentVoices += channel.voices.length;
@@ -433,7 +417,7 @@ SpessaSynthProcessor.prototype.handleMessage = handleMessage;
433
417
  SpessaSynthProcessor.prototype.sendChannelProperties = sendChannelProperties;
434
418
  SpessaSynthProcessor.prototype.callEvent = callEvent;
435
419
 
436
- // system exlcusive related
420
+ // system-exclusive related
437
421
  SpessaSynthProcessor.prototype.systemExclusive = systemExclusive;
438
422
 
439
423
  // note messages related
@@ -0,0 +1,13 @@
1
+ # About the message protocol
2
+ Since spessasynth runs in the audioWorklet thread, here is an explanation of how it works:
3
+
4
+ There's one processor per synthesizer, with a `MessagePort` for communication.
5
+ Each processor has a single `WorkletSequencer` instance that is idle by default.
6
+
7
+ The `Synthetizer`,
8
+ `Sequencer` and `SoundFontManager` classes are all interfaces
9
+ that do not do anything except sending the commands to te processor.
10
+
11
+ The synthesizer sends the commands (note on, off, etc.) directly to the processor where they are processed and executed.
12
+
13
+ The sequencer sends the commands through the connected synthesizer's messagePort, which then get processed as sequencer messages and routed
@@ -222,6 +222,7 @@ export function handleMessage(message)
222
222
 
223
223
  case workletMessageType.destroyWorklet:
224
224
  this.alive = false;
225
+ this.destroyWorkletProcessor();
225
226
  break;
226
227
 
227
228
  default:
@@ -123,10 +123,7 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
123
123
  // as it's interpolated (we don't want 0 attenuation for even a split second)
124
124
  voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTargetGain;
125
125
  // set initial pan to avoid split second changing from middle to the correct value
126
- voice.currentPan = ((Math.max(
127
- -500,
128
- Math.min(500, voice.modulatedGenerators[generatorTypes.pan])
129
- ) + 500) / 1000); // 0 to 1
126
+ voice.currentPan = Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan])); // -500 to 500
130
127
  });
131
128
 
132
129
  this.totalVoicesAmount += voices.length;
@@ -13,8 +13,6 @@ import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"
13
13
  import { customControllers } from "../worklet_utilities/controller_tables.js";
14
14
  import { WorkletLowpassFilter } from "../worklet_utilities/lowpass_filter.js";
15
15
 
16
- export const PAN_SMOOTHING_FACTOR = 0.05;
17
-
18
16
  /**
19
17
  * Renders a voice to the stereo output buffer
20
18
  * @param channel {WorkletProcessorChannel} the voice's channel
@@ -4,13 +4,29 @@ import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"
4
4
  /**
5
5
  * lowpass_filter.js
6
6
  * purpose: applies a low pass filter to a voice
7
- * note to self: most of this is code is just javascript version of the C code from fluidsynth,
8
- * they are the real smart guys.
7
+ * note to self: a lot of tricks and fixes come from fluidsynth.
8
+ * They are the real smart guys.
9
9
  * Shoutout to them!
10
10
  */
11
11
 
12
+ /**
13
+ * @typedef {Object} CachedCoefficient
14
+ * @property {number} a0 - Filter coefficient 1
15
+ * @property {number} a1 - Filter coefficient 2
16
+ * @property {number} a2 - Filter coefficient 3
17
+ * @property {number} a3 - Filter coefficient 4
18
+ * @property {number} a4 - Filter coefficient 5
19
+ */
20
+
12
21
  export class WorkletLowpassFilter
13
22
  {
23
+ /**
24
+ * Cached coefficient calculations
25
+ * stored as cachedCoefficients[reasonanceCb][cutoffCents]
26
+ * @type {CachedCoefficient[][]}
27
+ * @private
28
+ */
29
+ static cachedCoefficients = [];
14
30
  /**
15
31
  * Filter coefficient 1
16
32
  * @type {number}
@@ -71,25 +87,13 @@ export class WorkletLowpassFilter
71
87
  */
72
88
  reasonanceCb = 0;
73
89
 
74
- /**
75
- * Resonance gain
76
- * @type {number}
77
- */
78
- reasonanceGain = 1;
79
-
80
90
  /**
81
91
  * Cutoff frequency in cents
82
- * Note: defaults to 13501 to cause a recalculation even at initial fc being 13500
92
+ * Note: defaults to 13,501 to cause a recalculation even at initial fc being 13,500
83
93
  * @type {number}
84
94
  */
85
95
  cutoffCents = 13501;
86
96
 
87
- /**
88
- * Cutoff frequency in Hz
89
- * @type {number}
90
- */
91
- cutoffHz = 20001;
92
-
93
97
  /**
94
98
  * Applies a low-pass filter to the given buffer
95
99
  * @param voice {WorkletVoice} the voice we're working on
@@ -139,36 +143,58 @@ export class WorkletLowpassFilter
139
143
  */
140
144
  static calculateCoefficients(filter)
141
145
  {
142
- filter.cutoffHz = absCentsToHz(filter.cutoffCents);
143
-
144
- // fix cutoff on low frequencies (fluid_iir_filter.c line 392)
145
- filter.cutoffHz = Math.min(filter.cutoffHz, 0.45 * sampleRate);
146
-
147
- const qDb = filter.reasonanceCb / 10;
148
- // correct the filter gain, like fluid does
149
- filter.reasonanceGain = decibelAttenuationToGain(-1 * (qDb - 3.01)); // -1 because it's attenuation and we don't want attenuation
150
-
151
- // reduce the gain by the Q factor (fluid_iir_filter.c line 250)
152
- const qGain = 1 / Math.sqrt(decibelAttenuationToGain(-qDb));
153
-
154
-
155
- // code is ported from https://github.com/sinshu/meltysynth/ to work with js.
156
- let w = 2 * Math.PI * filter.cutoffHz / sampleRate; // we're in the audioworkletglobalscope so we can use sampleRate
157
- let cosw = Math.cos(w);
158
- let alpha = Math.sin(w) / (2 * filter.reasonanceGain);
159
-
160
- let b1 = (1 - cosw) * qGain;
161
- let b0 = b1 / 2;
162
- let b2 = b0;
163
- let a0 = 1 + alpha;
164
- let a1 = -2 * cosw;
165
- let a2 = 1 - alpha;
166
-
167
- // set coefficients
168
- filter.a0 = b0 / a0;
169
- filter.a1 = b1 / a0;
170
- filter.a2 = b2 / a0;
171
- filter.a3 = a1 / a0;
172
- filter.a4 = a2 / a0;
146
+ const cutoffCents = ~~filter.cutoffCents; // Math.floor
147
+ const qCb = filter.reasonanceCb;
148
+ // check if these coefficients were already cached
149
+ if (WorkletLowpassFilter.cachedCoefficients?.[qCb]?.[cutoffCents] === undefined)
150
+ {
151
+ let cutoffHz = absCentsToHz(cutoffCents);
152
+
153
+ // fix cutoff on low frequencies (fluid_iir_filter.c line 392)
154
+ cutoffHz = Math.min(cutoffHz, 0.45 * sampleRate);
155
+
156
+ const qDb = qCb / 10;
157
+ // correct the filter gain, like fluid does
158
+ const reasonanceGain = decibelAttenuationToGain(-1 * (qDb - 3.01)); // -1 because it's attenuation, and we don't want attenuation
159
+
160
+ // reduce the gain by the Q factor (fluid_iir_filter.c line 250)
161
+ const qGain = 1 / Math.sqrt(decibelAttenuationToGain(-qDb));
162
+
163
+
164
+ // initial filtering code was ported from meltysynth created by sinshu.
165
+ let w = 2 * Math.PI * cutoffHz / sampleRate; // we're in the AudioWorkletGlobalScope so we can use sampleRate
166
+ let cosw = Math.cos(w);
167
+ let alpha = Math.sin(w) / (2 * reasonanceGain);
168
+
169
+ let b1 = (1 - cosw) * qGain;
170
+ let b0 = b1 / 2;
171
+ let b2 = b0;
172
+ let a0 = 1 + alpha;
173
+ let a1 = -2 * cosw;
174
+ let a2 = 1 - alpha;
175
+
176
+ /**
177
+ * set coefficients
178
+ * @type {CachedCoefficient}
179
+ */
180
+ const toCache = {};
181
+ toCache.a0 = b0 / a0;
182
+ toCache.a1 = b1 / a0;
183
+ toCache.a2 = b2 / a0;
184
+ toCache.a3 = a1 / a0;
185
+ toCache.a4 = a2 / a0;
186
+
187
+ if (WorkletLowpassFilter.cachedCoefficients[qCb] === undefined)
188
+ {
189
+ WorkletLowpassFilter.cachedCoefficients[qCb] = [];
190
+ }
191
+ WorkletLowpassFilter.cachedCoefficients[qCb][cutoffCents] = toCache;
192
+ }
193
+ const cached = WorkletLowpassFilter.cachedCoefficients[qCb][cutoffCents];
194
+ filter.a0 = cached.a0;
195
+ filter.a1 = cached.a1;
196
+ filter.a2 = cached.a2;
197
+ filter.a3 = cached.a3;
198
+ filter.a4 = cached.a4;
173
199
  }
174
200
  }
@@ -1,14 +1,32 @@
1
1
  import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js";
2
2
 
3
- export const WORKLET_SYSTEM_REVERB_DIVIDER = 4600;
4
- export const WORKLET_SYSTEM_CHORUS_DIVIDER = 2000;
5
- const HALF_PI = Math.PI / 2;
6
-
7
3
  /**
8
4
  * stereo_panner.js
9
5
  * purpose: pans a given voice out to the stereo output and to the effects' outputs
10
6
  */
11
7
 
8
+ export const PAN_SMOOTHING_FACTOR = 0.1;
9
+
10
+ export const WORKLET_SYSTEM_REVERB_DIVIDER = 4600;
11
+ export const WORKLET_SYSTEM_CHORUS_DIVIDER = 2000;
12
+ const HALF_PI = Math.PI / 2;
13
+
14
+ const MIN_PAN = -500;
15
+ const MAX_PAN = 500;
16
+ const PAN_RESOLUTION = MAX_PAN - MIN_PAN;
17
+
18
+ // initialize pan lookup tables
19
+ const panTableLeft = new Float32Array(PAN_RESOLUTION + 1);
20
+ const panTableRight = new Float32Array(PAN_RESOLUTION + 1);
21
+ for (let pan = MIN_PAN; pan <= MAX_PAN; pan++)
22
+ {
23
+ // clamp to 0-1
24
+ const realPan = (pan - MIN_PAN) / PAN_RESOLUTION;
25
+ const tableIndex = pan - MIN_PAN;
26
+ panTableLeft[tableIndex] = Math.cos(HALF_PI * realPan);
27
+ panTableRight[tableIndex] = Math.sin(HALF_PI * realPan);
28
+ }
29
+
12
30
  /**
13
31
  * Pans the voice to the given output buffers
14
32
  * @param voice {WorkletVoice} the voice to pan
@@ -29,43 +47,51 @@ export function panVoice(voice,
29
47
  {
30
48
  return;
31
49
  }
32
- const pan = ((Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan])) + 500) / 1000); // 0 to 1
33
- voice.currentPan += (pan - voice.currentPan) * this.panSmoothingFactor; // smooth out pan to prevent clicking
50
+ // clamp -500 to 500
51
+ const pan = Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan]));
52
+ // smooth out pan to prevent clicking
53
+ voice.currentPan += (pan - voice.currentPan) * this.panSmoothingFactor;
34
54
 
35
55
  const gain = this.currentGain;
36
- // pan the voice and write out
37
- const gainLeft = Math.cos(HALF_PI * voice.currentPan) * gain * this.panLeft;
38
- const gainRight = Math.sin(HALF_PI * voice.currentPan) * gain * this.panRight;
39
- // disable reverb and chorus in one output mode
56
+ const index = ~~(pan + 500);
57
+ // get voice's gain levels for each channel
58
+ const gainLeft = panTableLeft[index] * gain * this.panLeft;
59
+ const gainRight = panTableRight[index] * gain * this.panRight;
40
60
 
41
- const reverbLevel = this.reverbGain * voice.modulatedGenerators[generatorTypes.reverbEffectsSend] / WORKLET_SYSTEM_REVERB_DIVIDER * gain;
42
- const chorusLevel = this.chorusGain * voice.modulatedGenerators[generatorTypes.chorusEffectsSend] / WORKLET_SYSTEM_CHORUS_DIVIDER;
43
-
44
- if (reverbLevel > 0 && !this.oneOutputMode)
61
+ // disable reverb and chorus in one output mode
62
+ if (!this.oneOutputMode)
45
63
  {
46
- const reverbLeft = reverb[0];
47
- const reverbRight = reverb[1];
48
- for (let i = 0; i < inputBuffer.length; i++)
64
+ // reverb is mono so we need to multiply by gain
65
+ const reverbLevel = this.reverbGain * voice.modulatedGenerators[generatorTypes.reverbEffectsSend] / WORKLET_SYSTEM_REVERB_DIVIDER * gain;
66
+ // chorus is stereo so we do not need to
67
+ const chorusLevel = this.chorusGain * voice.modulatedGenerators[generatorTypes.chorusEffectsSend] / WORKLET_SYSTEM_CHORUS_DIVIDER;
68
+
69
+ if (reverbLevel > 0)
49
70
  {
50
- reverbLeft[i] += reverbLevel * inputBuffer[i];
51
- reverbRight[i] += reverbLevel * inputBuffer[i];
71
+ const reverbLeft = reverb[0];
72
+ const reverbRight = reverb[1];
73
+ for (let i = 0; i < inputBuffer.length; i++)
74
+ {
75
+ reverbLeft[i] += reverbLevel * inputBuffer[i];
76
+ reverbRight[i] += reverbLevel * inputBuffer[i];
77
+ }
52
78
  }
53
- }
54
-
55
- if (chorusLevel > 0 && !this.oneOutputMode)
56
- {
57
- const chorusLeft = chorus[0];
58
- const chorusRight = chorus[1];
59
- const chorusLeftGain = gainLeft * chorusLevel;
60
- const chorusRightGain = gainRight * chorusLevel;
61
- for (let i = 0; i < inputBuffer.length; i++)
79
+
80
+ if (chorusLevel > 0)
62
81
  {
63
- chorusLeft[i] += chorusLeftGain * inputBuffer[i];
64
- chorusRight[i] += chorusRightGain * inputBuffer[i];
82
+ const chorusLeft = chorus[0];
83
+ const chorusRight = chorus[1];
84
+ const chorusLeftGain = gainLeft * chorusLevel;
85
+ const chorusRightGain = gainRight * chorusLevel;
86
+ for (let i = 0; i < inputBuffer.length; i++)
87
+ {
88
+ chorusLeft[i] += chorusLeftGain * inputBuffer[i];
89
+ chorusRight[i] += chorusRightGain * inputBuffer[i];
90
+ }
65
91
  }
66
92
  }
67
93
 
68
- // mix out the audio data
94
+ // mix down the audio data
69
95
  if (gainLeft > 0)
70
96
  {
71
97
  for (let i = 0; i < inputBuffer.length; i++)
@@ -220,10 +220,10 @@ class WorkletVoice
220
220
  currentTuningCalculated = 1;
221
221
 
222
222
  /**
223
- * From 0 to 1.
223
+ * From -500 to 500.
224
224
  * @param {number}
225
225
  */
226
- currentPan = 0.5;
226
+ currentPan = 0;
227
227
 
228
228
  /**
229
229
  * If MIDI Tuning Standard is already applied (at note-on time),
@@ -337,7 +337,7 @@ export function getWorkletVoices(channel,
337
337
  // override patch
338
338
  const overridePatch = this.keyModifierManager.hasOverridePatch(channel, midiNote);
339
339
 
340
- // overriden patch is not cached
340
+ // override patch is not cached
341
341
  if (cached !== undefined && !overridePatch)
342
342
  {
343
343
  return cached.map(v => WorkletVoice.copy(v, currentTime));