spessasynth_lib 0.0.1
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/modules.xml +8 -0
- package/.idea/spessasynth_lib.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/copy_version.sh +38 -0
- package/index.js +73 -0
- package/package/@types/externals/stbvorbis_sync/stbvorbis_sync.min.d.ts +1 -0
- package/package/@types/index.d.ts +34 -0
- package/package/@types/midi_handler/midi_handler.d.ts +39 -0
- package/package/@types/midi_handler/web_midi_link.d.ts +12 -0
- package/package/@types/midi_parser/midi_data.d.ts +95 -0
- package/package/@types/midi_parser/midi_editor.d.ts +45 -0
- package/package/@types/midi_parser/midi_loader.d.ts +100 -0
- package/package/@types/midi_parser/midi_message.d.ts +154 -0
- package/package/@types/midi_parser/midi_writer.d.ts +6 -0
- package/package/@types/midi_parser/rmidi_writer.d.ts +9 -0
- package/package/@types/midi_parser/used_keys_loaded.d.ts +7 -0
- package/package/@types/sequencer/sequencer.d.ts +180 -0
- package/package/@types/sequencer/worklet_sequencer/sequencer_message.d.ts +28 -0
- package/package/@types/soundfont/read/generators.d.ts +98 -0
- package/package/@types/soundfont/read/instruments.d.ts +50 -0
- package/package/@types/soundfont/read/modulators.d.ts +73 -0
- package/package/@types/soundfont/read/presets.d.ts +87 -0
- package/package/@types/soundfont/read/riff_chunk.d.ts +31 -0
- package/package/@types/soundfont/read/samples.d.ts +134 -0
- package/package/@types/soundfont/read/zones.d.ts +141 -0
- package/package/@types/soundfont/soundfont.d.ts +76 -0
- package/package/@types/soundfont/write/ibag.d.ts +6 -0
- package/package/@types/soundfont/write/igen.d.ts +6 -0
- package/package/@types/soundfont/write/imod.d.ts +6 -0
- package/package/@types/soundfont/write/inst.d.ts +6 -0
- package/package/@types/soundfont/write/pbag.d.ts +6 -0
- package/package/@types/soundfont/write/pgen.d.ts +6 -0
- package/package/@types/soundfont/write/phdr.d.ts +6 -0
- package/package/@types/soundfont/write/pmod.d.ts +6 -0
- package/package/@types/soundfont/write/sdta.d.ts +11 -0
- package/package/@types/soundfont/write/shdr.d.ts +8 -0
- package/package/@types/soundfont/write/soundfont_trimmer.d.ts +6 -0
- package/package/@types/soundfont/write/write.d.ts +21 -0
- package/package/@types/synthetizer/audio_effects/effects_config.d.ts +29 -0
- package/package/@types/synthetizer/audio_effects/fancy_chorus.d.ts +93 -0
- package/package/@types/synthetizer/audio_effects/reverb.d.ts +7 -0
- package/package/@types/synthetizer/synth_event_handler.d.ts +161 -0
- package/package/@types/synthetizer/synthetizer.d.ts +294 -0
- package/package/@types/synthetizer/worklet_system/message_protocol/worklet_message.d.ts +89 -0
- package/package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +134 -0
- package/package/@types/synthetizer/worklet_url.d.ts +5 -0
- package/package/@types/utils/buffer_to_wav.d.ts +8 -0
- package/package/@types/utils/byte_functions/big_endian.d.ts +13 -0
- package/package/@types/utils/byte_functions/little_endian.d.ts +35 -0
- package/package/@types/utils/byte_functions/string.d.ts +22 -0
- package/package/@types/utils/byte_functions/variable_length_quantity.d.ts +12 -0
- package/package/@types/utils/indexed_array.d.ts +21 -0
- package/package/@types/utils/loggin.d.ts +26 -0
- package/package/@types/utils/other.d.ts +32 -0
- package/package/LICENSE +26 -0
- package/package/README.md +84 -0
- package/package/externals/NOTICE +9 -0
- package/package/externals/libvorbis/@types/OggVorbisEncoder.d.ts +34 -0
- package/package/externals/libvorbis/OggVorbisEncoder.min.js +1 -0
- package/package/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +12 -0
- package/package/externals/stbvorbis_sync/LICENSE +202 -0
- package/package/externals/stbvorbis_sync/stbvorbis_sync.min.js +1 -0
- package/package/index.js +73 -0
- package/package/midi_handler/README.md +3 -0
- package/package/midi_handler/midi_handler.js +118 -0
- package/package/midi_handler/web_midi_link.js +41 -0
- package/package/midi_parser/README.md +3 -0
- package/package/midi_parser/midi_data.js +121 -0
- package/package/midi_parser/midi_editor.js +557 -0
- package/package/midi_parser/midi_loader.js +502 -0
- package/package/midi_parser/midi_message.js +234 -0
- package/package/midi_parser/midi_writer.js +95 -0
- package/package/midi_parser/rmidi_writer.js +271 -0
- package/package/midi_parser/used_keys_loaded.js +172 -0
- package/package/package.json +43 -0
- package/package/sequencer/README.md +23 -0
- package/package/sequencer/sequencer.js +439 -0
- package/package/sequencer/worklet_sequencer/events.js +92 -0
- package/package/sequencer/worklet_sequencer/play.js +309 -0
- package/package/sequencer/worklet_sequencer/process_event.js +167 -0
- package/package/sequencer/worklet_sequencer/process_tick.js +85 -0
- package/package/sequencer/worklet_sequencer/sequencer_message.js +39 -0
- package/package/sequencer/worklet_sequencer/song_control.js +193 -0
- package/package/sequencer/worklet_sequencer/worklet_sequencer.js +218 -0
- package/package/soundfont/README.md +8 -0
- package/package/soundfont/read/generators.js +212 -0
- package/package/soundfont/read/instruments.js +125 -0
- package/package/soundfont/read/modulators.js +249 -0
- package/package/soundfont/read/presets.js +300 -0
- package/package/soundfont/read/riff_chunk.js +81 -0
- package/package/soundfont/read/samples.js +398 -0
- package/package/soundfont/read/zones.js +310 -0
- package/package/soundfont/soundfont.js +357 -0
- package/package/soundfont/write/ibag.js +39 -0
- package/package/soundfont/write/igen.js +75 -0
- package/package/soundfont/write/imod.js +46 -0
- package/package/soundfont/write/inst.js +34 -0
- package/package/soundfont/write/pbag.js +39 -0
- package/package/soundfont/write/pgen.js +77 -0
- package/package/soundfont/write/phdr.js +42 -0
- package/package/soundfont/write/pmod.js +46 -0
- package/package/soundfont/write/sdta.js +72 -0
- package/package/soundfont/write/shdr.js +54 -0
- package/package/soundfont/write/soundfont_trimmer.js +169 -0
- package/package/soundfont/write/write.js +180 -0
- package/package/synthetizer/README.md +6 -0
- package/package/synthetizer/audio_effects/effects_config.js +21 -0
- package/package/synthetizer/audio_effects/fancy_chorus.js +120 -0
- package/package/synthetizer/audio_effects/impulse_response_2.flac +0 -0
- package/package/synthetizer/audio_effects/reverb.js +24 -0
- package/package/synthetizer/synth_event_handler.js +156 -0
- package/package/synthetizer/synthetizer.js +766 -0
- package/package/synthetizer/worklet_processor.min.js +13 -0
- package/package/synthetizer/worklet_system/README.md +6 -0
- package/package/synthetizer/worklet_system/main_processor.js +363 -0
- package/package/synthetizer/worklet_system/message_protocol/handle_message.js +197 -0
- package/package/synthetizer/worklet_system/message_protocol/message_sending.js +74 -0
- package/package/synthetizer/worklet_system/message_protocol/worklet_message.js +121 -0
- package/package/synthetizer/worklet_system/minify_processor.sh +4 -0
- package/package/synthetizer/worklet_system/worklet_methods/controller_control.js +230 -0
- package/package/synthetizer/worklet_system/worklet_methods/data_entry.js +277 -0
- package/package/synthetizer/worklet_system/worklet_methods/note_off.js +109 -0
- package/package/synthetizer/worklet_system/worklet_methods/note_on.js +91 -0
- package/package/synthetizer/worklet_system/worklet_methods/program_control.js +183 -0
- package/package/synthetizer/worklet_system/worklet_methods/reset_controllers.js +177 -0
- package/package/synthetizer/worklet_system/worklet_methods/snapshot.js +129 -0
- package/package/synthetizer/worklet_system/worklet_methods/system_exclusive.js +272 -0
- package/package/synthetizer/worklet_system/worklet_methods/tuning_control.js +195 -0
- package/package/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
- package/package/synthetizer/worklet_system/worklet_methods/voice_control.js +233 -0
- package/package/synthetizer/worklet_system/worklet_processor.js +9 -0
- package/package/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
- package/package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +130 -0
- package/package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
- package/package/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
- package/package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +81 -0
- package/package/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
- package/package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +265 -0
- package/package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +234 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +116 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +272 -0
- package/package/synthetizer/worklet_url.js +5 -0
- package/package/utils/README.md +4 -0
- package/package/utils/buffer_to_wav.js +101 -0
- package/package/utils/byte_functions/big_endian.js +28 -0
- package/package/utils/byte_functions/little_endian.js +74 -0
- package/package/utils/byte_functions/string.js +97 -0
- package/package/utils/byte_functions/variable_length_quantity.js +37 -0
- package/package/utils/encode_vorbis.js +30 -0
- package/package/utils/indexed_array.js +41 -0
- package/package/utils/loggin.js +79 -0
- package/package/utils/other.js +54 -0
- package/package.json +43 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import { IndexedByteArray } from '../utils/indexed_array.js'
|
|
2
|
+
import { consoleColors } from '../utils/other.js'
|
|
3
|
+
import { getEvent, messageTypes, midiControllers } from '../midi_parser/midi_message.js'
|
|
4
|
+
import { EventHandler } from './synth_event_handler.js'
|
|
5
|
+
import { FancyChorus } from './audio_effects/fancy_chorus.js'
|
|
6
|
+
import { getReverbProcessor } from './audio_effects/reverb.js'
|
|
7
|
+
import {
|
|
8
|
+
ALL_CHANNELS_OR_DIFFERENT_ACTION, masterParameterType,
|
|
9
|
+
returnMessageType,
|
|
10
|
+
workletMessageType,
|
|
11
|
+
} from './worklet_system/message_protocol/worklet_message.js'
|
|
12
|
+
import { SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js'
|
|
13
|
+
import { DEFAULT_EFFECTS_CONFIG } from './audio_effects/effects_config.js'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* synthesizer.js
|
|
18
|
+
* purpose: responds to midi messages and called functions, managing the channels and passing the messages to them
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} StartRenderingDataConfig
|
|
23
|
+
* @property {MIDI} parsedMIDI - the MIDI to render
|
|
24
|
+
* @property {SynthesizerSnapshot} snapshot - the snapshot to apply
|
|
25
|
+
* @property {boolean|undefined} oneOutput - if synth should use one output with 32 channels (2 audio channels for each midi channel). this disables chorus and reverb.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const WORKLET_PROCESSOR_NAME = "spessasynth-worklet-system";
|
|
29
|
+
|
|
30
|
+
export const VOICE_CAP = 450;
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_PERCUSSION = 9;
|
|
33
|
+
export const MIDI_CHANNEL_COUNT = 16;
|
|
34
|
+
export const DEFAULT_SYNTH_MODE = "gs";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new instance of the SpessaSynth synthesizer
|
|
38
|
+
* @param targetNode {AudioNode}
|
|
39
|
+
* @param soundFontBuffer {ArrayBuffer} the soundfont file array buffer
|
|
40
|
+
* @param enableEventSystem {boolean} enables the event system. Defaults to true
|
|
41
|
+
* @param startRenderingData {StartRenderingDataConfig} if set, starts playing this immediately and restores the values
|
|
42
|
+
* @param effectsConfig {EffectsConfig} optional configuration for the audio effects.
|
|
43
|
+
*/
|
|
44
|
+
export class Synthetizer {
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new instance of the SpessaSynth synthesizer
|
|
47
|
+
* @param targetNode {AudioNode}
|
|
48
|
+
* @param soundFontBuffer {ArrayBuffer} the soundfont file array buffer
|
|
49
|
+
* @param enableEventSystem {boolean} enables the event system. Defaults to true
|
|
50
|
+
* @param startRenderingData {StartRenderingDataConfig} if set, starts playing this immediately and restores the values
|
|
51
|
+
* @param effectsConfig {EffectsConfig} optional configuration for the audio effects.
|
|
52
|
+
*/
|
|
53
|
+
constructor(targetNode,
|
|
54
|
+
soundFontBuffer,
|
|
55
|
+
enableEventSystem = true,
|
|
56
|
+
startRenderingData = undefined,
|
|
57
|
+
effectsConfig = DEFAULT_EFFECTS_CONFIG) {
|
|
58
|
+
SpessaSynthInfo("%cInitializing SpessaSynth synthesizer...", consoleColors.info);
|
|
59
|
+
this.context = targetNode.context;
|
|
60
|
+
const oneOutputMode = startRenderingData?.oneOutput === true;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Allows to set up custom event listeners for the synthesizer
|
|
64
|
+
* @type {EventHandler}
|
|
65
|
+
*/
|
|
66
|
+
this.eventHandler = new EventHandler();
|
|
67
|
+
|
|
68
|
+
this._voiceCap = VOICE_CAP;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* the new channels will have their audio sent to the moduled output by this constant.
|
|
72
|
+
* what does that mean? e.g. if outputsAmount is 16, then channel's 16 audio will be sent to channel 0
|
|
73
|
+
* @type {number}
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
this._outputsAmount = MIDI_CHANNEL_COUNT;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* the amount of midi channels
|
|
80
|
+
* @type {number}
|
|
81
|
+
*/
|
|
82
|
+
this.channelsAmount = this._outputsAmount;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Indicates if the synth is fully ready
|
|
86
|
+
* @type {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
this.isReady = new Promise(resolve => this._resolveReady = resolve);
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* individual channel voices amount
|
|
93
|
+
* @type {ChannelProperty[]}
|
|
94
|
+
*/
|
|
95
|
+
this.channelProperties = [];
|
|
96
|
+
for (let i = 0; i < this.channelsAmount; i++)
|
|
97
|
+
{
|
|
98
|
+
this.addNewChannel(false);
|
|
99
|
+
}
|
|
100
|
+
this.channelProperties[DEFAULT_PERCUSSION].isDrum = true;
|
|
101
|
+
this._voicesAmount = 0;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* For Black MIDI's - forces release time to 50ms
|
|
105
|
+
* @type {boolean}
|
|
106
|
+
*/
|
|
107
|
+
this._highPerformanceMode = false;
|
|
108
|
+
|
|
109
|
+
// create a worklet processor
|
|
110
|
+
let processorChannelCount = Array(this._outputsAmount + 2).fill(2);
|
|
111
|
+
let processorOutputsCount = this._outputsAmount + 2;
|
|
112
|
+
if(oneOutputMode)
|
|
113
|
+
{
|
|
114
|
+
processorOutputsCount = 1;
|
|
115
|
+
processorChannelCount = [32];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// first two outputs: reverb, chorsu, the others are the channel outputs
|
|
119
|
+
try {
|
|
120
|
+
this.worklet = new AudioWorkletNode(this.context, WORKLET_PROCESSOR_NAME, {
|
|
121
|
+
outputChannelCount: processorChannelCount,
|
|
122
|
+
numberOfOutputs: processorOutputsCount,
|
|
123
|
+
processorOptions: {
|
|
124
|
+
midiChannels: this._outputsAmount,
|
|
125
|
+
soundfont: soundFontBuffer,
|
|
126
|
+
enableEventSystem: enableEventSystem,
|
|
127
|
+
startRenderingData: startRenderingData
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (e)
|
|
132
|
+
{
|
|
133
|
+
throw new Error("Could not create the audioWorklet. Did you forget to addModule()?");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @typedef {Object} PresetListElement
|
|
138
|
+
* @property {string} presetName
|
|
139
|
+
* @property {number} program
|
|
140
|
+
* @property {number} bank
|
|
141
|
+
*
|
|
142
|
+
* used in "presetlistchange" event
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
// worklet sends us some data back
|
|
146
|
+
this.worklet.port.onmessage = e => this.handleMessage(e.data);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @type {function(SynthesizerSnapshot)}
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
this._snapshotCallback = undefined;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* for the worklet sequencer's messages
|
|
156
|
+
* @type {function(WorkletSequencerReturnMessageType, any)}
|
|
157
|
+
*/
|
|
158
|
+
this.sequencerCallbackFunction = undefined;
|
|
159
|
+
|
|
160
|
+
// add reverb
|
|
161
|
+
if(effectsConfig.reverbEnabled && !oneOutputMode)
|
|
162
|
+
{
|
|
163
|
+
this.reverbProcessor = getReverbProcessor(this.context, effectsConfig.reverbImpulseResponse);
|
|
164
|
+
this.reverbProcessor.connect(targetNode);
|
|
165
|
+
this.worklet.connect(this.reverbProcessor, 0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if(effectsConfig.chorusEnabled && !oneOutputMode)
|
|
169
|
+
{
|
|
170
|
+
this.chorusProcessor = new FancyChorus(targetNode, effectsConfig.chorusConfig);
|
|
171
|
+
this.worklet.connect(this.chorusProcessor.input, 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if(oneOutputMode)
|
|
175
|
+
{
|
|
176
|
+
// one output mode: one output (duh)
|
|
177
|
+
this.worklet.connect(targetNode, 0);
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
{
|
|
181
|
+
// connect all outputs to the output node
|
|
182
|
+
for (let i = 2; i < this.channelsAmount + 2; i++)
|
|
183
|
+
{
|
|
184
|
+
this.worklet.connect(targetNode, i);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// attach newchannel to keep track of channels count
|
|
189
|
+
this.eventHandler.addEvent("newchannel", "synth-new-channel", () => {
|
|
190
|
+
this.channelsAmount++;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The maximum amount of voices allowed at once
|
|
196
|
+
* @returns {number}
|
|
197
|
+
*/
|
|
198
|
+
get voiceCap()
|
|
199
|
+
{
|
|
200
|
+
return this._voiceCap;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The maximum amount of voices allowed at once
|
|
205
|
+
* @param value {number}
|
|
206
|
+
*/
|
|
207
|
+
set voiceCap(value)
|
|
208
|
+
{
|
|
209
|
+
this.post({
|
|
210
|
+
messageType: workletMessageType.setMasterParameter,
|
|
211
|
+
messageData: [masterParameterType.voicesCap, value]
|
|
212
|
+
})
|
|
213
|
+
this._voiceCap = value;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* For Black MIDI's - forces release time to 50ms
|
|
218
|
+
* @param {boolean} value
|
|
219
|
+
*/
|
|
220
|
+
set highPerformanceMode(value)
|
|
221
|
+
{
|
|
222
|
+
this._highPerformanceMode = value;
|
|
223
|
+
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
get highPerformanceMode()
|
|
227
|
+
{
|
|
228
|
+
return this._highPerformanceMode;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Sets the SpessaSynth's log level
|
|
233
|
+
* @param enableInfo {boolean} - enable info (verbose)
|
|
234
|
+
* @param enableWarning {boolean} - enable warnings (unrecognized messages)
|
|
235
|
+
* @param enableGroup {boolean} - enable groups (recomended)
|
|
236
|
+
* @param enableTable {boolean} - enable table (debug message)
|
|
237
|
+
*/
|
|
238
|
+
setLogLevel(enableInfo, enableWarning, enableGroup, enableTable)
|
|
239
|
+
{
|
|
240
|
+
this.post({
|
|
241
|
+
channelNumber: -1,
|
|
242
|
+
messageType: workletMessageType.setLogLevel,
|
|
243
|
+
messageData: [enableInfo, enableWarning, enableGroup, enableTable]
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Handles the messages received from the worklet
|
|
249
|
+
* @param message {WorkletReturnMessage}
|
|
250
|
+
* @private
|
|
251
|
+
*/
|
|
252
|
+
handleMessage(message)
|
|
253
|
+
{
|
|
254
|
+
const messageData = message.messageData;
|
|
255
|
+
switch (message.messageType)
|
|
256
|
+
{
|
|
257
|
+
case returnMessageType.channelProperties:
|
|
258
|
+
/**
|
|
259
|
+
* @type {ChannelProperty[]}
|
|
260
|
+
*/
|
|
261
|
+
this.channelProperties = messageData;
|
|
262
|
+
|
|
263
|
+
this._voicesAmount = this.channelProperties.reduce((sum, voices) => sum + voices.voicesAmount, 0);
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case returnMessageType.eventCall:
|
|
267
|
+
this.eventHandler.callEvent(messageData.eventName, messageData.eventData);
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
case returnMessageType.sequencerSpecific:
|
|
271
|
+
if(this.sequencerCallbackFunction)
|
|
272
|
+
{
|
|
273
|
+
this.sequencerCallbackFunction(messageData.messageType, messageData.messageData);
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case returnMessageType.synthesizerSnapshot:
|
|
278
|
+
if(this._snapshotCallback)
|
|
279
|
+
{
|
|
280
|
+
this._snapshotCallback(messageData);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
case returnMessageType.ready:
|
|
285
|
+
this._resolveReady();
|
|
286
|
+
break;
|
|
287
|
+
|
|
288
|
+
case returnMessageType.soundfontError:
|
|
289
|
+
SpessaSynthWarn(new Error(messageData));
|
|
290
|
+
this.eventHandler.callEvent("soundfonterror", messageData);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Gets a complete snapshot of the synthesizer, including controllers
|
|
296
|
+
* @returns {Promise<SynthesizerSnapshot>}
|
|
297
|
+
*/
|
|
298
|
+
async getSynthesizerSnapshot()
|
|
299
|
+
{
|
|
300
|
+
return new Promise(resolve => {
|
|
301
|
+
this._snapshotCallback = s => {
|
|
302
|
+
this._snapshotCallback = undefined;
|
|
303
|
+
resolve(s);
|
|
304
|
+
};
|
|
305
|
+
this.post({
|
|
306
|
+
messageType: workletMessageType.requestSynthesizerSnapshot,
|
|
307
|
+
messageData: undefined,
|
|
308
|
+
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Adds a new channel to the synthesizer
|
|
315
|
+
* @param postMessage {boolean} leave at true, set to false only at initialization
|
|
316
|
+
*/
|
|
317
|
+
addNewChannel(postMessage = true)
|
|
318
|
+
{
|
|
319
|
+
this.channelProperties.push({
|
|
320
|
+
voicesAmount: 0,
|
|
321
|
+
pitchBend: 0,
|
|
322
|
+
pitchBendRangeSemitones: 0,
|
|
323
|
+
isMuted: false,
|
|
324
|
+
isDrum: false
|
|
325
|
+
});
|
|
326
|
+
if(!postMessage)
|
|
327
|
+
{
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.post({
|
|
331
|
+
channelNumber: 0,
|
|
332
|
+
messageType: workletMessageType.addNewChannel,
|
|
333
|
+
messageData: null
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @param channel {number}
|
|
339
|
+
* @param value {{delay: number, depth: number, rate: number}}
|
|
340
|
+
*/
|
|
341
|
+
setVibrato(channel, value)
|
|
342
|
+
{
|
|
343
|
+
this.post({
|
|
344
|
+
channelNumber: channel,
|
|
345
|
+
messageType: workletMessageType.setChannelVibrato,
|
|
346
|
+
messageData: value
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Connects the individual audio outputs to the given audio nodes. In the app it's used by the renderer.
|
|
352
|
+
* @param audioNodes {AudioNode[]}
|
|
353
|
+
*/
|
|
354
|
+
connectIndividualOutputs(audioNodes)
|
|
355
|
+
{
|
|
356
|
+
if(audioNodes.length !== this._outputsAmount)
|
|
357
|
+
{
|
|
358
|
+
throw new Error(`input nodes amount differs from the system's outputs amount!
|
|
359
|
+
Expected ${this._outputsAmount} got ${audioNodes.length}`);
|
|
360
|
+
}
|
|
361
|
+
for (let outputNumber = 0; outputNumber < this._outputsAmount; outputNumber++) {
|
|
362
|
+
// + 2 because chorus and reverb come first!
|
|
363
|
+
this.worklet.connect(audioNodes[outputNumber], outputNumber + 2);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/*
|
|
368
|
+
* Prevents any further changes to the vibrato via NRPN messages and sets it to disabled
|
|
369
|
+
*/
|
|
370
|
+
lockAndResetChannelVibrato()
|
|
371
|
+
{
|
|
372
|
+
// rate -1 disables, see worklet_message.js line 9
|
|
373
|
+
// channel -1 is all
|
|
374
|
+
this.setVibrato(ALL_CHANNELS_OR_DIFFERENT_ACTION, {depth: 0, rate: -1, delay: 0});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* A message for debugging
|
|
379
|
+
*/
|
|
380
|
+
debugMessage()
|
|
381
|
+
{
|
|
382
|
+
SpessaSynthInfo(this);
|
|
383
|
+
this.post({
|
|
384
|
+
channelNumber: 0,
|
|
385
|
+
messageType: workletMessageType.debugMessage,
|
|
386
|
+
messageData: undefined
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Starts playing a note
|
|
392
|
+
* @param channel {number} usually 0-15: the channel to play the note
|
|
393
|
+
* @param midiNote {number} 0-127 the key number of the note
|
|
394
|
+
* @param velocity {number} 0-127 the velocity of the note (generally controls loudness)
|
|
395
|
+
* @param enableDebugging {boolean} set to true to log technical details to console
|
|
396
|
+
*/
|
|
397
|
+
noteOn(channel, midiNote, velocity, enableDebugging = false) {
|
|
398
|
+
this.post({
|
|
399
|
+
channelNumber: channel,
|
|
400
|
+
messageType: workletMessageType.noteOn,
|
|
401
|
+
messageData: [midiNote, velocity, enableDebugging]
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Stops playing a note
|
|
407
|
+
* @param channel {number} usually 0-15: the channel of the note
|
|
408
|
+
* @param midiNote {number} 0-127 the key number of the note
|
|
409
|
+
* @param force {boolean} instantly kills the note if true
|
|
410
|
+
*/
|
|
411
|
+
noteOff(channel, midiNote, force = false) {
|
|
412
|
+
if(force)
|
|
413
|
+
{
|
|
414
|
+
this.post({
|
|
415
|
+
channelNumber: channel,
|
|
416
|
+
messageType: workletMessageType.killNote,
|
|
417
|
+
messageData: midiNote
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
this.post({
|
|
422
|
+
channelNumber: channel,
|
|
423
|
+
messageType: workletMessageType.noteOff,
|
|
424
|
+
messageData: midiNote
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Stops all notes
|
|
431
|
+
* @param force {boolean} if we should instantly kill the note, defaults to false
|
|
432
|
+
*/
|
|
433
|
+
stopAll(force=false) {
|
|
434
|
+
this.post({
|
|
435
|
+
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
|
|
436
|
+
messageType: workletMessageType.stopAll,
|
|
437
|
+
messageData: force ? 1 : 0
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Changes the given controller
|
|
444
|
+
* @param channel {number} usually 0-15: the channel to change the controller
|
|
445
|
+
* @param controllerNumber {number} 0-127 the MIDI CC number
|
|
446
|
+
* @param controllerValue {number} 0-127 the controller value
|
|
447
|
+
* @param force {boolean} forces the controller change, even if it's locked or gm system is set and the cc is bank select
|
|
448
|
+
*/
|
|
449
|
+
controllerChange(channel, controllerNumber, controllerValue, force=false)
|
|
450
|
+
{
|
|
451
|
+
controllerValue = Math.floor(controllerValue);
|
|
452
|
+
controllerNumber = Math.floor(controllerNumber);
|
|
453
|
+
this.post({
|
|
454
|
+
channelNumber: channel,
|
|
455
|
+
messageType: workletMessageType.ccChange,
|
|
456
|
+
messageData: [controllerNumber, controllerValue, force]
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Resets all controllers (for every channel)
|
|
462
|
+
*/
|
|
463
|
+
resetControllers()
|
|
464
|
+
{
|
|
465
|
+
this.post({
|
|
466
|
+
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
|
|
467
|
+
messageType: workletMessageType.ccReset,
|
|
468
|
+
messageData: undefined
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Applies pressure to a given channel
|
|
474
|
+
* @param channel {number} usually 0-15: the channel to change the controller
|
|
475
|
+
* @param pressure {number} 0-127: the pressure to apply
|
|
476
|
+
*/
|
|
477
|
+
channelPressure(channel, pressure)
|
|
478
|
+
{
|
|
479
|
+
this.post({
|
|
480
|
+
channelNumber: channel,
|
|
481
|
+
messageType: workletMessageType.channelPressure,
|
|
482
|
+
messageData: pressure
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Applies pressure to a given note
|
|
488
|
+
* @param channel {number} usually 0-15: the channel to change the controller
|
|
489
|
+
* @param midiNote {number} 0-127: the MIDI note
|
|
490
|
+
* @param pressure {number} 0-127: the pressure to apply
|
|
491
|
+
*/
|
|
492
|
+
polyPressure(channel, midiNote, pressure)
|
|
493
|
+
{
|
|
494
|
+
this.post({
|
|
495
|
+
channelNumber: channel,
|
|
496
|
+
messageType: workletMessageType.polyPressure,
|
|
497
|
+
messageData: [midiNote, pressure]
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @param data {WorkletMessage}
|
|
503
|
+
*/
|
|
504
|
+
post(data)
|
|
505
|
+
{
|
|
506
|
+
this.worklet.port.postMessage(data);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Sets the pitch of the given channel
|
|
511
|
+
* @param channel {number} usually 0-15: the channel to change pitch
|
|
512
|
+
* @param MSB {number} SECOND byte of the MIDI pitchWheel message
|
|
513
|
+
* @param LSB {number} FIRST byte of the MIDI pitchWheel message
|
|
514
|
+
*/
|
|
515
|
+
pitchWheel(channel, MSB, LSB)
|
|
516
|
+
{
|
|
517
|
+
this.post({
|
|
518
|
+
channelNumber: channel,
|
|
519
|
+
messageType: workletMessageType.pitchWheel,
|
|
520
|
+
messageData: [MSB, LSB],
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Transposes the synthetizer's pitch by given semitones amount (percussion channels do not get affected)
|
|
526
|
+
* @param semitones {number} the semitones to transpose by. Can be a floating point number for more precision
|
|
527
|
+
*/
|
|
528
|
+
transpose(semitones)
|
|
529
|
+
{
|
|
530
|
+
this.transposeChannel(ALL_CHANNELS_OR_DIFFERENT_ACTION, semitones, false);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Transposes the channel by given amount of semitones
|
|
535
|
+
* @param channel {number} the channel number
|
|
536
|
+
* @param semitones {number} the transposition of the channel, can be a float
|
|
537
|
+
* @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel
|
|
538
|
+
*/
|
|
539
|
+
transposeChannel(channel, semitones, force=false)
|
|
540
|
+
{
|
|
541
|
+
this.post({
|
|
542
|
+
channelNumber: channel,
|
|
543
|
+
messageType: workletMessageType.transpose,
|
|
544
|
+
messageData: [semitones, force]
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Sets the main volume
|
|
550
|
+
* @param volume {number} 0-1 the volume
|
|
551
|
+
*/
|
|
552
|
+
setMainVolume(volume)
|
|
553
|
+
{
|
|
554
|
+
this.post({
|
|
555
|
+
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
|
|
556
|
+
messageType: workletMessageType.setMasterParameter,
|
|
557
|
+
messageData: [masterParameterType.mainVolume, volume]
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Sets the master stereo panning
|
|
563
|
+
* @param pan {number} -1 to 1, the pan (-1 is left, 0 is midde, 1 is right)
|
|
564
|
+
*/
|
|
565
|
+
setMasterPan(pan)
|
|
566
|
+
{
|
|
567
|
+
this.post({
|
|
568
|
+
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
|
|
569
|
+
messageType: workletMessageType.setMasterParameter,
|
|
570
|
+
messageData: [masterParameterType.masterPan, pan]
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Sets the channel's pitch bend range, in semitones
|
|
576
|
+
* @param channel {number} usually 0-15: the channel to change
|
|
577
|
+
* @param pitchBendRangeSemitones {number} the bend range in semitones
|
|
578
|
+
*/
|
|
579
|
+
setPitchBendRange(channel, pitchBendRangeSemitones)
|
|
580
|
+
{
|
|
581
|
+
// set range
|
|
582
|
+
this.controllerChange(channel, midiControllers.RPNMsb, 0);
|
|
583
|
+
this.controllerChange(channel, midiControllers.dataEntryMsb, pitchBendRangeSemitones);
|
|
584
|
+
|
|
585
|
+
// reset rpn
|
|
586
|
+
this.controllerChange(channel, midiControllers.RPNMsb, 127);
|
|
587
|
+
this.controllerChange(channel, midiControllers.RPNLsb, 127);
|
|
588
|
+
this.controllerChange(channel, midiControllers.dataEntryMsb, 0);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Changes the patch for a given channel
|
|
593
|
+
* @param channel {number} usually 0-15: the channel to change
|
|
594
|
+
* @param programNumber {number} 0-127 the MIDI patch number
|
|
595
|
+
* @param userChange {boolean} indicates if the program change has been called by user. defaults to false
|
|
596
|
+
*/
|
|
597
|
+
programChange(channel, programNumber, userChange=false)
|
|
598
|
+
{
|
|
599
|
+
this.post({
|
|
600
|
+
channelNumber: channel,
|
|
601
|
+
messageType: workletMessageType.programChange,
|
|
602
|
+
messageData: [programNumber, userChange]
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Causes the given midi channel to ignore controller messages for the given controller number
|
|
608
|
+
* @param channel {number} usually 0-15: the channel to lock
|
|
609
|
+
* @param controllerNumber {number} 0-127 MIDI CC number NOTE: -1 locks the preset
|
|
610
|
+
* @param isLocked {boolean} true if locked, false if unlocked
|
|
611
|
+
*/
|
|
612
|
+
lockController(channel, controllerNumber, isLocked)
|
|
613
|
+
{
|
|
614
|
+
this.post({
|
|
615
|
+
channelNumber: channel,
|
|
616
|
+
messageType: workletMessageType.lockController,
|
|
617
|
+
messageData: [controllerNumber, isLocked]
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Mutes or unmutes the given channel
|
|
623
|
+
* @param channel {number} usually 0-15: the channel to lock
|
|
624
|
+
* @param isMuted {boolean} indicates if the channel is muted
|
|
625
|
+
*/
|
|
626
|
+
muteChannel(channel, isMuted)
|
|
627
|
+
{
|
|
628
|
+
this.post({
|
|
629
|
+
channelNumber: channel,
|
|
630
|
+
messageType: workletMessageType.muteChannel,
|
|
631
|
+
messageData: isMuted
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Reloads the sounfont.
|
|
637
|
+
* @param soundFontBuffer {ArrayBuffer} the new soundfont file array buffer
|
|
638
|
+
* @return {Promise<void>}
|
|
639
|
+
*/
|
|
640
|
+
async reloadSoundFont(soundFontBuffer)
|
|
641
|
+
{
|
|
642
|
+
// copy and use transferable
|
|
643
|
+
const bufferCopy = soundFontBuffer.slice(0);
|
|
644
|
+
await new Promise(resolve => {
|
|
645
|
+
this._resolveReady = resolve;
|
|
646
|
+
this.worklet.port.postMessage({
|
|
647
|
+
channelNumber: 0,
|
|
648
|
+
messageType: workletMessageType.reloadSoundFont,
|
|
649
|
+
messageData: bufferCopy
|
|
650
|
+
}, [bufferCopy]);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Sends a MIDI Sysex message to the synthesizer
|
|
656
|
+
* @param messageData {IndexedByteArray} the message's data (excluding the F0 byte, but including the F7 at the end)
|
|
657
|
+
*/
|
|
658
|
+
systemExclusive(messageData)
|
|
659
|
+
{
|
|
660
|
+
this.post({
|
|
661
|
+
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
|
|
662
|
+
messageType: workletMessageType.systemExclusive,
|
|
663
|
+
messageData: Array.from(messageData)
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Toggles drums on a given channel
|
|
669
|
+
* @param channel {number}
|
|
670
|
+
* @param isDrum {boolean}
|
|
671
|
+
*/
|
|
672
|
+
setDrums(channel, isDrum)
|
|
673
|
+
{
|
|
674
|
+
this.post({
|
|
675
|
+
channelNumber: channel,
|
|
676
|
+
messageType: workletMessageType.setDrums,
|
|
677
|
+
messageData: isDrum
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* sends a raw MIDI message to the synthesizer
|
|
683
|
+
* @param message {ArrayLike<number>} the midi message, each number is a byte
|
|
684
|
+
*/
|
|
685
|
+
sendMessage(message)
|
|
686
|
+
{
|
|
687
|
+
// discard as soon as possible if high perf
|
|
688
|
+
const statusByteData = getEvent(message[0]);
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
// process the event
|
|
692
|
+
switch (statusByteData.status)
|
|
693
|
+
{
|
|
694
|
+
case messageTypes.noteOn:
|
|
695
|
+
const velocity = message[2];
|
|
696
|
+
if(velocity > 0) {
|
|
697
|
+
this.noteOn(statusByteData.channel, message[1], velocity);
|
|
698
|
+
}
|
|
699
|
+
else
|
|
700
|
+
{
|
|
701
|
+
this.noteOff(statusByteData.channel, message[1]);
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
|
|
705
|
+
case messageTypes.noteOff:
|
|
706
|
+
this.noteOff(statusByteData.channel, message[1]);
|
|
707
|
+
break;
|
|
708
|
+
|
|
709
|
+
case messageTypes.pitchBend:
|
|
710
|
+
this.pitchWheel(statusByteData.channel, message[2], message[1]);
|
|
711
|
+
break;
|
|
712
|
+
|
|
713
|
+
case messageTypes.controllerChange:
|
|
714
|
+
this.controllerChange(statusByteData.channel, message[1], message[2]);
|
|
715
|
+
break;
|
|
716
|
+
|
|
717
|
+
case messageTypes.programChange:
|
|
718
|
+
this.programChange(statusByteData.channel, message[1]);
|
|
719
|
+
break;
|
|
720
|
+
|
|
721
|
+
case messageTypes.polyPressure:
|
|
722
|
+
this.polyPressure(statusByteData.channel, message[0], message[1]);
|
|
723
|
+
break;
|
|
724
|
+
|
|
725
|
+
case messageTypes.channelPressure:
|
|
726
|
+
this.channelPressure(statusByteData.channel, message[1]);
|
|
727
|
+
break;
|
|
728
|
+
|
|
729
|
+
case messageTypes.systemExclusive:
|
|
730
|
+
this.systemExclusive(new IndexedByteArray(message.slice(1)));
|
|
731
|
+
break;
|
|
732
|
+
|
|
733
|
+
case messageTypes.reset:
|
|
734
|
+
this.stopAll(true);
|
|
735
|
+
this.resetControllers();
|
|
736
|
+
break;
|
|
737
|
+
|
|
738
|
+
default:
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* @returns {number} the audioContext's current time
|
|
745
|
+
*/
|
|
746
|
+
get currentTime()
|
|
747
|
+
{
|
|
748
|
+
return this.context.currentTime;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* @returns {number} the current amount of voices playing
|
|
753
|
+
*/
|
|
754
|
+
get voicesAmount()
|
|
755
|
+
{
|
|
756
|
+
return this._voicesAmount;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
reverbateEverythingBecauseWhyNot()
|
|
760
|
+
{
|
|
761
|
+
for (let i = 0; i < this.channelsAmount; i++) {
|
|
762
|
+
this.controllerChange(i, midiControllers.effects1Depth, 127);
|
|
763
|
+
}
|
|
764
|
+
return "That's the spirit!";
|
|
765
|
+
}
|
|
766
|
+
}
|