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