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.
- package/@types/synthetizer/synthetizer.d.ts +13 -10
- package/@types/synthetizer/worklet_system/message_protocol/worklet_message.d.ts +3 -0
- package/package.json +1 -1
- package/soundfont/dls/read_articulation.js +13 -8
- package/synthetizer/synthetizer.js +40 -15
- package/synthetizer/worklet_processor.min.js +6 -6
- package/synthetizer/worklet_system/main_processor.js +13 -0
- package/synthetizer/worklet_system/message_protocol/handle_message.js +4 -0
- package/synthetizer/worklet_system/message_protocol/worklet_message.js +3 -0
- package/synthetizer/worklet_system/worklet_methods/voice_control.js +11 -3
- package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +13 -4
- package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +73 -2
|
@@ -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
|
}
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
158
|
-
const fraction = (env.sustainDb - 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
|
|
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
|
|
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
|
}
|