@marmooo/midy 0.2.4 → 0.2.6

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.
@@ -3,6 +3,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGM1 = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
+ // 2-3 times faster than Map
7
+ class SparseMap {
8
+ constructor(size) {
9
+ this.data = new Array(size);
10
+ this.activeIndices = [];
11
+ }
12
+ set(key, value) {
13
+ if (this.data[key] === undefined) {
14
+ this.activeIndices.push(key);
15
+ }
16
+ this.data[key] = value;
17
+ }
18
+ get(key) {
19
+ return this.data[key];
20
+ }
21
+ delete(key) {
22
+ if (this.data[key] !== undefined) {
23
+ this.data[key] = undefined;
24
+ const index = this.activeIndices.indexOf(key);
25
+ if (index !== -1) {
26
+ this.activeIndices.splice(index, 1);
27
+ }
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ has(key) {
33
+ return this.data[key] !== undefined;
34
+ }
35
+ get size() {
36
+ return this.activeIndices.length;
37
+ }
38
+ clear() {
39
+ for (let i = 0; i < this.activeIndices.length; i++) {
40
+ const key = this.activeIndices[i];
41
+ this.data[key] = undefined;
42
+ }
43
+ this.activeIndices = [];
44
+ }
45
+ *[Symbol.iterator]() {
46
+ for (let i = 0; i < this.activeIndices.length; i++) {
47
+ const key = this.activeIndices[i];
48
+ yield [key, this.data[key]];
49
+ }
50
+ }
51
+ forEach(callback) {
52
+ for (let i = 0; i < this.activeIndices.length; i++) {
53
+ const key = this.activeIndices[i];
54
+ callback(this.data[key], key, this);
55
+ }
56
+ }
57
+ }
6
58
  class Note {
7
59
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
60
  Object.defineProperty(this, "bufferSource", {
@@ -17,37 +69,31 @@ class Note {
17
69
  writable: true,
18
70
  value: void 0
19
71
  });
20
- Object.defineProperty(this, "volumeEnvelopeNode", {
21
- enumerable: true,
22
- configurable: true,
23
- writable: true,
24
- value: void 0
25
- });
26
- Object.defineProperty(this, "volumeDepth", {
72
+ Object.defineProperty(this, "filterDepth", {
27
73
  enumerable: true,
28
74
  configurable: true,
29
75
  writable: true,
30
76
  value: void 0
31
77
  });
32
- Object.defineProperty(this, "modulationLFO", {
78
+ Object.defineProperty(this, "volumeEnvelopeNode", {
33
79
  enumerable: true,
34
80
  configurable: true,
35
81
  writable: true,
36
82
  value: void 0
37
83
  });
38
- Object.defineProperty(this, "modulationDepth", {
84
+ Object.defineProperty(this, "volumeDepth", {
39
85
  enumerable: true,
40
86
  configurable: true,
41
87
  writable: true,
42
88
  value: void 0
43
89
  });
44
- Object.defineProperty(this, "vibratoLFO", {
90
+ Object.defineProperty(this, "modulationLFO", {
45
91
  enumerable: true,
46
92
  configurable: true,
47
93
  writable: true,
48
94
  value: void 0
49
95
  });
50
- Object.defineProperty(this, "vibratoDepth", {
96
+ Object.defineProperty(this, "modulationDepth", {
51
97
  enumerable: true,
52
98
  configurable: true,
53
99
  writable: true,
@@ -181,6 +227,18 @@ class MidyGM1 {
181
227
  writable: true,
182
228
  value: this.initSoundFontTable()
183
229
  });
230
+ Object.defineProperty(this, "audioBufferCounter", {
231
+ enumerable: true,
232
+ configurable: true,
233
+ writable: true,
234
+ value: new Map()
235
+ });
236
+ Object.defineProperty(this, "audioBufferCache", {
237
+ enumerable: true,
238
+ configurable: true,
239
+ writable: true,
240
+ value: new Map()
241
+ });
184
242
  Object.defineProperty(this, "isPlaying", {
185
243
  enumerable: true,
186
244
  configurable: true,
@@ -233,7 +291,7 @@ class MidyGM1 {
233
291
  enumerable: true,
234
292
  configurable: true,
235
293
  writable: true,
236
- value: new Map()
294
+ value: new SparseMap(128)
237
295
  });
238
296
  this.audioContext = audioContext;
239
297
  this.masterVolume = new GainNode(audioContext);
@@ -246,7 +304,7 @@ class MidyGM1 {
246
304
  initSoundFontTable() {
247
305
  const table = new Array(128);
248
306
  for (let i = 0; i < 128; i++) {
249
- table[i] = new Map();
307
+ table[i] = new SparseMap(128);
250
308
  }
251
309
  return table;
252
310
  }
@@ -299,7 +357,7 @@ class MidyGM1 {
299
357
  ...this.constructor.channelSettings,
300
358
  state: new ControllerState(),
301
359
  ...this.setChannelAudioNodes(audioContext),
302
- scheduledNotes: new Map(),
360
+ scheduledNotes: new SparseMap(128),
303
361
  };
304
362
  });
305
363
  return channels;
@@ -333,9 +391,8 @@ class MidyGM1 {
333
391
  return audioBuffer;
334
392
  }
335
393
  }
336
- async createNoteBufferNode(voiceParams, isSF3) {
394
+ createNoteBufferNode(audioBuffer, voiceParams) {
337
395
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
338
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
339
396
  bufferSource.buffer = audioBuffer;
340
397
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
341
398
  if (bufferSource.loop) {
@@ -349,31 +406,32 @@ class MidyGM1 {
349
406
  const event = this.timeline[queueIndex];
350
407
  if (event.startTime > t + this.lookAhead)
351
408
  break;
409
+ const startTime = event.startTime + this.startDelay - offset;
352
410
  switch (event.type) {
353
411
  case "noteOn":
354
412
  if (event.velocity !== 0) {
355
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
413
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
356
414
  break;
357
415
  }
358
416
  /* falls through */
359
417
  case "noteOff": {
360
- const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
418
+ const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, startTime);
361
419
  if (notePromise) {
362
420
  this.notePromises.push(notePromise);
363
421
  }
364
422
  break;
365
423
  }
366
424
  case "controller":
367
- this.handleControlChange(event.channel, event.controllerType, event.value);
425
+ this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
368
426
  break;
369
427
  case "programChange":
370
- this.handleProgramChange(event.channel, event.programNumber);
428
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
371
429
  break;
372
430
  case "pitchBend":
373
- this.setPitchBend(event.channel, event.value + 8192);
431
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
374
432
  break;
375
433
  case "sysEx":
376
- this.handleSysEx(event.data);
434
+ this.handleSysEx(event.data, startTime);
377
435
  }
378
436
  queueIndex++;
379
437
  }
@@ -400,6 +458,7 @@ class MidyGM1 {
400
458
  await Promise.all(this.notePromises);
401
459
  this.notePromises = [];
402
460
  this.exclusiveClassMap.clear();
461
+ this.audioBufferCache.clear();
403
462
  resolve();
404
463
  return;
405
464
  }
@@ -415,8 +474,9 @@ class MidyGM1 {
415
474
  }
416
475
  else if (this.isStopping) {
417
476
  await this.stopNotes(0, true);
418
- this.exclusiveClassMap.clear();
419
477
  this.notePromises = [];
478
+ this.exclusiveClassMap.clear();
479
+ this.audioBufferCache.clear();
420
480
  resolve();
421
481
  this.isStopping = false;
422
482
  this.isPaused = false;
@@ -447,6 +507,9 @@ class MidyGM1 {
447
507
  secondToTicks(second, secondsPerBeat) {
448
508
  return second * this.ticksPerBeat / secondsPerBeat;
449
509
  }
510
+ getAudioBufferId(programNumber, noteNumber, velocity) {
511
+ return `${programNumber}:${noteNumber}:${velocity}`;
512
+ }
450
513
  extractMidiData(midi) {
451
514
  const instruments = new Set();
452
515
  const timeline = [];
@@ -467,6 +530,8 @@ class MidyGM1 {
467
530
  switch (event.type) {
468
531
  case "noteOn": {
469
532
  const channel = tmpChannels[event.channel];
533
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
534
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
470
535
  if (channel.programNumber < 0) {
471
536
  instruments.add(`${channel.bank}:0`);
472
537
  channel.programNumber = 0;
@@ -483,6 +548,10 @@ class MidyGM1 {
483
548
  timeline.push(event);
484
549
  }
485
550
  }
551
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
552
+ if (count === 1)
553
+ this.audioBufferCounter.delete(audioBufferId);
554
+ }
486
555
  const priority = {
487
556
  controller: 0,
488
557
  sysEx: 1,
@@ -572,8 +641,20 @@ class MidyGM1 {
572
641
  const now = this.audioContext.currentTime;
573
642
  return this.resumeTime + now - this.startTime - this.startDelay;
574
643
  }
644
+ processScheduledNotes(channel, scheduleTime, callback) {
645
+ channel.scheduledNotes.forEach((noteList) => {
646
+ for (let i = 0; i < noteList.length; i++) {
647
+ const note = noteList[i];
648
+ if (!note)
649
+ continue;
650
+ if (scheduleTime < note.startTime)
651
+ continue;
652
+ callback(note);
653
+ }
654
+ });
655
+ }
575
656
  getActiveNotes(channel, time) {
576
- const activeNotes = new Map();
657
+ const activeNotes = new SparseMap(128);
577
658
  channel.scheduledNotes.forEach((noteList) => {
578
659
  const activeNote = this.getActiveNote(noteList, time);
579
660
  if (activeNote) {
@@ -645,20 +726,20 @@ class MidyGM1 {
645
726
  .setValueAtTime(attackVolume, volHold)
646
727
  .linearRampToValueAtTime(sustainVolume, volDecay);
647
728
  }
648
- setPitchEnvelope(note) {
649
- const now = this.audioContext.currentTime;
729
+ setPitchEnvelope(note, scheduleTime) {
730
+ scheduleTime ??= this.audioContext.currentTime;
650
731
  const { voiceParams } = note;
651
732
  const baseRate = voiceParams.playbackRate;
652
733
  note.bufferSource.playbackRate
653
- .cancelScheduledValues(now)
654
- .setValueAtTime(baseRate, now);
734
+ .cancelScheduledValues(scheduleTime)
735
+ .setValueAtTime(baseRate, scheduleTime);
655
736
  const modEnvToPitch = voiceParams.modEnvToPitch;
656
737
  if (modEnvToPitch === 0)
657
738
  return;
658
739
  const basePitch = this.rateToCent(baseRate);
659
740
  const peekPitch = basePitch + modEnvToPitch;
660
741
  const peekRate = this.centToRate(peekPitch);
661
- const modDelay = startTime + voiceParams.modDelay;
742
+ const modDelay = note.startTime + voiceParams.modDelay;
662
743
  const modAttack = modDelay + voiceParams.modAttack;
663
744
  const modHold = modAttack + voiceParams.modHold;
664
745
  const modDecay = modHold + voiceParams.modDecay;
@@ -715,11 +796,31 @@ class MidyGM1 {
715
796
  note.modulationLFO.connect(note.volumeDepth);
716
797
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
717
798
  }
799
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
800
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
801
+ const cache = this.audioBufferCache.get(audioBufferId);
802
+ if (cache) {
803
+ cache.counter += 1;
804
+ if (cache.maxCount <= cache.counter) {
805
+ this.audioBufferCache.delete(audioBufferId);
806
+ }
807
+ return cache.audioBuffer;
808
+ }
809
+ else {
810
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
811
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
812
+ const cache = { audioBuffer, maxCount, counter: 1 };
813
+ this.audioBufferCache.set(audioBufferId, cache);
814
+ return audioBuffer;
815
+ }
816
+ }
718
817
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
719
818
  const state = channel.state;
720
- const voiceParams = voice.getAllParams(state.array);
819
+ const controllerState = this.getControllerState(channel, noteNumber, velocity);
820
+ const voiceParams = voice.getAllParams(controllerState);
721
821
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
722
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
822
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
823
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
723
824
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
724
825
  note.filterNode = new BiquadFilterNode(this.audioContext, {
725
826
  type: "lowpass",
@@ -743,10 +844,10 @@ class MidyGM1 {
743
844
  if (soundFontIndex === undefined)
744
845
  return;
745
846
  const soundFont = this.soundFonts[soundFontIndex];
746
- const isSF3 = soundFont.parsed.info.version.major === 3;
747
847
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
748
848
  if (!voice)
749
849
  return;
850
+ const isSF3 = soundFont.parsed.info.version.major === 3;
750
851
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
751
852
  note.volumeEnvelopeNode.connect(channel.gainL);
752
853
  note.volumeEnvelopeNode.connect(channel.gainR);
@@ -795,10 +896,6 @@ class MidyGM1 {
795
896
  note.modulationDepth.disconnect();
796
897
  note.modulationLFO.stop();
797
898
  }
798
- if (note.vibratoDepth) {
799
- note.vibratoDepth.disconnect();
800
- note.vibratoLFO.stop();
801
- }
802
899
  resolve();
803
900
  };
804
901
  note.bufferSource.stop(stopTime);
@@ -893,16 +990,6 @@ class MidyGM1 {
893
990
  .cancelScheduledValues(now)
894
991
  .setValueAtTime(modulationDepth, now);
895
992
  }
896
- setVibLfoToPitch(channel, note) {
897
- const now = this.audioContext.currentTime;
898
- const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
899
- const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
900
- 2;
901
- const vibratoDepthSign = 0 < vibLfoToPitch;
902
- note.vibratoDepth.gain
903
- .cancelScheduledValues(now)
904
- .setValueAtTime(vibratoDepth * vibratoDepthSign, now);
905
- }
906
993
  setModLfoToFilterFc(note) {
907
994
  const now = this.audioContext.currentTime;
908
995
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
@@ -942,11 +1029,7 @@ class MidyGM1 {
942
1029
  this.setModLfoToPitch(channel, note);
943
1030
  }
944
1031
  },
945
- vibLfoToPitch: (channel, note, _prevValue) => {
946
- if (0 < channel.state.vibratoDepth) {
947
- this.setVibLfoToPitch(channel, note);
948
- }
949
- },
1032
+ vibLfoToPitch: (_channel, _note, _prevValue) => { },
950
1033
  modLfoToFilterFc: (channel, note, _prevValue) => {
951
1034
  if (0 < channel.state.modulationDepth)
952
1035
  this.setModLfoToFilterFc(note);
@@ -959,28 +1042,8 @@ class MidyGM1 {
959
1042
  reverbEffectsSend: (_channel, _note, _prevValue) => { },
960
1043
  delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
961
1044
  freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
962
- delayVibLFO: (channel, note, prevValue) => {
963
- if (0 < channel.state.vibratoDepth) {
964
- const now = this.audioContext.currentTime;
965
- const vibratoDelay = channel.state.vibratoDelay * 2;
966
- const prevStartTime = note.startTime + prevValue * vibratoDelay;
967
- if (now < prevStartTime)
968
- return;
969
- const value = note.voiceParams.delayVibLFO;
970
- const startTime = note.startTime + value * vibratoDelay;
971
- note.vibratoLFO.stop(now);
972
- note.vibratoLFO.start(startTime);
973
- }
974
- },
975
- freqVibLFO: (channel, note, _prevValue) => {
976
- if (0 < channel.state.vibratoDepth) {
977
- const now = this.audioContext.currentTime;
978
- const freqVibLFO = note.voiceParams.freqVibLFO;
979
- note.vibratoLFO.frequency
980
- .cancelScheduledValues(now)
981
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, now);
982
- }
983
- },
1045
+ delayVibLFO: (_channel, _note, _prevValue) => { },
1046
+ freqVibLFO: (_channel, _note, _prevValue) => { },
984
1047
  };
985
1048
  }
986
1049
  getControllerState(channel, noteNumber, velocity) {
@@ -1053,10 +1116,10 @@ class MidyGM1 {
1053
1116
  123: this.allNotesOff,
1054
1117
  };
1055
1118
  }
1056
- handleControlChange(channelNumber, controllerType, value) {
1119
+ handleControlChange(channelNumber, controllerType, value, startTime) {
1057
1120
  const handler = this.controlChangeHandlers[controllerType];
1058
1121
  if (handler) {
1059
- handler.call(this, channelNumber, value);
1122
+ handler.call(this, channelNumber, value, startTime);
1060
1123
  const channel = this.channels[channelNumber];
1061
1124
  this.applyVoiceParams(channel, controllerType + 128);
1062
1125
  }
@@ -1064,33 +1127,28 @@ class MidyGM1 {
1064
1127
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
1065
1128
  }
1066
1129
  }
1067
- updateModulation(channel) {
1068
- const now = this.audioContext.currentTime;
1130
+ updateModulation(channel, scheduleTime) {
1131
+ scheduleTime ??= this.audioContext.currentTime;
1069
1132
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1070
- channel.scheduledNotes.forEach((noteList) => {
1071
- for (let i = 0; i < noteList.length; i++) {
1072
- const note = noteList[i];
1073
- if (!note)
1074
- continue;
1075
- if (note.modulationDepth) {
1076
- note.modulationDepth.gain.setValueAtTime(depth, now);
1077
- }
1078
- else {
1079
- this.setPitchEnvelope(note);
1080
- this.startModulation(channel, note, now);
1081
- }
1133
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1134
+ if (note.modulationDepth) {
1135
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1136
+ }
1137
+ else {
1138
+ this.setPitchEnvelope(note, scheduleTime);
1139
+ this.startModulation(channel, note, scheduleTime);
1082
1140
  }
1083
1141
  });
1084
1142
  }
1085
- setModulationDepth(channelNumber, modulation) {
1143
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1086
1144
  const channel = this.channels[channelNumber];
1087
1145
  channel.state.modulationDepth = modulation / 127;
1088
- this.updateModulation(channel);
1146
+ this.updateModulation(channel, scheduleTime);
1089
1147
  }
1090
- setVolume(channelNumber, volume) {
1148
+ setVolume(channelNumber, volume, scheduleTime) {
1091
1149
  const channel = this.channels[channelNumber];
1092
1150
  channel.state.volume = volume / 127;
1093
- this.updateChannelVolume(channel);
1151
+ this.updateChannelVolume(channel, scheduleTime);
1094
1152
  }
1095
1153
  panToGain(pan) {
1096
1154
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1099,31 +1157,31 @@ class MidyGM1 {
1099
1157
  gainRight: Math.sin(theta),
1100
1158
  };
1101
1159
  }
1102
- setPan(channelNumber, pan) {
1160
+ setPan(channelNumber, pan, scheduleTime) {
1103
1161
  const channel = this.channels[channelNumber];
1104
1162
  channel.state.pan = pan / 127;
1105
- this.updateChannelVolume(channel);
1163
+ this.updateChannelVolume(channel, scheduleTime);
1106
1164
  }
1107
- setExpression(channelNumber, expression) {
1165
+ setExpression(channelNumber, expression, scheduleTime) {
1108
1166
  const channel = this.channels[channelNumber];
1109
1167
  channel.state.expression = expression / 127;
1110
- this.updateChannelVolume(channel);
1168
+ this.updateChannelVolume(channel, scheduleTime);
1111
1169
  }
1112
1170
  dataEntryLSB(channelNumber, value) {
1113
1171
  this.channels[channelNumber].dataLSB = value;
1114
1172
  this.handleRPN(channelNumber, 0);
1115
1173
  }
1116
- updateChannelVolume(channel) {
1117
- const now = this.audioContext.currentTime;
1174
+ updateChannelVolume(channel, scheduleTime) {
1175
+ scheduleTime ??= this.audioContext.currentTime;
1118
1176
  const state = channel.state;
1119
1177
  const volume = state.volume * state.expression;
1120
1178
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1121
1179
  channel.gainL.gain
1122
1180
  .cancelScheduledValues(now)
1123
- .setValueAtTime(volume * gainLeft, now);
1181
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1124
1182
  channel.gainR.gain
1125
1183
  .cancelScheduledValues(now)
1126
- .setValueAtTime(volume * gainRight, now);
1184
+ .setValueAtTime(volume * gainRight, scheduleTime);
1127
1185
  }
1128
1186
  setSustainPedal(channelNumber, value) {
1129
1187
  this.channels[channelNumber].state.sustainPedal = value / 127;