spessasynth_lib 3.26.0 → 3.26.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.
@@ -0,0 +1,637 @@
1
+ import { consoleColors } from "../utils/other.js";
2
+ import {
3
+ ALL_CHANNELS_OR_DIFFERENT_ACTION,
4
+ BasicMIDI,
5
+ masterParameterType,
6
+ MIDI,
7
+ MIDI_CHANNEL_COUNT,
8
+ SpessaSynthCoreUtils as util,
9
+ SpessaSynthLogging,
10
+ SpessaSynthProcessor,
11
+ SpessaSynthSequencer,
12
+ SynthesizerSnapshot
13
+ } from "spessasynth_core";
14
+ import { returnMessageType, workletMessageType } from "./worklet_message.js";
15
+ import { WORKLET_PROCESSOR_NAME } from "./worklet_url.js";
16
+ import { workletKeyModifierMessageType } from "./key_modifier_manager.js";
17
+ import { WorkletSoundfontManagerMessageType } from "./sfman_message.js";
18
+ import {
19
+ SongChangeType,
20
+ SpessaSynthSequencerMessageType,
21
+ SpessaSynthSequencerReturnMessageType
22
+ } from "../sequencer/sequencer_message.js";
23
+ import { fillWithDefaults } from "../utils/fill_with_defaults.js";
24
+ import { DEFAULT_SEQUENCER_OPTIONS } from "../sequencer/default_sequencer_options.js";
25
+ import { MIDIData } from "../sequencer/midi_data.js";
26
+
27
+
28
+ // a worklet processor wrapper for the synthesizer core
29
+ class WorkletSpessaProcessor extends AudioWorkletProcessor
30
+ {
31
+ /**
32
+ * If the worklet is alive
33
+ * @type {boolean}
34
+ */
35
+ alive = true;
36
+
37
+ /**
38
+ * Instead of 18 stereo outputs, there's one with 32 channels (no effects)
39
+ * @type {boolean}
40
+ */
41
+ oneOutputMode = false;
42
+
43
+ /**
44
+ * Creates a new worklet synthesis system. contains all channels
45
+ * @param options {{
46
+ * processorOptions: {
47
+ * midiChannels: number,
48
+ * soundfont: ArrayBuffer,
49
+ * enableEventSystem: boolean,
50
+ * startRenderingData: StartRenderingDataConfig
51
+ * }}}
52
+ */
53
+ constructor(options)
54
+ {
55
+ super();
56
+ const opts = options.processorOptions;
57
+
58
+
59
+ // one output is indicated by setting midiChannels to 1
60
+ this.oneOutputMode = opts.midiChannels === 1;
61
+
62
+ // prepare synthesizer connections
63
+ /**
64
+ * @param t {returnMessageType}
65
+ * @param d {any}
66
+ */
67
+ const postSyn = (t, d) =>
68
+ {
69
+ // noinspection JSCheckFunctionSignatures
70
+ this.postMessageToMainThread({
71
+ messageType: t,
72
+ messageData: d
73
+ });
74
+ };
75
+
76
+ // start rendering data
77
+ const startRenderingData = opts?.startRenderingData;
78
+ /**
79
+ * The snapshot that synth was restored from
80
+ * @type {SynthesizerSnapshot|undefined}
81
+ * @private
82
+ */
83
+ const snapshot = startRenderingData?.snapshot;
84
+
85
+ // noinspection JSUnresolvedReference
86
+ /**
87
+ * Initialize the synthesis engine
88
+ * @type {SpessaSynthProcessor}
89
+ */
90
+ this.synthesizer = new SpessaSynthProcessor(
91
+ opts.soundfont, // initial sound bank
92
+ sampleRate, // AudioWorkletGlobalScope
93
+ {
94
+ eventCall: (t, d) =>
95
+ {
96
+ postSyn(returnMessageType.eventCall, {
97
+ eventName: t,
98
+ eventData: d
99
+ });
100
+ },
101
+ ready: this.postReady.bind(this),
102
+ channelPropertyChange: (p, n) => postSyn(returnMessageType.channelPropertyChange, [n, p]),
103
+ masterParameterChange: (t, v) => postSyn(returnMessageType.masterParameterChange, [t, v])
104
+ },
105
+ !this.oneOutputMode, // one output mode disables effects
106
+ opts?.enableEventSystem, // enable message port?
107
+ currentTime, // AudioWorkletGlobalScope, sync with audioContext time
108
+ MIDI_CHANNEL_COUNT, // midi channel count (16)
109
+ snapshot
110
+ );
111
+
112
+ // initialize the sequencer engine
113
+ this.sequencer = new SpessaSynthSequencer(this.synthesizer);
114
+
115
+ const postSeq = (type, data) =>
116
+ {
117
+ this.postMessageToMainThread({
118
+ messageType: returnMessageType.sequencerSpecific,
119
+ messageData: {
120
+ messageType: type,
121
+ messageData: data
122
+ }
123
+ });
124
+ };
125
+
126
+ // receive messages from the main thread
127
+ this.port.onmessage = e => this.handleMessage(e.data);
128
+
129
+ // sequencer events
130
+ this.sequencer.onMIDIMessage = m =>
131
+ {
132
+ postSeq(SpessaSynthSequencerReturnMessageType.midiEvent, m);
133
+ };
134
+ this.sequencer.onTimeChange = t =>
135
+ {
136
+ postSeq(SpessaSynthSequencerReturnMessageType.timeChange, t);
137
+ };
138
+ this.sequencer.onPlaybackStop = p =>
139
+ {
140
+ postSeq(SpessaSynthSequencerReturnMessageType.pause, p);
141
+ };
142
+ this.sequencer.onSongChange = (i, a) =>
143
+ {
144
+ postSeq(SpessaSynthSequencerReturnMessageType.songChange, [i, a]);
145
+ };
146
+ this.sequencer.onMetaEvent = (e, i) =>
147
+ {
148
+ postSeq(SpessaSynthSequencerReturnMessageType.metaEvent, [e, i]);
149
+ };
150
+ this.sequencer.onLoopCountChange = c =>
151
+ {
152
+ postSeq(SpessaSynthSequencerReturnMessageType.loopCountChange, c);
153
+ };
154
+ this.sequencer.onSongListChange = l =>
155
+ {
156
+ const midiDataList = l.map(s => new MIDIData(s));
157
+ this.postMessageToMainThread({
158
+ messageType: returnMessageType.sequencerSpecific,
159
+ messageData: {
160
+ messageType: SpessaSynthSequencerReturnMessageType.songListChange,
161
+ messageData: midiDataList
162
+ }
163
+ });
164
+ };
165
+
166
+ // if sent, start rendering
167
+ if (startRenderingData)
168
+ {
169
+ if (snapshot !== undefined)
170
+ {
171
+ this.synthesizer.applySynthesizerSnapshot(snapshot);
172
+ }
173
+
174
+ util.SpessaSynthInfo("%cRendering enabled! Starting render.", consoleColors.info);
175
+ if (startRenderingData.parsedMIDI)
176
+ {
177
+ if (startRenderingData?.loopCount !== undefined)
178
+ {
179
+ this.sequencer.loopCount = startRenderingData?.loopCount;
180
+ this.sequencer.loop = true;
181
+ }
182
+ else
183
+ {
184
+ this.sequencer.loop = false;
185
+ }
186
+ // set voice cap to unlimited
187
+ this.synthesizer.voiceCap = Infinity;
188
+ this.synthesizer.processorInitialized.then(() =>
189
+ {
190
+ /**
191
+ * set options
192
+ * @type {SequencerOptions}
193
+ */
194
+ const seqOptions = fillWithDefaults(
195
+ startRenderingData.sequencerOptions,
196
+ DEFAULT_SEQUENCER_OPTIONS
197
+ );
198
+ this.sequencer.skipToFirstNoteOn = seqOptions.skipToFirstNoteOn;
199
+ this.sequencer.preservePlaybackState = seqOptions.preservePlaybackState;
200
+ // autoplay is ignored
201
+ try
202
+ {
203
+ this.sequencer.loadNewSongList([startRenderingData.parsedMIDI]);
204
+ }
205
+ catch (e)
206
+ {
207
+ console.error(e);
208
+ postSeq(SpessaSynthSequencerReturnMessageType.midiError, e);
209
+ }
210
+ });
211
+ }
212
+ }
213
+ }
214
+
215
+ postReady()
216
+ {
217
+ this.postMessageToMainThread({
218
+ messageType: returnMessageType.isFullyInitialized,
219
+ messageData: undefined
220
+ });
221
+ }
222
+
223
+ /**
224
+ * @param data {WorkletReturnMessage}
225
+ */
226
+ postMessageToMainThread(data)
227
+ {
228
+ this.port.postMessage(data);
229
+ }
230
+
231
+ /**
232
+ * @this {WorkletSpessaProcessor}
233
+ * @param message {WorkletMessage}
234
+ */
235
+ handleMessage(message)
236
+ {
237
+ const data = message.messageData;
238
+ const channel = message.channelNumber;
239
+
240
+ let channelObject;
241
+ if (channel >= 0)
242
+ {
243
+ channelObject = this.synthesizer.midiAudioChannels[channel];
244
+ if (channelObject === undefined)
245
+ {
246
+ util.SpessaSynthWarn(`Trying to access channel ${channel} which does not exist... ignoring!`);
247
+ return;
248
+ }
249
+ }
250
+ switch (message.messageType)
251
+ {
252
+ case workletMessageType.midiMessage:
253
+ this.synthesizer.processMessage(...data);
254
+ break;
255
+
256
+ case workletMessageType.customCcChange:
257
+ // custom controller change
258
+ channelObject.setCustomController(data[0], data[1]);
259
+ channelObject.updateChannelTuning();
260
+ break;
261
+
262
+ case workletMessageType.ccReset:
263
+ if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION)
264
+ {
265
+ this.synthesizer.resetAllControllers();
266
+ }
267
+ else
268
+ {
269
+ channelObject.resetControllers();
270
+ }
271
+ break;
272
+
273
+ case workletMessageType.setChannelVibrato:
274
+ if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION)
275
+ {
276
+ for (let i = 0; i < this.synthesizer.midiAudioChannels.length; i++)
277
+ {
278
+ const chan = this.synthesizer.midiAudioChannels[i];
279
+ if (data.rate === ALL_CHANNELS_OR_DIFFERENT_ACTION)
280
+ {
281
+ chan.disableAndLockGSNRPN();
282
+ }
283
+ else
284
+ {
285
+ chan.setVibrato(data.depth, data.rate, data.delay);
286
+ }
287
+ }
288
+ }
289
+ else if (data.rate === ALL_CHANNELS_OR_DIFFERENT_ACTION)
290
+ {
291
+ channelObject.disableAndLockGSNRPN();
292
+ }
293
+ else
294
+ {
295
+ channelObject.setVibrato(data.depth, data.rate, data.delay);
296
+ }
297
+ break;
298
+
299
+ case workletMessageType.stopAll:
300
+ if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION)
301
+ {
302
+ this.synthesizer.stopAllChannels(data === 1);
303
+ }
304
+ else
305
+ {
306
+ channelObject.stopAllNotes(data === 1);
307
+ }
308
+ break;
309
+
310
+ case workletMessageType.killNotes:
311
+ this.synthesizer.voiceKilling(data);
312
+ break;
313
+
314
+ case workletMessageType.muteChannel:
315
+ channelObject.muteChannel(data);
316
+ break;
317
+
318
+ case workletMessageType.addNewChannel:
319
+ this.synthesizer.createWorkletChannel(true);
320
+ break;
321
+
322
+ case workletMessageType.debugMessage:
323
+ console.debug(this.synthesizer);
324
+ break;
325
+
326
+ case workletMessageType.setMasterParameter:
327
+ /**
328
+ * @type {masterParameterType}
329
+ */
330
+ const type = data[0];
331
+ const value = data[1];
332
+ this.synthesizer.setMasterParameter(type, value);
333
+ break;
334
+
335
+ case workletMessageType.setDrums:
336
+ channelObject.setDrums(data);
337
+ break;
338
+
339
+ case workletMessageType.transpose:
340
+ if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION)
341
+ {
342
+ this.synthesizer.transposeAllChannels(data[0], data[1]);
343
+ }
344
+ else
345
+ {
346
+ channelObject.transposeChannel(data[0], data[1]);
347
+ }
348
+ break;
349
+
350
+ case workletMessageType.highPerformanceMode:
351
+ this.synthesizer.highPerformanceMode = data;
352
+ break;
353
+
354
+ case workletMessageType.lockController:
355
+ if (data[0] === ALL_CHANNELS_OR_DIFFERENT_ACTION)
356
+ {
357
+ channelObject.setPresetLock(data[1]);
358
+ }
359
+ else
360
+ {
361
+ channelObject.lockedControllers[data[0]] = data[1];
362
+ }
363
+ break;
364
+
365
+ case workletMessageType.sequencerSpecific:
366
+ const seq = this.sequencer;
367
+ const messageData = data.messageData;
368
+ const messageType = data.messageType;
369
+ switch (messageType)
370
+ {
371
+ default:
372
+ break;
373
+
374
+ case SpessaSynthSequencerMessageType.loadNewSongList:
375
+ try
376
+ {
377
+ /**
378
+ * @type {(BasicMIDI|{binary: ArrayBuffer, altName: string})[]}
379
+ */
380
+ const sList = messageData[0];
381
+ const songMap = sList.map(s =>
382
+ {
383
+ if (s.duration)
384
+ {
385
+ return s;
386
+ }
387
+ return new MIDI(s.binary, s.altName);
388
+ });
389
+ seq.loadNewSongList(songMap, messageData[1]);
390
+ }
391
+ catch (e)
392
+ {
393
+ console.error(e);
394
+ this.postMessageToMainThread({
395
+ messageType: returnMessageType.sequencerSpecific,
396
+ messageData: {
397
+ messageType: SpessaSynthSequencerReturnMessageType.midiError,
398
+ messageData: e
399
+ }
400
+ });
401
+ }
402
+ break;
403
+
404
+ case SpessaSynthSequencerMessageType.pause:
405
+ seq.pause();
406
+ break;
407
+
408
+ case SpessaSynthSequencerMessageType.play:
409
+ seq.play(messageData);
410
+ break;
411
+
412
+ case SpessaSynthSequencerMessageType.stop:
413
+ seq.stop();
414
+ break;
415
+
416
+ case SpessaSynthSequencerMessageType.setTime:
417
+ seq.currentTime = messageData;
418
+ break;
419
+
420
+ case SpessaSynthSequencerMessageType.changeMIDIMessageSending:
421
+ seq.sendMIDIMessages = messageData;
422
+ break;
423
+
424
+ case SpessaSynthSequencerMessageType.setPlaybackRate:
425
+ seq.playbackRate = messageData;
426
+ break;
427
+
428
+ case SpessaSynthSequencerMessageType.setLoop:
429
+ const [loop, count] = messageData;
430
+ seq.loop = loop;
431
+ if (count === ALL_CHANNELS_OR_DIFFERENT_ACTION)
432
+ {
433
+ seq.loopCount = Infinity;
434
+ }
435
+ else
436
+ {
437
+ seq.loopCount = count;
438
+ }
439
+ break;
440
+
441
+ case SpessaSynthSequencerMessageType.changeSong:
442
+ switch (messageData[0])
443
+ {
444
+ case SongChangeType.forwards:
445
+ seq.nextSong();
446
+ break;
447
+
448
+ case SongChangeType.backwards:
449
+ seq.previousSong();
450
+ break;
451
+
452
+ case SongChangeType.shuffleOff:
453
+ seq.shuffleMode = false;
454
+ seq.songIndex = seq.shuffledSongIndexes[seq.songIndex];
455
+ break;
456
+
457
+ case SongChangeType.shuffleOn:
458
+ seq.shuffleMode = true;
459
+ seq.shuffleSongIndexes();
460
+ seq.songIndex = 0;
461
+ seq.loadCurrentSong();
462
+ break;
463
+
464
+ case SongChangeType.index:
465
+ seq.songIndex = messageData[1];
466
+ seq.loadCurrentSong();
467
+ break;
468
+ }
469
+ break;
470
+
471
+ case SpessaSynthSequencerMessageType.getMIDI:
472
+ this.postMessageToMainThread({
473
+ messageType: returnMessageType.sequencerSpecific,
474
+ messageData: {
475
+ messageType: SpessaSynthSequencerReturnMessageType.getMIDI,
476
+ messageData: seq.midiData
477
+ }
478
+ });
479
+ break;
480
+
481
+ case SpessaSynthSequencerMessageType.setSkipToFirstNote:
482
+ seq.skipToFirstNoteOn = messageData;
483
+ break;
484
+
485
+ case SpessaSynthSequencerMessageType.setPreservePlaybackState:
486
+ seq.preservePlaybackState = messageData;
487
+ }
488
+ break;
489
+
490
+ case workletMessageType.soundFontManager:
491
+ try
492
+ {
493
+ const sfManager = this.synthesizer.soundfontManager;
494
+ const type = data[0];
495
+ const messageData = data[1];
496
+ switch (type)
497
+ {
498
+ case WorkletSoundfontManagerMessageType.addNewSoundFont:
499
+ sfManager.addNewSoundFont(messageData[0], messageData[1], messageData[2]);
500
+ break;
501
+
502
+ case WorkletSoundfontManagerMessageType.reloadSoundFont:
503
+ sfManager.reloadManager(messageData);
504
+ break;
505
+
506
+ case WorkletSoundfontManagerMessageType.deleteSoundFont:
507
+ sfManager.deleteSoundFont(messageData);
508
+ break;
509
+
510
+ case WorkletSoundfontManagerMessageType.rearrangeSoundFonts:
511
+ sfManager.rearrangeSoundFonts(messageData);
512
+ }
513
+ }
514
+ catch (e)
515
+ {
516
+ this.postMessageToMainThread({
517
+ messageType: returnMessageType.soundfontError,
518
+ messageData: e
519
+ });
520
+ }
521
+ this.synthesizer.clearSoundFont(true, false);
522
+ break;
523
+
524
+ case workletMessageType.keyModifierManager:
525
+ /**
526
+ * @type {workletKeyModifierMessageType}
527
+ */
528
+ const keyMessageType = data[0];
529
+ const man = this.synthesizer.keyModifierManager;
530
+ const keyMessageData = data[1];
531
+ switch (keyMessageType)
532
+ {
533
+ default:
534
+ return;
535
+
536
+ case workletKeyModifierMessageType.addMapping:
537
+ man.addMapping(...keyMessageData);
538
+ break;
539
+
540
+ case workletKeyModifierMessageType.clearMappings:
541
+ man.clearMappings();
542
+ break;
543
+
544
+ case workletKeyModifierMessageType.deleteMapping:
545
+ man.deleteMapping(...keyMessageData);
546
+ }
547
+ break;
548
+
549
+ case workletMessageType.requestSynthesizerSnapshot:
550
+ const snapshot = SynthesizerSnapshot.createSynthesizerSnapshot(this.synthesizer);
551
+ this.postMessageToMainThread({
552
+ messageType: returnMessageType.synthesizerSnapshot,
553
+ messageData: snapshot
554
+ });
555
+ break;
556
+
557
+ case workletMessageType.setLogLevel:
558
+ SpessaSynthLogging(data[0], data[1], data[2], data[3]);
559
+ break;
560
+
561
+ case workletMessageType.setEffectsGain:
562
+ this.synthesizer.reverbGain = data[0];
563
+ this.synthesizer.chorusGain = data[1];
564
+ break;
565
+
566
+ case workletMessageType.destroyWorklet:
567
+ this.alive = false;
568
+ this.synthesizer.destroySynthProcessor();
569
+ delete this.synthesizer;
570
+ delete this.sequencer.midiData;
571
+ delete this.sequencer;
572
+ break;
573
+
574
+ default:
575
+ util.SpessaSynthWarn("Unrecognized event:", data);
576
+ break;
577
+ }
578
+ }
579
+
580
+ // noinspection JSUnusedGlobalSymbols
581
+ /**
582
+ * the audio worklet processing logic
583
+ * @param inputs {Float32Array[][]} required by WebAudioAPI
584
+ * @param outputs {Float32Array[][]} the outputs to write to, only the first two channels are populated
585
+ * @returns {boolean} true unless it's not alive
586
+ */
587
+ process(inputs, outputs)
588
+ {
589
+ if (!this.alive)
590
+ {
591
+ return false;
592
+ }
593
+ // process sequencer
594
+ this.sequencer.processTick();
595
+
596
+ if (this.oneOutputMode)
597
+ {
598
+ const out = outputs[0];
599
+ // 1 output with 32 channels.
600
+ // channels are ordered as follows:
601
+ // midiChannel1L, midiChannel1R,
602
+ // midiChannel2L, midiChannel2R
603
+ // and so on
604
+ /**
605
+ * @type {Float32Array[][]}
606
+ */
607
+ const channelMap = [];
608
+ for (let i = 0; i < 32; i += 2)
609
+ {
610
+ channelMap.push([out[i], out[i + 1]]);
611
+ }
612
+ this.synthesizer.renderAudioSplit(
613
+ [], [], // effects are disabled
614
+ channelMap
615
+ );
616
+ }
617
+ else
618
+ {
619
+ // 18 outputs, each a stereo one
620
+ // 0: reverb
621
+ // 1: chorus
622
+ // 2: channel 1
623
+ // 3: channel 2
624
+ // and so on
625
+ this.synthesizer.renderAudioSplit(
626
+ outputs[0], // reverb
627
+ outputs[1], // chorus
628
+ outputs.slice(2)
629
+ );
630
+ }
631
+ return true;
632
+ }
633
+ }
634
+
635
+ // noinspection JSUnresolvedReference
636
+ registerProcessor(WORKLET_PROCESSOR_NAME, WorkletSpessaProcessor);
637
+ util.SpessaSynthInfo("%cProcessor successfully registered!", consoleColors.recognized);