spessasynth_lib 3.25.17 → 3.25.18

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.
@@ -80,7 +80,8 @@ export class MIDIBuilder extends BasicMIDI
80
80
  }
81
81
  if (event === messageTypes.endOfTrack)
82
82
  {
83
- SpessaSynthWarn("The EndOfTrack is added automatically. Ignoring!");
83
+ SpessaSynthWarn(
84
+ "The EndOfTrack is added automatically and does not influence the duration. Consider adding a voice event instead.");
84
85
  return;
85
86
  }
86
87
  // remove the end of track
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.25.17",
3
+ "version": "3.25.18",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "browser": "index.js",
6
6
  "type": "module",
@@ -316,7 +316,7 @@ export function play(resetTime = false)
316
316
  {
317
317
  this.playingNotes.forEach(n =>
318
318
  {
319
- this.synth.noteOn(n.channel, n.midiNote, n.velocity, false, true);
319
+ this.synth.noteOn(n.channel, n.midiNote, n.velocity);
320
320
  });
321
321
  }
322
322
  this.setProcessHandler();
@@ -39,6 +39,14 @@ export function readArticulation(chunk, disableVibrato)
39
39
  const scale = readLittleEndian(artData, 4) | 0;
40
40
  const value = scale >> 16; // convert it to 16 bit as soundfont uses that
41
41
 
42
+ // modulatorConverterDebug(
43
+ // source,
44
+ // control,
45
+ // destination,
46
+ // value,
47
+ // transform
48
+ // );
49
+
42
50
  // interpret this somehow...
43
51
  // if source and control are both zero, it's a generator
44
52
  if (source === 0 && control === 0 && transform === 0)
@@ -147,6 +155,29 @@ export function readArticulation(chunk, disableVibrato)
147
155
  // if not, modulator?
148
156
  {
149
157
  let isGenerator = true;
158
+
159
+ const applyKeyToCorrection = (value, keyToGen, realGen) =>
160
+ {
161
+ // according to viena and another strange (with modulators) rendition of gm.dls in sf2,
162
+ // it shall be divided by -128
163
+ // and a strange correction needs to be applied to the real value:
164
+ // real + (60 / 128) * scale
165
+ const keyToGenValue = value / -128;
166
+ generators.push(new Generator(keyToGen, keyToGenValue));
167
+ // airfont 340 fix
168
+ if (keyToGenValue <= 120)
169
+ {
170
+ const correction = Math.round((60 / 128) * value);
171
+ generators.forEach(g =>
172
+ {
173
+ if (g.generatorType === realGen)
174
+ {
175
+ g.generatorValue += correction;
176
+ }
177
+ });
178
+ }
179
+ };
180
+
150
181
  // a few special cases which are generators:
151
182
  if (control === DLSSources.none)
152
183
  {
@@ -197,73 +228,25 @@ export function readArticulation(chunk, disableVibrato)
197
228
  // key to vol env hold
198
229
  if (source === DLSSources.keyNum && destination === DLSDestinations.volEnvHold)
199
230
  {
200
- // according to viena and another strange (with modulators) rendition of gm.dls in sf2,
201
- // it shall be divided by -128
202
- // and a strange correction needs to be applied to the real value:
203
- // real + (60 / 128) * scale
204
- generators.push(new Generator(generatorTypes.keyNumToVolEnvHold, value / -128));
205
- const correction = Math.round((60 / 128) * value);
206
- generators.forEach(g =>
207
- {
208
- if (g.generatorType === generatorTypes.holdVolEnv)
209
- {
210
- g.generatorValue += correction;
211
- }
212
- });
231
+ applyKeyToCorrection(value, generatorTypes.keyNumToVolEnvHold, generatorTypes.holdVolEnv);
213
232
  }
214
233
  else
215
234
  // key to vol env decay
216
235
  if (source === DLSSources.keyNum && destination === DLSDestinations.volEnvDecay)
217
236
  {
218
- // according to viena and another strange (with modulators) rendition of gm.dls in sf2,
219
- // it shall be divided by -128
220
- // and a strange correction needs to be applied to the real value:
221
- // real + (60 / 128) * scale
222
- generators.push(new Generator(generatorTypes.keyNumToVolEnvDecay, value / -128));
223
- const correction = Math.round((60 / 128) * value);
224
- generators.forEach(g =>
225
- {
226
- if (g.generatorType === generatorTypes.decayVolEnv)
227
- {
228
- g.generatorValue += correction;
229
- }
230
- });
237
+ applyKeyToCorrection(value, generatorTypes.keyNumToVolEnvDecay, generatorTypes.decayVolEnv);
231
238
  }
232
239
  else
233
240
  // key to mod env hold
234
241
  if (source === DLSSources.keyNum && destination === DLSDestinations.modEnvHold)
235
242
  {
236
- // according to viena and another strange (with modulators) rendition of gm.dls in sf2,
237
- // it shall be divided by -128
238
- // and a strange correction needs to be applied to the real value:
239
- // real + (60 / 128) * scale
240
- generators.push(new Generator(generatorTypes.keyNumToModEnvHold, value / -128));
241
- const correction = Math.round((60 / 128) * value);
242
- generators.forEach(g =>
243
- {
244
- if (g.generatorType === generatorTypes.holdModEnv)
245
- {
246
- g.generatorValue += correction;
247
- }
248
- });
243
+ applyKeyToCorrection(value, generatorTypes.keyNumToModEnvHold, generatorTypes.holdModEnv);
249
244
  }
250
245
  else
251
246
  // key to mod env decay
252
247
  if (source === DLSSources.keyNum && destination === DLSDestinations.modEnvDecay)
253
248
  {
254
- // according to viena and another strange (with modulators) rendition of gm.dls in sf2,
255
- // it shall be divided by -128
256
- // and a strange correction needs to be applied to the real value:
257
- // real + (60 / 128) * scale
258
- generators.push(new Generator(generatorTypes.keyNumToModEnvDecay, value / -128));
259
- const correction = Math.round((60 / 128) * value);
260
- generators.forEach(g =>
261
- {
262
- if (g.generatorType === generatorTypes.decayModEnv)
263
- {
264
- g.generatorValue += correction;
265
- }
266
- });
249
+ applyKeyToCorrection(value, generatorTypes.keyNumToModEnvDecay, generatorTypes.decayModEnv);
267
250
  }
268
251
  else
269
252
  {
@@ -27,7 +27,6 @@
27
27
  * @property {number} channel - The MIDI channel number.
28
28
  * @property {number} program - The program number.
29
29
  * @property {number} bank - The bank number.
30
- * @property {boolean} userCalled - Indicates if the change was user-initiated.
31
30
  */
32
31
 
33
32
  /**
@@ -1,6 +1,5 @@
1
- import { IndexedByteArray } from "../utils/indexed_array.js";
2
1
  import { consoleColors } from "../utils/other.js";
3
- import { getEvent, messageTypes, midiControllers } from "../midi_parser/midi_message.js";
2
+ import { messageTypes, midiControllers } from "../midi_parser/midi_message.js";
4
3
  import { EventHandler } from "./synth_event_handler.js";
5
4
  import { FancyChorus } from "./audio_effects/fancy_chorus.js";
6
5
  import { getReverbProcessor } from "./audio_effects/reverb.js";
@@ -32,6 +31,18 @@ import { DEFAULT_SEQUENCER_OPTIONS } from "../sequencer/default_sequencer_option
32
31
  * purpose: responds to midi messages and called functions, managing the channels and passing the messages to them
33
32
  */
34
33
 
34
+ /**
35
+ * @typedef {Object} SynthMethodOptions
36
+ * @property {number} time - the audio context time when the event should execute, in seconds.
37
+ */
38
+
39
+ /**
40
+ * @type {SynthMethodOptions}
41
+ */
42
+ const DEFAULT_SYNTH_METHOD_OPTIONS = {
43
+ time: 0
44
+ };
45
+
35
46
 
36
47
  export class Synthetizer
37
48
  {
@@ -566,20 +577,53 @@ export class Synthetizer
566
577
  });
567
578
  }
568
579
 
580
+ /**
581
+ * sends a raw MIDI message to the synthesizer.
582
+ * @param message {number[]|Uint8Array} the midi message, each number is a byte.
583
+ * @param channelOffset {number} the channel offset of the message.
584
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
585
+ */
586
+ sendMessage(message, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
587
+ {
588
+ this._sendInternal(message, channelOffset, false, eventOptions);
589
+ }
590
+
591
+ /**
592
+ * @param message {number[]|Uint8Array}
593
+ * @param offset {number}
594
+ * @param force {boolean}
595
+ * @param eventOptions {SynthMethodOptions}
596
+ * @private
597
+ */
598
+ _sendInternal(message, offset, force = false, eventOptions)
599
+ {
600
+ const opts = fillWithDefaults(eventOptions ?? {}, DEFAULT_SYNTH_METHOD_OPTIONS);
601
+ this.post({
602
+ messageType: workletMessageType.midiMessage,
603
+ messageData: [new Uint8Array(message), offset, force, opts]
604
+ });
605
+ }
606
+
607
+
569
608
  /**
570
609
  * Starts playing a note
571
610
  * @param channel {number} usually 0-15: the channel to play the note.
572
611
  * @param midiNote {number} 0-127 the key number of the note.
573
612
  * @param velocity {number} 0-127 the velocity of the note (generally controls loudness).
574
- * @param enableDebugging {boolean} set to true to log technical details to console.
613
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
575
614
  */
576
- noteOn(channel, midiNote, velocity, enableDebugging = false)
615
+ noteOn(channel, midiNote, velocity, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
577
616
  {
578
- this.post({
579
- channelNumber: channel,
580
- messageType: workletMessageType.noteOn,
581
- messageData: [midiNote, velocity, enableDebugging]
582
- });
617
+ const ch = channel % 16;
618
+ const offset = channel - ch;
619
+ midiNote %= 128;
620
+ velocity %= 128;
621
+ // check for legacy "enableDebugging"
622
+ if (eventOptions === true)
623
+ {
624
+ eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS;
625
+ }
626
+ this.sendMessage([messageTypes.noteOn | ch, midiNote, velocity], offset, eventOptions);
583
627
  }
584
628
 
585
629
  /**
@@ -587,25 +631,15 @@ export class Synthetizer
587
631
  * @param channel {number} usually 0-15: the channel of the note.
588
632
  * @param midiNote {number} 0-127 the key number of the note.
589
633
  * @param force {boolean} instantly kills the note if true.
634
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
590
635
  */
591
- noteOff(channel, midiNote, force = false)
636
+ noteOff(channel, midiNote, force = false, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
592
637
  {
593
- if (force)
594
- {
595
- this.post({
596
- channelNumber: channel,
597
- messageType: workletMessageType.killNote,
598
- messageData: midiNote
599
- });
600
- }
601
- else
602
- {
603
- this.post({
604
- channelNumber: channel,
605
- messageType: workletMessageType.noteOff,
606
- messageData: midiNote
607
- });
608
- }
638
+ midiNote %= 128;
639
+
640
+ const ch = channel % 16;
641
+ const offset = channel - ch;
642
+ this._sendInternal([messageTypes.noteOff | ch, midiNote], offset, force, eventOptions);
609
643
  }
610
644
 
611
645
  /**
@@ -628,20 +662,25 @@ export class Synthetizer
628
662
  * @param controllerNumber {number} 0-127 the MIDI CC number.
629
663
  * @param controllerValue {number} 0-127 the controller value.
630
664
  * @param force {boolean} forces the controller-change message, even if it's locked or gm system is set and the cc is bank select.
665
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
631
666
  */
632
- controllerChange(channel, controllerNumber, controllerValue, force = false)
667
+ controllerChange(channel, controllerNumber, controllerValue, force = false, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
633
668
  {
634
669
  if (controllerNumber > 127 || controllerNumber < 0)
635
670
  {
636
671
  throw new Error(`Invalid controller number: ${controllerNumber}`);
637
672
  }
638
- controllerValue = Math.floor(controllerValue);
639
- controllerNumber = Math.floor(controllerNumber);
640
- this.post({
641
- channelNumber: channel,
642
- messageType: workletMessageType.ccChange,
643
- messageData: [controllerNumber, controllerValue, force]
644
- });
673
+ controllerValue = Math.floor(controllerValue) % 128;
674
+ controllerNumber = Math.floor(controllerNumber) % 128;
675
+ // controller change has its own message for the force property
676
+ const ch = channel % 16;
677
+ const offset = channel - ch;
678
+ this._sendInternal(
679
+ [messageTypes.controllerChange | ch, controllerNumber, controllerValue],
680
+ offset,
681
+ force,
682
+ eventOptions
683
+ );
645
684
  }
646
685
 
647
686
  /**
@@ -660,14 +699,14 @@ export class Synthetizer
660
699
  * Applies pressure to a given channel.
661
700
  * @param channel {number} usually 0-15: the channel to change the controller.
662
701
  * @param pressure {number} 0-127: the pressure to apply.
702
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
663
703
  */
664
- channelPressure(channel, pressure)
704
+ channelPressure(channel, pressure, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
665
705
  {
666
- this.post({
667
- channelNumber: channel,
668
- messageType: workletMessageType.channelPressure,
669
- messageData: pressure
670
- });
706
+ const ch = channel % 16;
707
+ const offset = channel - ch;
708
+ pressure %= 128;
709
+ this.sendMessage([messageTypes.channelPressure | ch, pressure], offset, eventOptions);
671
710
  }
672
711
 
673
712
  /**
@@ -675,14 +714,29 @@ export class Synthetizer
675
714
  * @param channel {number} usually 0-15: the channel to change the controller.
676
715
  * @param midiNote {number} 0-127: the MIDI note.
677
716
  * @param pressure {number} 0-127: the pressure to apply.
717
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
678
718
  */
679
- polyPressure(channel, midiNote, pressure)
719
+ polyPressure(channel, midiNote, pressure, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
680
720
  {
681
- this.post({
682
- channelNumber: channel,
683
- messageType: workletMessageType.polyPressure,
684
- messageData: [midiNote, pressure]
685
- });
721
+ const ch = channel % 16;
722
+ const offset = channel - ch;
723
+ midiNote %= 128;
724
+ pressure %= 128;
725
+ this.sendMessage([messageTypes.polyPressure | ch, midiNote, pressure], offset, eventOptions);
726
+ }
727
+
728
+ /**
729
+ * Sets the pitch of the given channel.
730
+ * @param channel {number} usually 0-15: the channel to change pitch.
731
+ * @param MSB {number} SECOND byte of the MIDI pitchWheel message.
732
+ * @param LSB {number} FIRST byte of the MIDI pitchWheel message.
733
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
734
+ */
735
+ pitchWheel(channel, MSB, LSB, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
736
+ {
737
+ const ch = channel % 16;
738
+ const offset = channel - ch;
739
+ this.sendMessage([messageTypes.pitchBend | ch, LSB, MSB], offset, eventOptions);
686
740
  }
687
741
 
688
742
  /**
@@ -697,21 +751,6 @@ export class Synthetizer
697
751
  this.worklet.port.postMessage(data);
698
752
  }
699
753
 
700
- /**
701
- * Sets the pitch of the given channel.
702
- * @param channel {number} usually 0-15: the channel to change pitch.
703
- * @param MSB {number} SECOND byte of the MIDI pitchWheel message.
704
- * @param LSB {number} FIRST byte of the MIDI pitchWheel message.
705
- */
706
- pitchWheel(channel, MSB, LSB)
707
- {
708
- this.post({
709
- channelNumber: channel,
710
- messageType: workletMessageType.pitchWheel,
711
- messageData: [MSB, LSB]
712
- });
713
- }
714
-
715
754
  /**
716
755
  * Transposes the synthetizer's pitch by given semitones amount (percussion channels don’t get affected).
717
756
  * @param semitones {number} the semitones to transpose by.
@@ -776,16 +815,14 @@ export class Synthetizer
776
815
  * Changes the patch for a given channel
777
816
  * @param channel {number} usually 0-15: the channel to change
778
817
  * @param programNumber {number} 0-127 the MIDI patch number
779
- * @param userChange {boolean} indicates if user has called the program change.
780
818
  * defaults to false
781
819
  */
782
- programChange(channel, programNumber, userChange = false)
820
+ programChange(channel, programNumber)
783
821
  {
784
- this.post({
785
- channelNumber: channel,
786
- messageType: workletMessageType.programChange,
787
- messageData: [programNumber, userChange]
788
- });
822
+ const ch = channel % 16;
823
+ const offset = channel - ch;
824
+ programNumber %= 128;
825
+ this.sendMessage([messageTypes.programChange | ch, programNumber], offset);
789
826
  }
790
827
 
791
828
  /**
@@ -796,11 +833,14 @@ export class Synthetizer
796
833
  */
797
834
  velocityOverride(channel, velocity)
798
835
  {
799
- this.post({
800
- channelNumber: channel,
801
- messageType: workletMessageType.ccChange,
802
- messageData: [channelConfiguration.velocityOverride, velocity, true]
803
- });
836
+ const ch = channel % 16;
837
+ const offset = channel - ch;
838
+ this._sendInternal(
839
+ [messageTypes.controllerChange | ch, channelConfiguration.velocityOverride, velocity],
840
+ offset,
841
+ true,
842
+ DEFAULT_SYNTH_METHOD_OPTIONS
843
+ );
804
844
  }
805
845
 
806
846
  /**
@@ -851,14 +891,16 @@ export class Synthetizer
851
891
  * @param messageData {number[]|ArrayLike|Uint8Array} the message's data
852
892
  * (excluding the F0 byte, but including the F7 at the end).
853
893
  * @param channelOffset {number} channel offset for the system exclusive message, defaults to zero.
894
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
854
895
  */
855
- systemExclusive(messageData, channelOffset = 0)
896
+ systemExclusive(messageData, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
856
897
  {
857
- this.post({
858
- channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
859
- messageType: workletMessageType.systemExclusive,
860
- messageData: [Array.from(messageData), channelOffset]
861
- });
898
+ this._sendInternal(
899
+ [messageTypes.systemExclusive, ...Array.from(messageData)],
900
+ channelOffset,
901
+ false,
902
+ eventOptions
903
+ );
862
904
  }
863
905
 
864
906
  // noinspection JSUnusedGlobalSymbols
@@ -919,70 +961,6 @@ export class Synthetizer
919
961
  });
920
962
  }
921
963
 
922
- /**
923
- * sends a raw MIDI message to the synthesizer.
924
- * @param message {number[]|Uint8Array} the midi message, each number is a byte.
925
- * @param channelOffset {number} the channel offset of the message.
926
- */
927
- sendMessage(message, channelOffset = 0)
928
- {
929
- // discard as soon as possible if high perf
930
- const statusByteData = getEvent(message[0]);
931
-
932
- statusByteData.channel += channelOffset;
933
- // process the event
934
- switch (statusByteData.status)
935
- {
936
- case messageTypes.noteOn:
937
- const velocity = message[2];
938
- if (velocity > 0)
939
- {
940
- this.noteOn(statusByteData.channel, message[1], velocity);
941
- }
942
- else
943
- {
944
- this.noteOff(statusByteData.channel, message[1]);
945
- }
946
- break;
947
-
948
- case messageTypes.noteOff:
949
- this.noteOff(statusByteData.channel, message[1]);
950
- break;
951
-
952
- case messageTypes.pitchBend:
953
- this.pitchWheel(statusByteData.channel, message[2], message[1]);
954
- break;
955
-
956
- case messageTypes.controllerChange:
957
- this.controllerChange(statusByteData.channel, message[1], message[2]);
958
- break;
959
-
960
- case messageTypes.programChange:
961
- this.programChange(statusByteData.channel, message[1]);
962
- break;
963
-
964
- case messageTypes.polyPressure:
965
- this.polyPressure(statusByteData.channel, message[0], message[1]);
966
- break;
967
-
968
- case messageTypes.channelPressure:
969
- this.channelPressure(statusByteData.channel, message[1]);
970
- break;
971
-
972
- case messageTypes.systemExclusive:
973
- this.systemExclusive(new IndexedByteArray(message.slice(1)));
974
- break;
975
-
976
- case messageTypes.reset:
977
- this.stopAll(true);
978
- this.resetControllers();
979
- break;
980
-
981
- default:
982
- break;
983
- }
984
- }
985
-
986
964
  /**
987
965
  * Updates the reverb processor with a new impulse response.
988
966
  * @param buffer {AudioBuffer} the new reverb impulse response.