spessasynth_core 1.0.0

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.
Files changed (55) hide show
  1. package/.idea/inspectionProfiles/Project_Default.xml +10 -0
  2. package/.idea/jsLibraryMappings.xml +6 -0
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/spessasynth_core.iml +12 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/README.md +376 -0
  7. package/index.js +7 -0
  8. package/package.json +34 -0
  9. package/spessasynth_core/midi_parser/README.md +3 -0
  10. package/spessasynth_core/midi_parser/midi_loader.js +381 -0
  11. package/spessasynth_core/midi_parser/midi_message.js +231 -0
  12. package/spessasynth_core/sequencer/sequencer.js +192 -0
  13. package/spessasynth_core/sequencer/worklet_sequencer/play.js +221 -0
  14. package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +138 -0
  15. package/spessasynth_core/sequencer/worklet_sequencer/process_tick.js +85 -0
  16. package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +90 -0
  17. package/spessasynth_core/soundfont/README.md +4 -0
  18. package/spessasynth_core/soundfont/chunk/generators.js +205 -0
  19. package/spessasynth_core/soundfont/chunk/instruments.js +60 -0
  20. package/spessasynth_core/soundfont/chunk/modulators.js +232 -0
  21. package/spessasynth_core/soundfont/chunk/presets.js +264 -0
  22. package/spessasynth_core/soundfont/chunk/riff_chunk.js +46 -0
  23. package/spessasynth_core/soundfont/chunk/samples.js +250 -0
  24. package/spessasynth_core/soundfont/chunk/zones.js +264 -0
  25. package/spessasynth_core/soundfont/soundfont_parser.js +301 -0
  26. package/spessasynth_core/synthetizer/README.md +6 -0
  27. package/spessasynth_core/synthetizer/synthesizer.js +303 -0
  28. package/spessasynth_core/synthetizer/worklet_system/README.md +3 -0
  29. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +285 -0
  30. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +280 -0
  31. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +102 -0
  32. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +75 -0
  33. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +140 -0
  34. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +265 -0
  35. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +105 -0
  36. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
  37. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +186 -0
  38. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
  39. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +95 -0
  40. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
  41. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
  42. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +76 -0
  43. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
  44. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +194 -0
  45. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
  46. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +173 -0
  47. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +105 -0
  48. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +313 -0
  49. package/spessasynth_core/utils/README.md +4 -0
  50. package/spessasynth_core/utils/buffer_to_wav.js +70 -0
  51. package/spessasynth_core/utils/byte_functions.js +141 -0
  52. package/spessasynth_core/utils/loggin.js +79 -0
  53. package/spessasynth_core/utils/other.js +49 -0
  54. package/spessasynth_core/utils/shiftable_array.js +26 -0
  55. package/spessasynth_core/utils/stbvorbis_sync.js +1877 -0
@@ -0,0 +1,265 @@
1
+ import { arrayToHexString, consoleColors } from '../../../utils/other.js'
2
+ import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
3
+ /**
4
+ * Executes a system exclusive
5
+ * @param messageData {number[]} - the message data without f0
6
+ * @this {Synthesizer}
7
+ */
8
+
9
+ export function systemExclusive(messageData)
10
+ {
11
+ const type = messageData[0];
12
+ switch (type)
13
+ {
14
+ default:
15
+ SpessaSynthWarn(`%cUnrecognized SysEx: %c${arrayToHexString(messageData)}`,
16
+ consoleColors.warn,
17
+ consoleColors.unrecognized);
18
+ break;
19
+
20
+ // non realtime
21
+ case 0x7E:
22
+ // gm system
23
+ if(messageData[2] === 0x09)
24
+ {
25
+ if(messageData[3] === 0x01)
26
+ {
27
+ SpessaSynthInfo("%cGM system on", consoleColors.info);
28
+ this.system = "gm";
29
+ }
30
+ else if(messageData[3] === 0x03)
31
+ {
32
+ SpessaSynthInfo("%cGM2 system on", consoleColors.info);
33
+ this.system = "gm2";
34
+ }
35
+ else
36
+ {
37
+ SpessaSynthInfo("%cGM system off, defaulting to GS", consoleColors.info);
38
+ this.system = "gs";
39
+ }
40
+ }
41
+ break;
42
+
43
+ // realtime
44
+ case 0x7F:
45
+ if(messageData[2] === 0x04 && messageData[3] === 0x01)
46
+ {
47
+ // main volume
48
+ const vol = messageData[5] << 7 | messageData[4];
49
+ this.setMainVolume(vol / 16384);
50
+ SpessaSynthInfo(`%cMaster Volume. Volume: %c${vol}`,
51
+ consoleColors.info,
52
+ consoleColors.value);
53
+ }
54
+ else
55
+ if(messageData[2] === 0x04 && messageData[3] === 0x03)
56
+ {
57
+ // fine tuning
58
+ const tuningValue = ((messageData[5] << 7) | messageData[6]) - 8192;
59
+ const cents = Math.floor(tuningValue / 81.92); // [-100;+99] cents range
60
+ this.setMasterTuning(cents);
61
+ SpessaSynthInfo(`%cMaster Fine Tuning. Cents: %c${cents}`,
62
+ consoleColors.info,
63
+ consoleColors.value)
64
+ }
65
+ else
66
+ if(messageData[2] === 0x04 && messageData[3] === 0x04)
67
+ {
68
+ // coarse tuning
69
+ // lsb is ignored
70
+ const semitones = messageData[5] - 64;
71
+ const cents = semitones * 100;
72
+ this.setMasterTuning(cents);
73
+ SpessaSynthInfo(`%cMaster Coarse Tuning. Cents: %c${cents}`,
74
+ consoleColors.info,
75
+ consoleColors.value)
76
+ }
77
+ else
78
+ {
79
+ SpessaSynthWarn(
80
+ `%cUnrecognized MIDI Real-time message: %c${arrayToHexString(messageData)}`,
81
+ consoleColors.warn,
82
+ consoleColors.unrecognized)
83
+ }
84
+ break;
85
+
86
+ // this is a roland sysex
87
+ // http://www.bandtrax.com.au/sysex.htm
88
+ // https://cdn.roland.com/assets/media/pdf/AT-20R_30R_MI.pdf
89
+ case 0x41:
90
+ // messagedata[1] is device id (ignore as we're everything >:) )
91
+ if(messageData[2] === 0x42 && messageData[3] === 0x12)
92
+ {
93
+ // this is a GS sysex
94
+ // messageData[5] and [6] is the system parameter, messageData[7] is the value
95
+ const messageValue = messageData[7];
96
+ if(messageData[6] === 0x7F)
97
+ {
98
+ // GS mode set
99
+ if(messageValue === 0x00) {
100
+ // this is a GS reset
101
+ SpessaSynthInfo("%cGS system on", consoleColors.info);
102
+ this.system = "gs";
103
+ }
104
+ else if(messageValue === 0x7F)
105
+ {
106
+ // GS mode off
107
+ SpessaSynthInfo("%cGS system off, switching to GM2", consoleColors.info);
108
+ this.system = "gm2";
109
+ }
110
+ return;
111
+ }
112
+ else
113
+ if(messageData[4] === 0x40)
114
+ {
115
+ // this is a system parameter
116
+ if((messageData[5] & 0x10) > 0)
117
+ {
118
+ // this is an individual part (channel) parameter
119
+ // determine the channel 0 means channel 10 (default), 1 means 1 etc.
120
+ const channel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][messageData[5] & 0x0F]; // for example 1A means A = 11, which corresponds to channel 12 (counting from 1)
121
+ switch (messageData[6])
122
+ {
123
+ default:
124
+ break;
125
+
126
+ case 0x15:
127
+ // this is the Use for Drum Part sysex (multiple drums)
128
+ this.setDrums(channel, messageValue > 0 && messageData[5] >> 4); // if set to other than 0, is a drum channel
129
+ SpessaSynthInfo(
130
+ `%cChannel %c${channel}%c ${messageValue > 0 && messageData[5] >> 4 ?
131
+ "is now a drum channel"
132
+ :
133
+ "now isn't a drum channel"
134
+ }%c via: %c${arrayToHexString(messageData)}`,
135
+ consoleColors.info,
136
+ consoleColors.value,
137
+ consoleColors.recognized,
138
+ consoleColors.info,
139
+ consoleColors.value);
140
+ return;
141
+
142
+ case 0x16:
143
+ // this is the pitch key shift sysex
144
+ const keyShift = messageValue - 64;
145
+ this.transposeChannel(channel, keyShift);
146
+ SpessaSynthInfo(`%cChannel %c${channel}%c pitch shift. Semitones %c${keyShift}%c, with %c${arrayToHexString(messageData)}`,
147
+ consoleColors.info,
148
+ consoleColors.recognized,
149
+ consoleColors.info,
150
+ consoleColors.value,
151
+ consoleColors.info,
152
+ consoleColors.value);
153
+ return;
154
+
155
+ case 0x40:
156
+ case 0x41:
157
+ case 0x42:
158
+ case 0x43:
159
+ case 0x44:
160
+ case 0x45:
161
+ case 0x46:
162
+ case 0x47:
163
+ case 0x48:
164
+ case 0x49:
165
+ case 0x4A:
166
+ case 0x4B:
167
+ // scale tuning
168
+ const cents = messageValue - 64;
169
+ SpessaSynthInfo(`%cChannel %c${channel}%c tuning. Cents %c${cents}%c, with %c${arrayToHexString(messageData)}`,
170
+ consoleColors.info,
171
+ consoleColors.recognized,
172
+ consoleColors.info,
173
+ consoleColors.value,
174
+ consoleColors.info,
175
+ consoleColors.value);
176
+ this.setChannelTuning(channel, cents);
177
+ }
178
+ }
179
+ else
180
+ // this is a global system parameter
181
+ if(messageData[5] === 0x00 && messageData[6] === 0x06)
182
+ {
183
+ // roland master pan
184
+ SpessaSynthInfo(`%cRoland GS Master Pan set to: %c${messageValue}%c with: %c${arrayToHexString(messageData)}`,
185
+ consoleColors.info,
186
+ consoleColors.value,
187
+ consoleColors.info,
188
+ consoleColors.value);
189
+ this.setMasterPan((messageValue - 64) / 64);
190
+ return;
191
+ }
192
+ else
193
+ if(messageData[5] === 0x00 && messageData[6] === 0x05)
194
+ {
195
+ // roland master key shift (transpose)
196
+ const transpose = messageValue - 64;
197
+ SpessaSynthInfo(`%cRoland GS Master Key-Shift set to: %c${transpose}%c with: %c${arrayToHexString(messageData)}`,
198
+ consoleColors.info,
199
+ consoleColors.value,
200
+ consoleColors.info,
201
+ consoleColors.value);
202
+ this.setMasterTuning(transpose * 100);
203
+ return;
204
+ }
205
+ else
206
+ if(messageData[5] === 0x00 && messageData[6] === 0x04)
207
+ {
208
+ // roland GS master volume
209
+ SpessaSynthInfo(`%cRoland GS Master Volume set to: %c${messageValue}%c with: %c${arrayToHexString(messageData)}`,
210
+ consoleColors.info,
211
+ consoleColors.value,
212
+ consoleColors.info,
213
+ consoleColors.value);
214
+ this.setMainVolume(messageValue / 127);
215
+ return;
216
+ }
217
+ }
218
+ // this is some other GS sysex...
219
+ SpessaSynthWarn(`%cUnrecognized Roland %cGS %cSysEx: %c${arrayToHexString(messageData)}`,
220
+ consoleColors.warn,
221
+ consoleColors.recognized,
222
+ consoleColors.warn,
223
+ consoleColors.unrecognized);
224
+ return;
225
+ }
226
+ else
227
+ if(messageData[2] === 0x16 && messageData[3] === 0x12 && messageData[4] === 0x10)
228
+ {
229
+ // this is a roland master volume message
230
+ this.setMainVolume(messageData[7] / 100);
231
+ SpessaSynthInfo(`%cRoland Master Volume control set to: %c${messageData[7]}%c via: %c${arrayToHexString(messageData)}`,
232
+ consoleColors.info,
233
+ consoleColors.value,
234
+ consoleColors.info,
235
+ consoleColors.value);
236
+ return;
237
+ }
238
+ else
239
+ {
240
+ // this is something else...
241
+ SpessaSynthWarn(`%cUnrecognized Roland SysEx: %c${arrayToHexString(messageData)}`,
242
+ consoleColors.warn,
243
+ consoleColors.unrecognized);
244
+ return;
245
+ }
246
+
247
+ // yamaha
248
+ case 0x43:
249
+ // XG on
250
+ if(messageData[2] === 0x4C && messageData[5] === 0x7E && messageData[6] === 0x00)
251
+ {
252
+ SpessaSynthInfo("%cXG system on", consoleColors.info);
253
+ this.system = "xg";
254
+ }
255
+ else
256
+ {
257
+ SpessaSynthWarn(`%cUnrecognized Yamaha SysEx: %c${arrayToHexString(messageData)}`,
258
+ consoleColors.warn,
259
+ consoleColors.unrecognized);
260
+ }
261
+ break;
262
+
263
+
264
+ }
265
+ }
@@ -0,0 +1,105 @@
1
+ import { customControllers, NON_CC_INDEX_OFFSET } from '../worklet_utilities/worklet_processor_channel.js'
2
+ import { consoleColors } from '../../../utils/other.js'
3
+ import { modulatorSources } from '../../../soundfont/chunk/modulators.js'
4
+ import { computeModulators } from '../worklet_utilities/worklet_modulator.js'
5
+ import { SpessaSynthInfo } from '../../../utils/loggin.js'
6
+
7
+ /**
8
+ * Transposes all channels by given amount of semitones
9
+ * @this {Synthesizer}
10
+ * @param semitones {number} Can be float
11
+ * @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel
12
+ */
13
+ export function transposeAllChannels(semitones, force = false)
14
+ {
15
+ this.transposition = 0;
16
+ for (let i = 0; i < this.workletProcessorChannels.length; i++) {
17
+ this.transposeChannel(i, semitones, force);
18
+ }
19
+ this.transposition = semitones;
20
+ }
21
+
22
+ /**
23
+ * Transposes the channel by given amount of semitones
24
+ * @this {Synthesizer}
25
+ * @param channel {number}
26
+ * @param semitones {number} Can be float
27
+ * @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel
28
+ */
29
+ export function transposeChannel(channel, semitones, force=false)
30
+ {
31
+ semitones += this.transposition;
32
+ const channelObject = this.workletProcessorChannels[channel];
33
+ if(channelObject.drumChannel && !force)
34
+ {
35
+ return;
36
+ }
37
+ channelObject.customControllers[customControllers.channelTranspose] = semitones * 100;
38
+ }
39
+
40
+ /**
41
+ * Sets the channel's tuning
42
+ * @this {Synthesizer}
43
+ * @param channel {number}
44
+ * @param cents {number}
45
+ */
46
+ export function setChannelTuning(channel, cents)
47
+ {
48
+ const channelObject = this.workletProcessorChannels[channel];
49
+ cents = Math.round(cents);
50
+ channelObject.customControllers[customControllers.channelTuning] = cents;
51
+ SpessaSynthInfo(`%cChannel ${channel} tuning. Cents: %c${cents}`,
52
+ consoleColors.info,
53
+ consoleColors.value);
54
+ }
55
+
56
+ /**
57
+ * Sets the worklet's master tuning
58
+ * @this {Synthesizer}
59
+ * @param cents {number}
60
+ */
61
+ export function setMasterTuning(cents)
62
+ {
63
+ cents = Math.round(cents);
64
+ for (let i = 0; i < this.workletProcessorChannels.length; i++) {
65
+ this.workletProcessorChannels[i].customControllers[customControllers.masterTuning] = cents;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * @this {Synthesizer}
71
+ * @param channel {number}
72
+ * @param cents {number}
73
+ */
74
+ export function setModulationDepth(channel, cents)
75
+ {
76
+ let channelObject = this.workletProcessorChannels[channel];
77
+ cents = Math.round(cents);
78
+ SpessaSynthInfo(`%cChannel ${channel} modulation depth. Cents: %c${cents}`,
79
+ consoleColors.info,
80
+ consoleColors.value);
81
+ /* ==============
82
+ IMPORTANT
83
+ here we convert cents into a multiplier.
84
+ midi spec assumes the default is 50 cents,
85
+ but it might be different for the soundfont,
86
+ so we create a multiplier by divinging cents by 50.
87
+ for example, if we want 100 cents, then multiplier will be 2,
88
+ which for a preset with depth of 50 will create 100.
89
+ ================*/
90
+ channelObject.customControllers[customControllers.modulationMultiplier] = cents / 50;
91
+ }
92
+
93
+ /**
94
+ * Sets the pitch of the given channel
95
+ * @this {Synthesizer}
96
+ * @param channel {number} usually 0-15: the channel to change pitch
97
+ * @param MSB {number} SECOND byte of the MIDI pitchWheel message
98
+ * @param LSB {number} FIRST byte of the MIDI pitchWheel message
99
+ */
100
+ export function pitchWheel(channel, MSB, LSB)
101
+ {
102
+ const bend = (LSB | (MSB << 7));
103
+ this.workletProcessorChannels[channel].midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = bend;
104
+ this.workletProcessorChannels[channel].voices.forEach(v => computeModulators(v, this.workletProcessorChannels[channel].midiControllers));
105
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @param channel {number}
3
+ * @this {Synthesizer}
4
+ */
5
+ export function disableAndLockVibrato(channel)
6
+ {
7
+ this.workletProcessorChannels[channel].lockVibrato = true;
8
+ this.workletProcessorChannels[channel].channelVibrato.rate = 0;
9
+ this.workletProcessorChannels[channel].channelVibrato.delay = 0;
10
+ this.workletProcessorChannels[channel].channelVibrato.depth = 0;
11
+ }
12
+
13
+ /**
14
+ * @param channel {number}
15
+ * @param depth {number}
16
+ * @param rate {number}
17
+ * @param delay {number}
18
+ * @this {Synthesizer}
19
+ */
20
+ export function setVibrato(channel, depth, rate, delay)
21
+ {
22
+ if(this.workletProcessorChannels[channel].lockVibrato)
23
+ {
24
+ return;
25
+ }
26
+ this.workletProcessorChannels[channel].vibrato.rate = rate;
27
+ this.workletProcessorChannels[channel].vibrato.delay = delay;
28
+ this.workletProcessorChannels[channel].vibrato.depth = depth;
29
+ }
@@ -0,0 +1,186 @@
1
+ import { generatorTypes } from '../../../soundfont/chunk/generators.js'
2
+ import { absCentsToHz, timecentsToSeconds } from '../worklet_utilities/unit_converter.js'
3
+ import { getLFOValue } from '../worklet_utilities/lfo.js'
4
+ import { customControllers } from '../worklet_utilities/worklet_processor_channel.js'
5
+ import { getModEnvValue } from '../worklet_utilities/modulation_envelope.js'
6
+ import { getOscillatorData } from '../worklet_utilities/wavetable_oscillator.js'
7
+ import { panVoice } from '../worklet_utilities/stereo_panner.js'
8
+ import { applyVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
9
+ import { applyLowpassFilter } from '../worklet_utilities/lowpass_filter.js'
10
+ import { MIN_NOTE_LENGTH } from '../../synthesizer.js'
11
+
12
+ /**
13
+ * Renders a voice to the stereo output buffer
14
+ * @param channel {WorkletProcessorChannel} the voice's channel
15
+ * @param voice {WorkletVoice} the voice to render
16
+ * @param output {Float32Array[]} the output buffer
17
+ * @param reverbOutput {Float32Array[]} output for reverb
18
+ * @param chorusOutput {Float32Array[]} output for chorus
19
+ * @this {Synthesizer}
20
+ */
21
+ export function renderVoice(channel, voice, output, reverbOutput, chorusOutput)
22
+ {
23
+ // if no matching sample, perhaps it's still being loaded...?
24
+ if(this.workletDumpedSamplesList[voice.sample.sampleID] === undefined)
25
+ {
26
+ return;
27
+ }
28
+
29
+ // check if release
30
+ if(!voice.isInRelease) {
31
+ // if not in release, check if the release time is
32
+ if (this.currentTime >= voice.releaseStartTime) {
33
+ voice.releaseStartModEnv = voice.currentModEnvValue;
34
+ voice.isInRelease = true;
35
+ }
36
+ }
37
+
38
+
39
+ // if the initial attenuation is more than 100dB, skip the voice (it's silent anyway)
40
+ if(voice.modulatedGenerators[generatorTypes.initialAttenuation] > 2500)
41
+ {
42
+ if(voice.isInRelease)
43
+ {
44
+ voice.finished = true;
45
+ }
46
+ return;
47
+ }
48
+
49
+ // TUNING
50
+
51
+ // calculate tuning
52
+ let cents = voice.modulatedGenerators[generatorTypes.fineTune]
53
+ + channel.customControllers[customControllers.channelTuning]
54
+ + channel.customControllers[customControllers.channelTranspose]
55
+ + channel.customControllers[customControllers.masterTuning];
56
+ let semitones = voice.modulatedGenerators[generatorTypes.coarseTune];
57
+
58
+ // calculate tuning by key
59
+ cents += (voice.targetKey - voice.sample.rootKey) * voice.modulatedGenerators[generatorTypes.scaleTuning];
60
+
61
+ // vibrato LFO
62
+ const vibratoDepth = voice.modulatedGenerators[generatorTypes.vibLfoToPitch];
63
+ if(vibratoDepth > 0)
64
+ {
65
+ const vibStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVibLFO]);
66
+ const vibFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqVibLFO]);
67
+ const lfoVal = getLFOValue(vibStart, vibFreqHz, this.currentTime);
68
+ if(lfoVal)
69
+ {
70
+ cents += lfoVal * (vibratoDepth * channel.customControllers[customControllers.modulationMultiplier]);
71
+ }
72
+ }
73
+
74
+ // lowpass frequency
75
+ let lowpassCents = voice.modulatedGenerators[generatorTypes.initialFilterFc];
76
+
77
+ // mod LFO
78
+ const modPitchDepth = voice.modulatedGenerators[generatorTypes.modLfoToPitch];
79
+ const modVolDepth = voice.modulatedGenerators[generatorTypes.modLfoToVolume];
80
+ const modFilterDepth = voice.modulatedGenerators[generatorTypes.modLfoToFilterFc];
81
+ let modLfoCentibels = 0;
82
+ if(modPitchDepth + modFilterDepth + modVolDepth > 0)
83
+ {
84
+ const modStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModLFO]);
85
+ const modFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqModLFO]);
86
+ const modLfoValue = getLFOValue(modStart, modFreqHz, this.currentTime);
87
+ cents += modLfoValue * (modPitchDepth * channel.customControllers[customControllers.modulationMultiplier]);
88
+ modLfoCentibels = modLfoValue * modVolDepth;
89
+ lowpassCents += modLfoValue * modFilterDepth;
90
+ }
91
+
92
+ // channel vibrato (GS NRPN)
93
+ if(channel.channelVibrato.depth > 0)
94
+ {
95
+ const channelVibrato = getLFOValue(voice.startTime + channel.channelVibrato.delay, channel.channelVibrato.rate, this.currentTime);
96
+ if(channelVibrato)
97
+ {
98
+ cents += channelVibrato * channel.channelVibrato.depth;
99
+ }
100
+ }
101
+
102
+ // mod env
103
+ const modEnvPitchDepth = voice.modulatedGenerators[generatorTypes.modEnvToPitch];
104
+ const modEnvFilterDepth = voice.modulatedGenerators[generatorTypes.modEnvToFilterFc];
105
+ const modEnv = getModEnvValue(voice, this.currentTime);
106
+ lowpassCents += modEnv * modEnvFilterDepth;
107
+ cents += modEnv * modEnvPitchDepth;
108
+
109
+ // finally calculate the playback rate
110
+ const centsTotal = ~~(cents + semitones * 100);
111
+ if(centsTotal !== voice.currentTuningCents)
112
+ {
113
+ voice.currentTuningCents = centsTotal;
114
+ voice.currentTuningCalculated = Math.pow(2, centsTotal / 1200);
115
+ }
116
+
117
+ // PANNING
118
+ const pan = ( (Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) ; // 0 to 1
119
+
120
+ // SYNTHESIS
121
+ const bufferOut = new Float32Array(output[0].length);
122
+
123
+ // wavetable oscillator
124
+ getOscillatorData(voice, this.workletDumpedSamplesList[voice.sample.sampleID], bufferOut);
125
+
126
+
127
+ // lowpass filter
128
+ applyLowpassFilter(voice, bufferOut, lowpassCents, this.sampleRate);
129
+
130
+ // volenv
131
+ applyVolumeEnvelope(voice, bufferOut, this.currentTime, modLfoCentibels, this.sampleTime);
132
+
133
+ // pan the voice and write out
134
+ voice.currentPan += (pan - voice.currentPan) * 0.1; // smooth out pan to prevent clicking
135
+ const panLeft = (1 - voice.currentPan) * this.panLeft;
136
+ const panRight = voice.currentPan * this.panRight;
137
+ panVoice(
138
+ panLeft,
139
+ panRight,
140
+ bufferOut,
141
+ output,
142
+ reverbOutput, voice.modulatedGenerators[generatorTypes.reverbEffectsSend],
143
+ chorusOutput, voice.modulatedGenerators[generatorTypes.chorusEffectsSend]);
144
+ }
145
+
146
+
147
+ /**
148
+ * @this {Synthesizer}
149
+ * @param amount {number}
150
+ */
151
+ export function voiceKilling(amount)
152
+ {
153
+ // kill the smallest velocity voices
154
+ let voicesOrderedByVelocity = this.workletProcessorChannels.map(channel => channel.voices);
155
+
156
+ /**
157
+ * @type {WorkletVoice[]}
158
+ */
159
+ voicesOrderedByVelocity = voicesOrderedByVelocity.flat();
160
+ voicesOrderedByVelocity.sort((v1, v2) => v1.velocity - v2.velocity);
161
+ if(voicesOrderedByVelocity.length < amount)
162
+ {
163
+ amount = voicesOrderedByVelocity.length;
164
+ }
165
+ for (let i = 0; i < amount; i++) {
166
+ const voice = voicesOrderedByVelocity[i];
167
+ this.workletProcessorChannels[voice.channelNumber].voices
168
+ .splice(this.workletProcessorChannels[voice.channelNumber].voices.indexOf(voice), 1);
169
+ this.totalVoicesAmount--;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Stops the voice
175
+ * @param voice {WorkletVoice} the voice to stop
176
+ * @this {Synthesizer}
177
+ */
178
+ export function releaseVoice(voice)
179
+ {
180
+ voice.releaseStartTime = this.currentTime;
181
+ // check if the note is shorter than the min note time, if so, extend it
182
+ if(voice.releaseStartTime - voice.startTime < MIN_NOTE_LENGTH)
183
+ {
184
+ voice.releaseStartTime = voice.startTime + MIN_NOTE_LENGTH;
185
+ }
186
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * lfo.js
3
+ * purpose: low frequency triangel oscillator
4
+ */
5
+
6
+ /**
7
+ * Calculates a triangular wave value for the given time
8
+ * @param startTime {number} seconds
9
+ * @param frequency {number} Hz
10
+ * @param currentTime {number} seconds
11
+ * @return {number} the value from -1 to 1
12
+ */
13
+ export function getLFOValue(startTime, frequency, currentTime) {
14
+ if (currentTime < startTime) {
15
+ return 0;
16
+ }
17
+
18
+ const xVal = (currentTime - startTime) / (1 / frequency) - 0.25;
19
+ // offset by -0.25, otherwise we start at -1 and can have unexpected jump in pitch or lowpass (happened with Synth Strings 2)
20
+
21
+ // triangle, not sine
22
+ return Math.abs(xVal - (~~(xVal + 0.5))) * 4 - 1;
23
+ }