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.
- package/.idea/inspectionProfiles/Project_Default.xml +10 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/spessasynth_core.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +376 -0
- package/index.js +7 -0
- package/package.json +34 -0
- package/spessasynth_core/midi_parser/README.md +3 -0
- package/spessasynth_core/midi_parser/midi_loader.js +381 -0
- package/spessasynth_core/midi_parser/midi_message.js +231 -0
- package/spessasynth_core/sequencer/sequencer.js +192 -0
- package/spessasynth_core/sequencer/worklet_sequencer/play.js +221 -0
- package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +138 -0
- package/spessasynth_core/sequencer/worklet_sequencer/process_tick.js +85 -0
- package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +90 -0
- package/spessasynth_core/soundfont/README.md +4 -0
- package/spessasynth_core/soundfont/chunk/generators.js +205 -0
- package/spessasynth_core/soundfont/chunk/instruments.js +60 -0
- package/spessasynth_core/soundfont/chunk/modulators.js +232 -0
- package/spessasynth_core/soundfont/chunk/presets.js +264 -0
- package/spessasynth_core/soundfont/chunk/riff_chunk.js +46 -0
- package/spessasynth_core/soundfont/chunk/samples.js +250 -0
- package/spessasynth_core/soundfont/chunk/zones.js +264 -0
- package/spessasynth_core/soundfont/soundfont_parser.js +301 -0
- package/spessasynth_core/synthetizer/README.md +6 -0
- package/spessasynth_core/synthetizer/synthesizer.js +303 -0
- package/spessasynth_core/synthetizer/worklet_system/README.md +3 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +285 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +280 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +102 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +75 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +140 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +265 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +105 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +186 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +95 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +76 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +194 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +173 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +105 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +313 -0
- package/spessasynth_core/utils/README.md +4 -0
- package/spessasynth_core/utils/buffer_to_wav.js +70 -0
- package/spessasynth_core/utils/byte_functions.js +141 -0
- package/spessasynth_core/utils/loggin.js +79 -0
- package/spessasynth_core/utils/other.js +49 -0
- package/spessasynth_core/utils/shiftable_array.js +26 -0
- package/spessasynth_core/utils/stbvorbis_sync.js +1877 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { consoleColors } from '../../../utils/other.js'
|
|
2
|
+
import { midiControllers } from '../../../midi_parser/midi_message.js'
|
|
3
|
+
import {
|
|
4
|
+
customControllers,
|
|
5
|
+
dataEntryStates,
|
|
6
|
+
NON_CC_INDEX_OFFSET,
|
|
7
|
+
} from '../worklet_utilities/worklet_processor_channel.js'
|
|
8
|
+
import { modulatorSources } from '../../../soundfont/chunk/modulators.js'
|
|
9
|
+
import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Executes a data entry for an NRP for a sc88pro NRP (because touhou yes) and RPN tuning
|
|
13
|
+
* @param channel {number}
|
|
14
|
+
* @param dataValue {number} dataEntryCoarse MSB
|
|
15
|
+
* @this {Synthesizer}
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
export function dataEntryCoarse(channel, dataValue)
|
|
19
|
+
{
|
|
20
|
+
const channelObject = this.workletProcessorChannels[channel];
|
|
21
|
+
let addDefaultVibrato = () =>
|
|
22
|
+
{
|
|
23
|
+
if(channelObject.channelVibrato.delay === 0 && channelObject.channelVibrato.rate === 0 && channelObject.channelVibrato.depth === 0)
|
|
24
|
+
{
|
|
25
|
+
channelObject.channelVibrato.depth = 50;
|
|
26
|
+
channelObject.channelVibrato.rate = 8;
|
|
27
|
+
channelObject.channelVibrato.delay = 0.6;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
switch(channelObject.dataEntryState)
|
|
31
|
+
{
|
|
32
|
+
default:
|
|
33
|
+
case dataEntryStates.Idle:
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
// https://cdn.roland.com/assets/media/pdf/SC-88PRO_OM.pdf
|
|
37
|
+
// http://hummer.stanford.edu/sig/doc/classes/MidiOutput/rpn.html
|
|
38
|
+
case dataEntryStates.NRPFine:
|
|
39
|
+
switch(channelObject.NRPCoarse)
|
|
40
|
+
{
|
|
41
|
+
default:
|
|
42
|
+
if(dataValue === 64)
|
|
43
|
+
{
|
|
44
|
+
// default value
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
SpessaSynthWarn(
|
|
48
|
+
`%cUnrecognized NRPN for %c${channel}%c: %c(0x${channelObject.NRPCoarse.toString(16).toUpperCase()} 0x${channelObject.NRPFine.toString(16).toUpperCase()})%c data value: %c${dataValue}`,
|
|
49
|
+
consoleColors.warn,
|
|
50
|
+
consoleColors.recognized,
|
|
51
|
+
consoleColors.warn,
|
|
52
|
+
consoleColors.unrecognized,
|
|
53
|
+
consoleColors.warn,
|
|
54
|
+
consoleColors.value);
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case 0x01:
|
|
58
|
+
switch(channelObject.NRPFine)
|
|
59
|
+
{
|
|
60
|
+
default:
|
|
61
|
+
if(dataValue === 64)
|
|
62
|
+
{
|
|
63
|
+
// default value
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
SpessaSynthWarn(
|
|
67
|
+
`%cUnrecognized NRPN for %c${channel}%c: %c(0x${channelObject.NRPCoarse.toString(16)} 0x${channelObject.NRPFine.toString(16)})%c data value: %c${dataValue}`,
|
|
68
|
+
consoleColors.warn,
|
|
69
|
+
consoleColors.recognized,
|
|
70
|
+
consoleColors.warn,
|
|
71
|
+
consoleColors.unrecognized,
|
|
72
|
+
consoleColors.warn,
|
|
73
|
+
consoleColors.value);
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
// vibrato rate
|
|
77
|
+
case 0x08:
|
|
78
|
+
if(channelObject.lockVibrato)
|
|
79
|
+
{
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if(dataValue === 64)
|
|
83
|
+
{
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
addDefaultVibrato();
|
|
87
|
+
channelObject.channelVibrato.rate = (dataValue / 64) * 8;
|
|
88
|
+
SpessaSynthInfo(`%cVibrato rate for channel %c${channel}%c is now set to %c${channelObject.channelVibrato.rate}%cHz.`,
|
|
89
|
+
consoleColors.info,
|
|
90
|
+
consoleColors.recognized,
|
|
91
|
+
consoleColors.info,
|
|
92
|
+
consoleColors.value,
|
|
93
|
+
consoleColors.info);
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
// vibrato depth
|
|
97
|
+
case 0x09:
|
|
98
|
+
if(channelObject.lockVibrato)
|
|
99
|
+
{
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if(dataValue === 64)
|
|
103
|
+
{
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
addDefaultVibrato();
|
|
107
|
+
channelObject.channelVibrato.depth = dataValue / 2;
|
|
108
|
+
SpessaSynthInfo(`%cVibrato depth for %c${channel}%c is now set to %c${channelObject.channelVibrato.depth}%c cents range of detune.`,
|
|
109
|
+
consoleColors.info,
|
|
110
|
+
consoleColors.recognized,
|
|
111
|
+
consoleColors.info,
|
|
112
|
+
consoleColors.value,
|
|
113
|
+
consoleColors.info);
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
// vibrato delay
|
|
117
|
+
case 0x0A:
|
|
118
|
+
if(channelObject.lockVibrato)
|
|
119
|
+
{
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if(dataValue === 64)
|
|
123
|
+
{
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
addDefaultVibrato();
|
|
127
|
+
channelObject.channelVibrato.delay = (dataValue / 64) / 3;
|
|
128
|
+
SpessaSynthInfo(`%cVibrato delay for %c${channel}%c is now set to %c${channelObject.channelVibrato.delay}%c seconds.`,
|
|
129
|
+
consoleColors.info,
|
|
130
|
+
consoleColors.recognized,
|
|
131
|
+
consoleColors.info,
|
|
132
|
+
consoleColors.value,
|
|
133
|
+
consoleColors.info);
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
// filter cutoff
|
|
137
|
+
case 0x20:
|
|
138
|
+
// affect the "brightness" controller as we have a default modulator that controls it
|
|
139
|
+
const ccValue = dataValue;
|
|
140
|
+
this.controllerChange(channel, midiControllers.brightness, dataValue)
|
|
141
|
+
SpessaSynthInfo(`%cFilter cutoff for %c${channel}%c is now set to %c${ccValue}`,
|
|
142
|
+
consoleColors.info,
|
|
143
|
+
consoleColors.recognized,
|
|
144
|
+
consoleColors.info,
|
|
145
|
+
consoleColors.value);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
// drum reverb
|
|
150
|
+
case 0x1D:
|
|
151
|
+
if(!channelObject.percussionChannel)
|
|
152
|
+
{
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const reverb = dataValue;
|
|
156
|
+
this.controllerChange(channel, midiControllers.effects1Depth, reverb);
|
|
157
|
+
SpessaSynthInfo(
|
|
158
|
+
`%cGS Drum reverb for %c${channel}%c: %c${reverb}`,
|
|
159
|
+
consoleColors.info,
|
|
160
|
+
consoleColors.recognized,
|
|
161
|
+
consoleColors.info,
|
|
162
|
+
consoleColors.value);
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
// drum chorus
|
|
166
|
+
case 0x1E:
|
|
167
|
+
if(!channelObject.percussionChannel)
|
|
168
|
+
{
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const chorus = dataValue;
|
|
172
|
+
this.controllerChange(channel, midiControllers.effects3Depth, chorus);
|
|
173
|
+
SpessaSynthInfo(
|
|
174
|
+
`%cGS Drum chorus for %c${channel}%c: %c${chorus}`,
|
|
175
|
+
consoleColors.info,
|
|
176
|
+
consoleColors.recognized,
|
|
177
|
+
consoleColors.info,
|
|
178
|
+
consoleColors.value);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case dataEntryStates.RPCoarse:
|
|
183
|
+
case dataEntryStates.RPFine:
|
|
184
|
+
switch(channelObject.RPValue)
|
|
185
|
+
{
|
|
186
|
+
default:
|
|
187
|
+
SpessaSynthWarn(
|
|
188
|
+
`%cUnrecognized RPN for %c${channel}%c: %c(0x${channelObject.RPValue.toString(16)})%c data value: %c${dataValue}`,
|
|
189
|
+
consoleColors.warn,
|
|
190
|
+
consoleColors.recognized,
|
|
191
|
+
consoleColors.warn,
|
|
192
|
+
consoleColors.unrecognized,
|
|
193
|
+
consoleColors.warn,
|
|
194
|
+
consoleColors.value);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
// pitch bend range
|
|
198
|
+
case 0x0000:
|
|
199
|
+
channelObject.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = dataValue << 7;
|
|
200
|
+
SpessaSynthInfo(`%cChannel ${channel} bend range. Semitones: %c${dataValue}`,
|
|
201
|
+
consoleColors.info,
|
|
202
|
+
consoleColors.value);
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
// coarse tuning
|
|
206
|
+
case 0x0002:
|
|
207
|
+
// semitones
|
|
208
|
+
this.setChannelTuning(channel, (dataValue - 64) * 100);
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
// fine tuning
|
|
212
|
+
case 0x0001:
|
|
213
|
+
// note: this will not work properly unless the lsb is sent!
|
|
214
|
+
// here we store the raw value to then adjust in fine
|
|
215
|
+
this.setChannelTuning(channel, (dataValue - 64));
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
// modulation depth
|
|
219
|
+
case 0x0005:
|
|
220
|
+
this.setModulationDepth(channel, dataValue * 100);
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
case 0x3FFF:
|
|
224
|
+
this.resetParameters(channel);
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Executes a data entry for an RPN tuning
|
|
234
|
+
* @param channel {number}
|
|
235
|
+
* @param dataValue {number} dataEntry LSB
|
|
236
|
+
* @this {Synthesizer}
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
export function dataEntryFine(channel, dataValue)
|
|
240
|
+
{
|
|
241
|
+
const channelObject = this.workletProcessorChannels[channel];
|
|
242
|
+
switch (channelObject.dataEntryState)
|
|
243
|
+
{
|
|
244
|
+
default:
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case dataEntryStates.RPCoarse:
|
|
248
|
+
case dataEntryStates.RPFine:
|
|
249
|
+
switch(channelObject.RPValue)
|
|
250
|
+
{
|
|
251
|
+
default:
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
// pitch bend range fine tune is not supported in the SoundFont2 format. (pitchbend range is in semitones rather than cents)
|
|
255
|
+
case 0x0000:
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
// fine tuning
|
|
259
|
+
case 0x0001:
|
|
260
|
+
// grab the data and shift
|
|
261
|
+
const coarse = channelObject.customControllers[customControllers.channelTuning];
|
|
262
|
+
const finalTuning = (coarse << 7) | dataValue;
|
|
263
|
+
this.setChannelTuning(channel, finalTuning * 0.0122); // multiply by 8192 / 100 (cent increment)
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
// modulation depth
|
|
267
|
+
case 0x0005:
|
|
268
|
+
const currentModulationDepthCents = channelObject.customControllers[customControllers.modulationMultiplier] * 50;
|
|
269
|
+
let cents = currentModulationDepthCents + (dataValue / 128) * 100;
|
|
270
|
+
this.setModulationDepth(channel, cents);
|
|
271
|
+
break
|
|
272
|
+
|
|
273
|
+
case 0x3FFF:
|
|
274
|
+
this.resetParameters(channel);
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { generatorTypes } from '../../../soundfont/chunk/generators.js'
|
|
2
|
+
import { consoleColors } from '../../../utils/other.js'
|
|
3
|
+
import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Release a note
|
|
7
|
+
* @param channel {number}
|
|
8
|
+
* @param midiNote {number}
|
|
9
|
+
* @this {Synthesizer}
|
|
10
|
+
*/
|
|
11
|
+
export function noteOff(channel, midiNote)
|
|
12
|
+
{
|
|
13
|
+
if(midiNote > 127 || midiNote < 0)
|
|
14
|
+
{
|
|
15
|
+
SpessaSynthWarn(`Received a noteOn for note`, midiNote, "Ignoring.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// if high performance mode, kill notes instead of stopping them
|
|
20
|
+
if(this.highPerformanceMode)
|
|
21
|
+
{
|
|
22
|
+
// if the channel is percussion channel, do not kill the notes
|
|
23
|
+
if(!this.workletProcessorChannels[channel].drumChannel)
|
|
24
|
+
{
|
|
25
|
+
this.killNote(channel, midiNote);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const channelVoices = this.workletProcessorChannels[channel].voices;
|
|
31
|
+
channelVoices.forEach(v => {
|
|
32
|
+
if(v.midiNote !== midiNote || v.isInRelease === true)
|
|
33
|
+
{
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// if hold pedal, move to sustain
|
|
37
|
+
if(this.workletProcessorChannels[channel].holdPedal) {
|
|
38
|
+
this.workletProcessorChannels[channel].sustainedVoices.push(v);
|
|
39
|
+
}
|
|
40
|
+
else
|
|
41
|
+
{
|
|
42
|
+
this.releaseVoice(v);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stops a note nearly instantly
|
|
49
|
+
* @param channel {number}
|
|
50
|
+
* @param midiNote {number}
|
|
51
|
+
* @this {Synthesizer}
|
|
52
|
+
*/
|
|
53
|
+
export function killNote(channel, midiNote)
|
|
54
|
+
{
|
|
55
|
+
this.workletProcessorChannels[channel].voices.forEach(v => {
|
|
56
|
+
if(v.midiNote !== midiNote)
|
|
57
|
+
{
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
v.modulatedGenerators[generatorTypes.releaseVolEnv] = -12000; // set release to be very short
|
|
61
|
+
this.releaseVoice(v);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* stops all notes
|
|
67
|
+
* @param channel {number}
|
|
68
|
+
* @param force {boolean}
|
|
69
|
+
* @this {Synthesizer}
|
|
70
|
+
*/
|
|
71
|
+
export function stopAll(channel, force = false)
|
|
72
|
+
{
|
|
73
|
+
const channelVoices = this.workletProcessorChannels[channel].voices;
|
|
74
|
+
if(force)
|
|
75
|
+
{
|
|
76
|
+
// force stop all
|
|
77
|
+
channelVoices.length = 0;
|
|
78
|
+
this.workletProcessorChannels[channel].sustainedVoices.length = 0;
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
{
|
|
82
|
+
channelVoices.forEach(v => {
|
|
83
|
+
if(v.isInRelease) return;
|
|
84
|
+
this.releaseVoice(v);
|
|
85
|
+
});
|
|
86
|
+
this.workletProcessorChannels[channel].sustainedVoices.forEach(v => {
|
|
87
|
+
this.releaseVoice(v);
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @this {Synthesizer}
|
|
94
|
+
* @param force {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function stopAllChannels(force = false)
|
|
97
|
+
{
|
|
98
|
+
SpessaSynthInfo("%cStop all received!", consoleColors.info);
|
|
99
|
+
for (let i = 0; i < this.workletProcessorChannels.length; i++) {
|
|
100
|
+
this.stopAll(i, force);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getWorkletVoices } from '../worklet_utilities/worklet_voice.js'
|
|
2
|
+
import { generatorTypes } from '../../../soundfont/chunk/generators.js'
|
|
3
|
+
import { computeModulators } from '../worklet_utilities/worklet_modulator.js'
|
|
4
|
+
import { VOICE_CAP } from "../../synthesizer.js";
|
|
5
|
+
import { SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Append the voices
|
|
9
|
+
* @param channel {number}
|
|
10
|
+
* @param midiNote {number}
|
|
11
|
+
* @param velocity {number}
|
|
12
|
+
* @param enableDebugging {boolean}
|
|
13
|
+
* @this {Synthesizer}
|
|
14
|
+
*/
|
|
15
|
+
export function noteOn(channel, midiNote, velocity, enableDebugging = false)
|
|
16
|
+
{
|
|
17
|
+
if (velocity === 0) {
|
|
18
|
+
this.noteOff(channel, midiNote);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
(this.highPerformanceMode && this.totalVoicesAmount > 200 && velocity < 40) ||
|
|
24
|
+
(this.highPerformanceMode && velocity < 10) ||
|
|
25
|
+
(this.workletProcessorChannels[channel].isMuted)
|
|
26
|
+
) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if(midiNote > 127 || midiNote < 0)
|
|
31
|
+
{
|
|
32
|
+
SpessaSynthWarn(`Received a noteOn for note`, midiNote, "Ignoring.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
// get voices
|
|
38
|
+
const voices = getWorkletVoices(
|
|
39
|
+
channel,
|
|
40
|
+
midiNote,
|
|
41
|
+
velocity,
|
|
42
|
+
this.workletProcessorChannels[channel].preset,
|
|
43
|
+
this.currentTime,
|
|
44
|
+
this.sampleRate,
|
|
45
|
+
data => this.sampleDump(data.channel, data.sampleID, data.sampleData),
|
|
46
|
+
this.workletProcessorChannels[channel].cachedVoices,
|
|
47
|
+
enableDebugging);
|
|
48
|
+
|
|
49
|
+
// add voices and exclusive class apply
|
|
50
|
+
const channelVoices = this.workletProcessorChannels[channel].voices;
|
|
51
|
+
voices.forEach(voice => {
|
|
52
|
+
const exclusive = voice.generators[generatorTypes.exclusiveClass];
|
|
53
|
+
if(exclusive !== 0)
|
|
54
|
+
{
|
|
55
|
+
channelVoices.forEach(v => {
|
|
56
|
+
if(v.generators[generatorTypes.exclusiveClass] === exclusive)
|
|
57
|
+
{
|
|
58
|
+
this.releaseVoice(v);
|
|
59
|
+
v.generators[generatorTypes.releaseVolEnv] = -7200; // make the release nearly instant
|
|
60
|
+
computeModulators(v, this.workletProcessorChannels[channel].midiControllers);
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
computeModulators(voice, this.workletProcessorChannels[channel].midiControllers);
|
|
65
|
+
voice.currentAttenuationDb = 100;
|
|
66
|
+
})
|
|
67
|
+
channelVoices.push(...voices);
|
|
68
|
+
|
|
69
|
+
this.totalVoicesAmount += voices.length;
|
|
70
|
+
// cap the voices
|
|
71
|
+
if(this.totalVoicesAmount > VOICE_CAP)
|
|
72
|
+
{
|
|
73
|
+
this.voiceKilling(this.totalVoicesAmount - VOICE_CAP);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { midiControllers } from '../../../midi_parser/midi_message.js'
|
|
2
|
+
import { SoundFont2 } from '../../../soundfont/soundfont_parser.js'
|
|
3
|
+
import { clearSamplesList } from '../worklet_utilities/worklet_voice.js'
|
|
4
|
+
import { generatorTypes } from '../../../soundfont/chunk/generators.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* executes a program change
|
|
8
|
+
* @param channel {number}
|
|
9
|
+
* @param programNumber {number}
|
|
10
|
+
* @param userChange {boolean}
|
|
11
|
+
* @this {Synthesizer}
|
|
12
|
+
*/
|
|
13
|
+
export function programChange(channel, programNumber, userChange=false)
|
|
14
|
+
{
|
|
15
|
+
/**
|
|
16
|
+
* @type {WorkletProcessorChannel}
|
|
17
|
+
*/
|
|
18
|
+
const channelObject = this.workletProcessorChannels[channel];
|
|
19
|
+
if(channelObject.lockPreset)
|
|
20
|
+
{
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// always 128 for percussion
|
|
24
|
+
const bank = (channelObject.drumChannel ? 128 : channelObject.midiControllers[midiControllers.bankSelect]);
|
|
25
|
+
const preset = this.soundfont.getPreset(bank, programNumber);
|
|
26
|
+
this.setPreset(channel, preset);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param channel {number}
|
|
31
|
+
* @param preset {Preset}
|
|
32
|
+
* @this {Synthesizer}
|
|
33
|
+
*/
|
|
34
|
+
export function setPreset(channel, preset)
|
|
35
|
+
{
|
|
36
|
+
if(this.workletProcessorChannels[channel].lockPreset)
|
|
37
|
+
{
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.workletProcessorChannels[channel].preset = preset;
|
|
41
|
+
|
|
42
|
+
// reset cached voices
|
|
43
|
+
this.workletProcessorChannels[channel].cachedVoices = [];
|
|
44
|
+
for (let i = 0; i < 128; i++) {
|
|
45
|
+
this.workletProcessorChannels[channel].cachedVoices.push([]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Toggles drums on a given channel
|
|
51
|
+
* @param channel {number}
|
|
52
|
+
* @param isDrum {boolean}
|
|
53
|
+
* @this {Synthesizer}
|
|
54
|
+
*/
|
|
55
|
+
export function setDrums(channel, isDrum)
|
|
56
|
+
{
|
|
57
|
+
const channelObject = this.workletProcessorChannels[channel];
|
|
58
|
+
if(isDrum)
|
|
59
|
+
{
|
|
60
|
+
channelObject.drumChannel = true;
|
|
61
|
+
this.setPreset(channel, this.soundfont.getPreset(128, channelObject.preset.program));
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
{
|
|
65
|
+
channelObject.percussionChannel = false;
|
|
66
|
+
this.setPreset(channel, this.soundfont.getPreset(0, channelObject.preset.program));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param buffer {ArrayBuffer}
|
|
72
|
+
* @this {Synthesizer}
|
|
73
|
+
*/
|
|
74
|
+
export function reloadSoundFont(buffer)
|
|
75
|
+
{
|
|
76
|
+
this.stopAllChannels(true);
|
|
77
|
+
delete this.soundfont;
|
|
78
|
+
clearSamplesList();
|
|
79
|
+
delete this.workletDumpedSamplesList;
|
|
80
|
+
this.workletDumpedSamplesList = [];
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
this.soundfont = new SoundFont2(buffer);
|
|
84
|
+
this.defaultPreset = this.soundfont.getPreset(0, 0);
|
|
85
|
+
this.drumPreset = this.soundfont.getPreset(128, 0);
|
|
86
|
+
|
|
87
|
+
for(let i = 0; i < this.workletProcessorChannels.length; i++)
|
|
88
|
+
{
|
|
89
|
+
const channelObject = this.workletProcessorChannels[i];
|
|
90
|
+
channelObject.cachedVoices = [];
|
|
91
|
+
for (let j = 0; j < 128; j++) {
|
|
92
|
+
channelObject.cachedVoices.push([]);
|
|
93
|
+
}
|
|
94
|
+
channelObject.lockPreset = false;
|
|
95
|
+
this.programChange(i, channelObject.preset.program);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* saves a sample
|
|
101
|
+
* @param channel {number}
|
|
102
|
+
* @param sampleID {number}
|
|
103
|
+
* @param sampleData {Float32Array}
|
|
104
|
+
* @this {Synthesizer}
|
|
105
|
+
*/
|
|
106
|
+
export function sampleDump(channel, sampleID, sampleData)
|
|
107
|
+
{
|
|
108
|
+
this.workletDumpedSamplesList[sampleID] = sampleData;
|
|
109
|
+
// the sample maybe was loaded after the voice was sent... adjust the end position!
|
|
110
|
+
|
|
111
|
+
// not for all channels because the system tells us for what channel this voice was dumped! yay!
|
|
112
|
+
this.workletProcessorChannels[channel].voices.forEach(v => {
|
|
113
|
+
if(v.sample.sampleID !== sampleID)
|
|
114
|
+
{
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
v.sample.end = sampleData.length - 1 + v.generators[generatorTypes.endAddrOffset] + (v.generators[generatorTypes.endAddrsCoarseOffset] * 32768);
|
|
118
|
+
// calculate for how long the sample has been playing and move the cursor there
|
|
119
|
+
v.sample.cursor = (v.sample.playbackStep * this.sampleRate) * (this.currentTime - v.startTime);
|
|
120
|
+
if(v.sample.loopingMode === 0) // no loop
|
|
121
|
+
{
|
|
122
|
+
if (v.sample.cursor >= v.sample.end)
|
|
123
|
+
{
|
|
124
|
+
v.finished = true;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
{
|
|
130
|
+
// go through modulo (adjust cursor if the sample has looped
|
|
131
|
+
if(v.sample.cursor > v.sample.loopEnd)
|
|
132
|
+
{
|
|
133
|
+
v.sample.cursor = v.sample.cursor % (v.sample.loopEnd - v.sample.loopStart) + v.sample.loopStart - 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// set start time to current!
|
|
137
|
+
v.startTime = this.currentTime;
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
}
|