spessasynth_lib 3.20.6 → 3.20.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.
@@ -38,6 +38,7 @@ import {
38
38
  } from './worklet_methods/program_control.js'
39
39
  import { applySynthesizerSnapshot, sendSynthesizerSnapshot } from './worklet_methods/snapshot.js'
40
40
  import { WorkletSoundfontManager } from './worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js'
41
+ import { interpolationTypes } from './worklet_utilities/wavetable_oscillator.js'
41
42
 
42
43
 
43
44
  /**
@@ -45,6 +46,8 @@ import { WorkletSoundfontManager } from './worklet_methods/worklet_soundfont_man
45
46
  * purpose: manages the synthesizer (and worklet sequencer) from the AudioWorkletGlobalScope and renders the audio data
46
47
  */
47
48
 
49
+ const WORKLET_PROCESSOR_VERSION = "3.20.9";
50
+
48
51
  export const MIN_NOTE_LENGTH = 0.07; // if the note is released faster than that, it forced to last that long
49
52
 
50
53
  export const SYNTHESIZER_GAIN = 1.0;
@@ -79,6 +82,12 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
79
82
  */
80
83
  this.deviceID = ALL_CHANNELS_OR_DIFFERENT_ACTION;
81
84
 
85
+ /**
86
+ * Interpolation type used
87
+ * @type {interpolationTypes}
88
+ */
89
+ this.interpolationType = interpolationTypes.linear;
90
+
82
91
  /**
83
92
  * @type {function}
84
93
  */
@@ -218,6 +227,10 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
218
227
 
219
228
  stbvorbis.isInitialized.then(() => {
220
229
  this.postReady();
230
+ this.post({
231
+ messageType: returnMessageType.identify,
232
+ messageData: WORKLET_PROCESSOR_VERSION
233
+ });
221
234
  SpessaSynthInfo("%cSpessaSynth is ready!", consoleColors.recognized);
222
235
  });
223
236
  }
@@ -141,6 +141,10 @@ export function handleMessage(message)
141
141
  case masterParameterType.voicesCap:
142
142
  this.voiceCap = value;
143
143
  break;
144
+
145
+ case masterParameterType.interpolationType:
146
+ this.interpolationType = value;
147
+ break;
144
148
  }
145
149
  break;
146
150
 
@@ -64,6 +64,7 @@ export const masterParameterType = {
64
64
  mainVolume: 0,
65
65
  masterPan: 1,
66
66
  voicesCap: 2,
67
+ interpolationType: 3
67
68
  }
68
69
 
69
70
 
@@ -106,6 +107,7 @@ export const ALL_CHANNELS_OR_DIFFERENT_ACTION = -1;
106
107
  * 4 - synthesizer snapshot -> snapshot<SynthesizerSnapshot> note: refer to snapshot.js
107
108
  * 5 - ready -> (no data)
108
109
  * 6 - soundfontError -> errorMessage<string>
110
+ * 7 - idenfity -> version<string>
109
111
  */
110
112
 
111
113
  /**
@@ -119,4 +121,5 @@ export const returnMessageType = {
119
121
  synthesizerSnapshot: 4,
120
122
  ready: 5,
121
123
  soundfontError: 6,
124
+ identify: 7,
122
125
  }
@@ -3,7 +3,7 @@ import { absCentsToHz, timecentsToSeconds } from '../worklet_utilities/unit_conv
3
3
  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
- import { getOscillatorData } from '../worklet_utilities/wavetable_oscillator.js'
6
+ import { getSampleLinear, getSampleNearest, interpolationTypes } from '../worklet_utilities/wavetable_oscillator.js'
7
7
  import { panVoice } from '../worklet_utilities/stereo_panner.js'
8
8
  import { applyLowpassFilter } from '../worklet_utilities/lowpass_filter.js'
9
9
  import { MIN_NOTE_LENGTH } from '../main_processor.js'
@@ -108,7 +108,8 @@ export function renderVoice(
108
108
  // use modulation multiplier (RPN modulation depth)
109
109
  cents += modLfoValue * (modPitchDepth * channel.customControllers[customControllers.modulationMultiplier]);
110
110
  // volenv volume offset
111
- modLfoCentibels = modLfoValue * modVolDepth;
111
+ // the lfo returns from -1 to 1, we change it to 0-1 here because the volume excursion is only positive
112
+ modLfoCentibels = (modLfoValue / 2 + 0.5) * modVolDepth;
112
113
  // lowpass frequency
113
114
  lowpassCents += modLfoValue * modFilterDepth;
114
115
  }
@@ -147,7 +148,14 @@ export function renderVoice(
147
148
  const bufferOut = new Float32Array(outputLeft.length);
148
149
 
149
150
  // wavetable oscillator
150
- getOscillatorData(voice, this.workletDumpedSamplesList[voice.sample.sampleID], bufferOut);
151
+ if(this.interpolationType === interpolationTypes.linear)
152
+ {
153
+ getSampleLinear(voice, this.workletDumpedSamplesList[voice.sample.sampleID], bufferOut);
154
+ }
155
+ else
156
+ {
157
+ getSampleNearest(voice, this.workletDumpedSamplesList[voice.sample.sampleID], bufferOut);
158
+ }
151
159
 
152
160
  // lowpass filter
153
161
  applyLowpassFilter(voice, bufferOut, lowpassCents);
@@ -145,7 +145,7 @@ export class WorkletVolumeEnvelope
145
145
  }
146
146
  // calculate absolute times (they can change so we have to recalculate every time
147
147
  env.attenuation = voice.modulatedGenerators[generatorTypes.initialAttenuation] / 10; // divide by ten to get decibelts
148
- env.sustainDb = voice.volumeEnvelope.attenuation + voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10;
148
+ env.sustainDb = Math.min(100, voice.volumeEnvelope.attenuation + voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10);
149
149
 
150
150
  // calculate durations
151
151
  env.attackDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.attackVolEnv]);
@@ -154,8 +154,8 @@ export class WorkletVolumeEnvelope
154
154
  // therefore we need to calculate the real time
155
155
  // (changing from attenuation to sustain instead of -100dB)
156
156
  const fullChange = voice.modulatedGenerators[generatorTypes.decayVolEnv];
157
- const keyNumAddition = ((60 - voice.targetKey) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay]);
158
- const fraction = (env.sustainDb - env.attenuation) / (100 - env.attenuation);
157
+ const keyNumAddition = (60 - voice.targetKey) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay];
158
+ const fraction = (env.sustainDb - env.attenuation) / 100;
159
159
  env.decayDuration = timecentsToSamples(fullChange + keyNumAddition) * fraction;
160
160
 
161
161
  env.releaseDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
@@ -334,7 +334,7 @@ export class WorkletVolumeEnvelope
334
334
  case 3:
335
335
  // decay phase: linear ramp from attenuation to sustain
336
336
  const dbDifference = env.sustainDb - env.attenuation;
337
- while(env.currentSampleTime++ < env.decayEnd)
337
+ while(env.currentSampleTime < env.decayEnd)
338
338
  {
339
339
  const newAttenuation = (1 - (env.decayEnd - env.currentSampleTime) / env.decayDuration) * dbDifference + env.attenuation;
340
340
  audioBuffer[filledBuffer] *= WorkletVolumeEnvelope.getInterpolatedGain(env, newAttenuation + decibelOffset, smoothingFactor);
@@ -350,6 +350,15 @@ export class WorkletVolumeEnvelope
350
350
 
351
351
  case 4:
352
352
  // sustain phase: stay at sustain
353
+ if(env.sustainDb > PERCEIVED_DB_SILENCE)
354
+ {
355
+ voice.finished = true;
356
+ while(filledBuffer < audioBuffer.length)
357
+ {
358
+ audioBuffer[filledBuffer++] = 0;
359
+ }
360
+ return;
361
+ }
353
362
  while(true)
354
363
  {
355
364
  audioBuffer[filledBuffer] *= WorkletVolumeEnvelope.getInterpolatedGain(env, env.sustainDb + decibelOffset, smoothingFactor);
@@ -3,14 +3,23 @@
3
3
  * purpose: plays back raw audio data at an arbitrary playback rate
4
4
  */
5
5
 
6
+ /**
7
+ *
8
+ * @enum {number}
9
+ */
10
+ export const interpolationTypes = {
11
+ linear: 0,
12
+ nearestNeighbor: 1
13
+ }
14
+
6
15
 
7
16
  /**
8
- * Fills the output buffer with raw sample data
17
+ * Fills the output buffer with raw sample data using linear interpolation
9
18
  * @param voice {WorkletVoice} the voice we're working on
10
19
  * @param sampleData {Float32Array} the sample data to write with
11
20
  * @param outputBuffer {Float32Array} the output buffer to write to
12
21
  */
13
- export function getOscillatorData(voice, sampleData, outputBuffer)
22
+ export function getSampleLinear(voice, sampleData, outputBuffer)
14
23
  {
15
24
  let cur = voice.sample.cursor;
16
25
  const loop = (voice.sample.loopingMode === 1) || (voice.sample.loopingMode === 3 && !voice.isInRelease);
@@ -93,4 +102,66 @@ export function getOscillatorData(voice, sampleData, outputBuffer)
93
102
  }
94
103
  }
95
104
  voice.sample.cursor = cur;
105
+ }
106
+
107
+ /**
108
+ * Fills the output buffer with raw sample data using no interpolation (nearest neighbor)
109
+ * @param voice {WorkletVoice} the voice we're working on
110
+ * @param sampleData {Float32Array} the sample data to write with
111
+ * @param outputBuffer {Float32Array} the output buffer to write to
112
+ */
113
+ export function getSampleNearest(voice, sampleData, outputBuffer)
114
+ {
115
+ let cur = voice.sample.cursor;
116
+ const loop = (voice.sample.loopingMode === 1) || (voice.sample.loopingMode === 3 && !voice.isInRelease);
117
+ const loopLength = voice.sample.loopEnd - voice.sample.loopStart;
118
+
119
+ if(loop)
120
+ {
121
+ for (let i = 0; i < outputBuffer.length; i++)
122
+ {
123
+ // check for loop
124
+ while(cur >= voice.sample.loopEnd)
125
+ {
126
+ cur -= loopLength;
127
+ }
128
+
129
+ // grab the nearest neighbor
130
+ let ceil = ~~cur + 1;
131
+
132
+ while(ceil >= voice.sample.loopEnd)
133
+ {
134
+ ceil -= loopLength;
135
+ }
136
+
137
+ outputBuffer[i] = sampleData[ceil];
138
+ cur += voice.sample.playbackStep * voice.currentTuningCalculated;
139
+ }
140
+ }
141
+ else
142
+ {
143
+ // check and correct end errors
144
+ if(voice.sample.end >= sampleData.length)
145
+ {
146
+ voice.sample.end = sampleData.length - 1;
147
+ }
148
+ for (let i = 0; i < outputBuffer.length; i++)
149
+ {
150
+
151
+ // nearest neighbor
152
+ const ceil = ~~cur + 1;
153
+
154
+ // flag the voice as finished if needed
155
+ if(ceil >= voice.sample.end)
156
+ {
157
+ voice.finished = true;
158
+ return;
159
+ }
160
+
161
+ //nearest neighbor (uncomment to use)
162
+ outputBuffer[i] = sampleData[ceil];
163
+ cur += voice.sample.playbackStep * voice.currentTuningCalculated;
164
+ }
165
+ }
166
+ voice.sample.cursor = cur;
96
167
  }