@marmooo/midy 0.2.9 → 0.3.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.
@@ -1,9 +1,7 @@
1
1
  export class MidyGMLite {
2
2
  static channelSettings: {
3
- currentBufferSource: null;
4
- isDrum: boolean;
5
3
  detune: number;
6
- program: number;
4
+ programNumber: number;
7
5
  bank: number;
8
6
  dataMSB: number;
9
7
  dataLSB: number;
@@ -13,6 +11,7 @@ export class MidyGMLite {
13
11
  };
14
12
  constructor(audioContext: any);
15
13
  mode: string;
14
+ numChannels: number;
16
15
  ticksPerBeat: number;
17
16
  totalTime: number;
18
17
  noteCheckInterval: number;
@@ -32,7 +31,8 @@ export class MidyGMLite {
32
31
  timeline: any[];
33
32
  instruments: any[];
34
33
  notePromises: any[];
35
- exclusiveClassMap: SparseMap;
34
+ exclusiveClassNotes: any[];
35
+ drumExclusiveClassNotes: any[];
36
36
  audioContext: any;
37
37
  masterVolume: any;
38
38
  scheduler: any;
@@ -75,8 +75,7 @@ export class MidyGMLite {
75
75
  };
76
76
  createChannels(audioContext: any): any[];
77
77
  createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
78
- calcLoopMode(channel: any, voiceParams: any): boolean;
79
- createBufferSource(channel: any, voiceParams: any, audioBuffer: any): any;
78
+ createBufferSource(voiceParams: any, audioBuffer: any): any;
80
79
  scheduleTimelineEvents(t: any, offset: any, queueIndex: any): Promise<any>;
81
80
  getQueueIndex(second: any): number;
82
81
  playNotes(): Promise<any>;
@@ -87,6 +86,7 @@ export class MidyGMLite {
87
86
  instruments: Set<any>;
88
87
  timeline: any[];
89
88
  };
89
+ stopActiveNotes(channelNumber: any, velocity: any, force: any, scheduleTime: any): Promise<any[]>;
90
90
  stopChannelNotes(channelNumber: any, velocity: any, force: any, scheduleTime: any): Promise<any[]>;
91
91
  stopNotes(velocity: any, force: any, scheduleTime: any): Promise<any[]>;
92
92
  start(): Promise<void>;
@@ -111,16 +111,20 @@ export class MidyGMLite {
111
111
  clampCutoffFrequency(frequency: any): number;
112
112
  setFilterEnvelope(note: any, scheduleTime: any): void;
113
113
  startModulation(channel: any, note: any, scheduleTime: any): void;
114
- getAudioBuffer(program: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
114
+ getAudioBuffer(programNumber: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
115
115
  createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, isSF3: any): Promise<Note>;
116
+ handleExclusiveClass(note: any, channelNumber: any, startTime: any): void;
117
+ handleDrumExclusiveClass(note: any, channelNumber: any, startTime: any): void;
116
118
  scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
117
119
  noteOn(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<void>;
118
- stopNote(endTime: any, stopTime: any, scheduledNotes: any, index: any): Promise<any>;
120
+ disconnectNote(note: any): void;
121
+ stopNote(endTime: any, stopTime: any, noteList: any, index: any): Promise<any>;
122
+ findNoteOffTarget(noteList: any): any[] | undefined;
119
123
  scheduleNoteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): Promise<any> | undefined;
120
124
  noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<any> | undefined;
121
125
  releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): (Promise<any> | undefined)[];
122
126
  handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<any>;
123
- handleProgramChange(channelNumber: any, program: any, _scheduleTime: any): void;
127
+ handleProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
124
128
  handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
125
129
  setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
126
130
  setModLfoToPitch(channel: any, note: any, scheduleTime: any): void;
@@ -177,6 +181,7 @@ export class MidyGMLite {
177
181
  handlePitchBendRangeRPN(channelNumber: any, scheduleTime: any): void;
178
182
  setPitchBendRange(channelNumber: any, value: any, scheduleTime: any): void;
179
183
  allSoundOff(channelNumber: any, _value: any, scheduleTime: any): Promise<any[]>;
184
+ resetAllStates(channelNumber: any): void;
180
185
  resetAllControllers(channelNumber: any): void;
181
186
  allNotesOff(channelNumber: any, _value: any, scheduleTime: any): Promise<any[]>;
182
187
  handleUniversalNonRealTimeExclusiveMessage(data: any, scheduleTime: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AAiJA;IAuBE;;;;;;;;;;;MAWE;IAEF,+BAcC;IAjDD,aAAa;IACb,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,kCAA+B;IAC/B,gCAA6B;IAC7B,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAClB,6BAAuC;IAgBrC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D;;;;;;;;;;;;;MAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCAWC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAeC;IAED,yCAWC;IAED,6DA2BC;IAED,sDAMC;IAED,0EASC;IAED,2EAsDC;IAED,mCAOC;IAED,0BAoDC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MA4EC;IAED,mGAgBC;IAED,wEAMC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,yDAQC;IAED,2DASC;IAED,qDAQC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,wCAIC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,yGAgBC;IAED,gHA4CC;IAED,kGAkDC;IAED,6FAQC;IAED,qFAwBC;IAED,yHAuBC;IAED,yGASC;IAED,4GAeC;IAED,mGA2BC;IAED,gFAGC;IAED,wFAGC;IAED,sEAWC;IAED,mEAQC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EA2CC;IAED;;;;;;;;;;;;;MAeC;IAED,kGAWC;IAED,wDAUC;IAED,iFAMC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAYC;IAED,kFAeC;IAED,uDAYC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAWC;IAED,gFAGC;IAED,8CAqBC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAED,6DAgBC;CACF;AAp5CD;IACE,uBAGC;IAFC,YAA2B;IAC3B,qBAAuB;IAGzB,gCAKC;IAED,mBAEC;IAED,0BAUC;IAED,uBAEC;IAED,mBAEC;IAED,cAMC;IASD,6BAKC;IAZD,qDAKC;CAQF;AAED;IASE,0FAMC;IAdD,kBAAa;IACb,gBAAW;IACX,iBAAY;IACZ,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
1
+ {"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AA+JA;IA2BE;;;;;;;;;MASE;IAEF,+BAcC;IAnDD,aAAa;IACb,oBAAiB;IACjB,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,kCAA+B;IAC/B,gCAA6B;IAC7B,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAClB,2BAAqC;IACrC,+BAEE;IAcA,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D;;;;;;;;;;;;;MAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCAWC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAeC;IAED,yCAaC;IAED,6DA2BC;IAED,4DASC;IAED,2EAsDC;IAED,mCAOC;IAED,0BAoDC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MA4EC;IAED,kGAiBC;IAED,mGAgBC;IAED,wEAMC;IAED,uBAKC;IAED,aAMC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,yDASC;IAED,2DASC;IAED,qDAQC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,wCAIC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,+GA0BC;IAED,gHAwCC;IAED,0EAiBC;IAED,8EAiBC;IAED,kGAoDC;IAED,6FAQC;IAED,gCASC;IAED,+EAiBC;IAED,oDAOC;IAED,yHAuBC;IAED,yGASC;IAED,4GAeC;IAED,mGA2BC;IAED,sFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAQC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EA2CC;IAED;;;;;;;;;;;;;MAeC;IAED,kGAWC;IAED,wDAUC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,uDAYC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,gFAGC;IAED,yCAUC;IAGD,8CAqBC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AA1/CD;IACE,uBAGC;IAFC,YAA2B;IAC3B,qBAAuB;IAGzB,gCAKC;IAED,mBAEC;IAED,0BAUC;IAED,uBAEC;IAED,mBAEC;IAED,cAMC;IASD,6BAKC;IAZD,qDAKC;CAQF;AAED;IASE,0FAMC;IAdD,kBAAa;IACb,gBAAW;IACX,iBAAY;IACZ,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
@@ -106,6 +106,19 @@ class Note {
106
106
  this.voiceParams = voiceParams;
107
107
  }
108
108
  }
109
+ const drumExclusiveClasses = new Uint8Array(128);
110
+ drumExclusiveClasses[42] = 1;
111
+ drumExclusiveClasses[44] = 1;
112
+ drumExclusiveClasses[46] = 1, // HH
113
+ drumExclusiveClasses[71] = 2;
114
+ drumExclusiveClasses[72] = 2; // Whistle
115
+ drumExclusiveClasses[73] = 3;
116
+ drumExclusiveClasses[74] = 3; // Guiro
117
+ drumExclusiveClasses[78] = 4;
118
+ drumExclusiveClasses[79] = 4; // Cuica
119
+ drumExclusiveClasses[80] = 5;
120
+ drumExclusiveClasses[81] = 5; // Triangle
121
+ const drumExclusiveClassCount = 5;
109
122
  // normalized to 0-1 for use with the SF2 modulator model
110
123
  const defaultControllerState = {
111
124
  noteOnVelocity: { type: 2, defaultValue: 0 },
@@ -179,6 +192,12 @@ class MidyGMLite {
179
192
  writable: true,
180
193
  value: "GM1"
181
194
  });
195
+ Object.defineProperty(this, "numChannels", {
196
+ enumerable: true,
197
+ configurable: true,
198
+ writable: true,
199
+ value: 16
200
+ });
182
201
  Object.defineProperty(this, "ticksPerBeat", {
183
202
  enumerable: true,
184
203
  configurable: true,
@@ -293,11 +312,17 @@ class MidyGMLite {
293
312
  writable: true,
294
313
  value: []
295
314
  });
296
- Object.defineProperty(this, "exclusiveClassMap", {
315
+ Object.defineProperty(this, "exclusiveClassNotes", {
316
+ enumerable: true,
317
+ configurable: true,
318
+ writable: true,
319
+ value: new Array(128)
320
+ });
321
+ Object.defineProperty(this, "drumExclusiveClassNotes", {
297
322
  enumerable: true,
298
323
  configurable: true,
299
324
  writable: true,
300
- value: new SparseMap(128)
325
+ value: new Array(this.numChannels * drumExclusiveClassCount)
301
326
  });
302
327
  this.audioContext = audioContext;
303
328
  this.masterVolume = new GainNode(audioContext);
@@ -364,8 +389,10 @@ class MidyGMLite {
364
389
  };
365
390
  }
366
391
  createChannels(audioContext) {
367
- const channels = Array.from({ length: 16 }, () => {
392
+ const channels = Array.from({ length: this.numChannels }, () => {
368
393
  return {
394
+ currentBufferSource: null,
395
+ isDrum: false,
369
396
  ...this.constructor.channelSettings,
370
397
  state: new ControllerState(),
371
398
  ...this.setChannelAudioNodes(audioContext),
@@ -404,18 +431,10 @@ class MidyGMLite {
404
431
  return audioBuffer;
405
432
  }
406
433
  }
407
- calcLoopMode(channel, voiceParams) {
408
- if (channel.isDrum) {
409
- return false;
410
- }
411
- else {
412
- return voiceParams.sampleModes % 2 !== 0;
413
- }
414
- }
415
- createBufferSource(channel, voiceParams, audioBuffer) {
434
+ createBufferSource(voiceParams, audioBuffer) {
416
435
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
417
436
  bufferSource.buffer = audioBuffer;
418
- bufferSource.loop = this.calcLoopMode(channel, voiceParams);
437
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
419
438
  if (bufferSource.loop) {
420
439
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
421
440
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -430,7 +449,7 @@ class MidyGMLite {
430
449
  const startTime = event.startTime + this.startDelay - offset;
431
450
  switch (event.type) {
432
451
  case "noteOn":
433
- if (event.velocity !== 0) {
452
+ if (0 < event.velocity) {
434
453
  await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
435
454
  break;
436
455
  }
@@ -478,7 +497,7 @@ class MidyGMLite {
478
497
  if (queueIndex >= this.timeline.length) {
479
498
  await Promise.all(this.notePromises);
480
499
  this.notePromises = [];
481
- this.exclusiveClassMap.clear();
500
+ this.exclusiveClassNotes.fill(undefined);
482
501
  this.audioBufferCache.clear();
483
502
  resolve();
484
503
  return;
@@ -497,7 +516,7 @@ class MidyGMLite {
497
516
  else if (this.isStopping) {
498
517
  await this.stopNotes(0, true, now);
499
518
  this.notePromises = [];
500
- this.exclusiveClassMap.clear();
519
+ this.exclusiveClassNotes.fill(undefined);
501
520
  this.audioBufferCache.clear();
502
521
  resolve();
503
522
  this.isStopping = false;
@@ -506,7 +525,7 @@ class MidyGMLite {
506
525
  }
507
526
  else if (this.isSeeking) {
508
527
  this.stopNotes(0, true, now);
509
- this.exclusiveClassMap.clear();
528
+ this.exclusiveClassNotes.fill(undefined);
510
529
  this.startTime = this.audioContext.currentTime;
511
530
  queueIndex = this.getQueueIndex(this.resumeTime);
512
531
  offset = this.resumeTime - this.startTime;
@@ -534,7 +553,7 @@ class MidyGMLite {
534
553
  extractMidiData(midi) {
535
554
  const instruments = new Set();
536
555
  const timeline = [];
537
- const tmpChannels = new Array(16);
556
+ const tmpChannels = new Array(this.channels.length);
538
557
  for (let i = 0; i < tmpChannels.length; i++) {
539
558
  tmpChannels[i] = {
540
559
  programNumber: -1,
@@ -597,6 +616,17 @@ class MidyGMLite {
597
616
  }
598
617
  return { instruments, timeline };
599
618
  }
619
+ stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
620
+ const channel = this.channels[channelNumber];
621
+ const promises = [];
622
+ const activeNotes = this.getActiveNotes(channel, scheduleTime);
623
+ activeNotes.forEach((note) => {
624
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
625
+ this.notePromises.push(promise);
626
+ promises.push(promise);
627
+ });
628
+ return Promise.all(promises);
629
+ }
600
630
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
601
631
  const channel = this.channels[channelNumber];
602
632
  const promises = [];
@@ -626,6 +656,9 @@ class MidyGMLite {
626
656
  if (!this.isPlaying)
627
657
  return;
628
658
  this.isStopping = true;
659
+ for (let i = 0; i < this.channels.length; i++) {
660
+ this.resetAllStates(i);
661
+ }
629
662
  }
630
663
  pause() {
631
664
  if (!this.isPlaying || this.isPaused)
@@ -665,6 +698,8 @@ class MidyGMLite {
665
698
  const note = noteList[i];
666
699
  if (!note)
667
700
  continue;
701
+ if (note.ending)
702
+ continue;
668
703
  callback(note);
669
704
  }
670
705
  });
@@ -801,8 +836,8 @@ class MidyGMLite {
801
836
  note.modulationLFO.connect(note.volumeDepth);
802
837
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
803
838
  }
804
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
805
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
839
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
840
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
806
841
  const cache = this.audioBufferCache.get(audioBufferId);
807
842
  if (cache) {
808
843
  cache.counter += 1;
@@ -825,8 +860,8 @@ class MidyGMLite {
825
860
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
826
861
  const voiceParams = voice.getAllParams(controllerState);
827
862
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
828
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
829
- note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
863
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
864
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
830
865
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
831
866
  note.filterNode = new BiquadFilterNode(this.audioContext, {
832
867
  type: "lowpass",
@@ -843,14 +878,43 @@ class MidyGMLite {
843
878
  note.bufferSource.start(startTime);
844
879
  return note;
845
880
  }
881
+ handleExclusiveClass(note, channelNumber, startTime) {
882
+ const exclusiveClass = note.voiceParams.exclusiveClass;
883
+ if (exclusiveClass === 0)
884
+ return;
885
+ const prev = this.exclusiveClassNotes[exclusiveClass];
886
+ if (prev) {
887
+ const [prevNote, prevChannelNumber] = prev;
888
+ if (prevNote && !prevNote.ending) {
889
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
890
+ startTime, true);
891
+ }
892
+ }
893
+ this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
894
+ }
895
+ handleDrumExclusiveClass(note, channelNumber, startTime) {
896
+ const channel = this.channels[channelNumber];
897
+ if (!channel.isDrum)
898
+ return;
899
+ const drumExclusiveClass = drumExclusiveClasses[noteNumber];
900
+ if (drumExclusiveClass === 0)
901
+ return;
902
+ const index = drumExclusiveClass * this.channels.length + channelNumber;
903
+ const prevNote = this.drumExclusiveClassNotes[index];
904
+ if (prevNote && !prevNote.ending) {
905
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
906
+ startTime, true);
907
+ }
908
+ this.drumExclusiveClassNotes[index] = note;
909
+ }
846
910
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
847
911
  const channel = this.channels[channelNumber];
848
912
  const bankNumber = channel.bank;
849
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
913
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
850
914
  if (soundFontIndex === undefined)
851
915
  return;
852
916
  const soundFont = this.soundFonts[soundFontIndex];
853
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
917
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
854
918
  if (!voice)
855
919
  return;
856
920
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -860,32 +924,47 @@ class MidyGMLite {
860
924
  if (0.5 <= channel.state.sustainPedal) {
861
925
  channel.sustainNotes.push(note);
862
926
  }
863
- const exclusiveClass = note.voiceParams.exclusiveClass;
864
- if (exclusiveClass !== 0) {
865
- if (this.exclusiveClassMap.has(exclusiveClass)) {
866
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
867
- const [prevNote, prevChannelNumber] = prevEntry;
868
- if (prevNote && !prevNote.ending) {
869
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
870
- startTime, true);
871
- }
872
- }
873
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
874
- }
927
+ this.handleExclusiveClass(note, channelNumber, startTime);
928
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
875
929
  const scheduledNotes = channel.scheduledNotes;
876
- if (scheduledNotes.has(noteNumber)) {
877
- scheduledNotes.get(noteNumber).push(note);
930
+ let noteList = scheduledNotes.get(noteNumber);
931
+ if (noteList) {
932
+ noteList.push(note);
878
933
  }
879
934
  else {
880
- scheduledNotes.set(noteNumber, [note]);
935
+ noteList = [note];
936
+ scheduledNotes.set(noteNumber, noteList);
937
+ }
938
+ if (channel.isDrum) {
939
+ const stopTime = startTime + note.bufferSource.buffer.duration;
940
+ const index = noteList.length - 1;
941
+ const promise = new Promise((resolve) => {
942
+ note.bufferSource.onended = () => {
943
+ noteList[index] = undefined;
944
+ this.disconnectNote(note);
945
+ resolve();
946
+ };
947
+ note.bufferSource.stop(stopTime);
948
+ });
949
+ this.notePromises.push(promise);
881
950
  }
882
951
  }
883
952
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
884
953
  scheduleTime ??= this.audioContext.currentTime;
885
954
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
886
955
  }
887
- stopNote(endTime, stopTime, scheduledNotes, index) {
888
- const note = scheduledNotes[index];
956
+ disconnectNote(note) {
957
+ note.bufferSource.disconnect();
958
+ note.filterNode.disconnect();
959
+ note.volumeEnvelopeNode.disconnect();
960
+ if (note.modulationDepth) {
961
+ note.volumeDepth.disconnect();
962
+ note.modulationDepth.disconnect();
963
+ note.modulationLFO.stop();
964
+ }
965
+ }
966
+ stopNote(endTime, stopTime, noteList, index) {
967
+ const note = noteList[index];
889
968
  note.volumeEnvelopeNode.gain
890
969
  .cancelScheduledValues(endTime)
891
970
  .linearRampToValueAtTime(0, stopTime);
@@ -895,41 +974,45 @@ class MidyGMLite {
895
974
  }, stopTime);
896
975
  return new Promise((resolve) => {
897
976
  note.bufferSource.onended = () => {
898
- scheduledNotes[index] = null;
899
- note.bufferSource.disconnect();
900
- note.filterNode.disconnect();
901
- note.volumeEnvelopeNode.disconnect();
902
- if (note.modulationDepth) {
903
- note.volumeDepth.disconnect();
904
- note.modulationDepth.disconnect();
905
- note.modulationLFO.stop();
906
- }
977
+ noteList[index] = undefined;
978
+ this.disconnectNote(note);
907
979
  resolve();
908
980
  };
909
981
  note.bufferSource.stop(stopTime);
910
982
  });
911
983
  }
984
+ findNoteOffTarget(noteList) {
985
+ for (let i = 0; i < noteList.length; i++) {
986
+ const note = noteList[i];
987
+ if (!note)
988
+ continue;
989
+ if (note.ending)
990
+ continue;
991
+ return [note, i];
992
+ }
993
+ }
912
994
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
913
995
  const channel = this.channels[channelNumber];
996
+ if (channel.isDrum)
997
+ return;
914
998
  if (!force && 0.5 <= channel.state.sustainPedal)
915
999
  return;
916
1000
  if (!channel.scheduledNotes.has(noteNumber))
917
1001
  return;
918
- const scheduledNotes = channel.scheduledNotes.get(noteNumber);
919
- for (let i = 0; i < scheduledNotes.length; i++) {
920
- const note = scheduledNotes[i];
921
- if (!note)
922
- continue;
923
- if (note.ending)
924
- continue;
925
- const volRelease = endTime + note.voiceParams.volRelease;
926
- const modRelease = endTime + note.voiceParams.modRelease;
927
- note.filterNode.frequency
928
- .cancelScheduledValues(endTime)
929
- .linearRampToValueAtTime(0, modRelease);
930
- const stopTime = Math.min(volRelease, modRelease);
931
- return this.stopNote(endTime, stopTime, scheduledNotes, i);
932
- }
1002
+ const noteList = channel.scheduledNotes.get(noteNumber);
1003
+ if (!noteList)
1004
+ return; // be careful with drum channel
1005
+ const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1006
+ if (!noteOffTarget)
1007
+ return;
1008
+ const [note, i] = noteOffTarget;
1009
+ const volRelease = endTime + note.voiceParams.volRelease;
1010
+ const modRelease = endTime + note.voiceParams.modRelease;
1011
+ note.filterNode.frequency
1012
+ .cancelScheduledValues(endTime)
1013
+ .linearRampToValueAtTime(0, modRelease);
1014
+ const stopTime = Math.min(volRelease, modRelease);
1015
+ return this.stopNote(endTime, stopTime, noteList, i);
933
1016
  }
934
1017
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
935
1018
  scheduleTime ??= this.audioContext.currentTime;
@@ -964,9 +1047,9 @@ class MidyGMLite {
964
1047
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
965
1048
  }
966
1049
  }
967
- handleProgramChange(channelNumber, program, _scheduleTime) {
1050
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
968
1051
  const channel = this.channels[channelNumber];
969
- channel.program = program;
1052
+ channel.programNumber = programNumber;
970
1053
  }
971
1054
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
972
1055
  const pitchBend = msb * 128 + lsb;
@@ -974,8 +1057,6 @@ class MidyGMLite {
974
1057
  }
975
1058
  setPitchBend(channelNumber, value, scheduleTime) {
976
1059
  const channel = this.channels[channelNumber];
977
- if (channel.isDrum)
978
- return;
979
1060
  scheduleTime ??= this.audioContext.currentTime;
980
1061
  const state = channel.state;
981
1062
  const prev = state.pitchWheel * 2 - 1;
@@ -1138,8 +1219,6 @@ class MidyGMLite {
1138
1219
  }
1139
1220
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1140
1221
  const channel = this.channels[channelNumber];
1141
- if (channel.isDrum)
1142
- return;
1143
1222
  scheduleTime ??= this.audioContext.currentTime;
1144
1223
  channel.state.modulationDepth = modulation / 127;
1145
1224
  this.updateModulation(channel, scheduleTime);
@@ -1186,8 +1265,6 @@ class MidyGMLite {
1186
1265
  }
1187
1266
  setSustainPedal(channelNumber, value, scheduleTime) {
1188
1267
  const channel = this.channels[channelNumber];
1189
- if (channel.isDrum)
1190
- return;
1191
1268
  scheduleTime ??= this.audioContext.currentTime;
1192
1269
  channel.state.sustainPedal = value / 127;
1193
1270
  if (64 <= value) {
@@ -1246,8 +1323,6 @@ class MidyGMLite {
1246
1323
  }
1247
1324
  setPitchBendRange(channelNumber, value, scheduleTime) {
1248
1325
  const channel = this.channels[channelNumber];
1249
- if (channel.isDrum)
1250
- return;
1251
1326
  scheduleTime ??= this.audioContext.currentTime;
1252
1327
  const state = channel.state;
1253
1328
  const prev = state.pitchWheelSensitivity;
@@ -1259,14 +1334,26 @@ class MidyGMLite {
1259
1334
  }
1260
1335
  allSoundOff(channelNumber, _value, scheduleTime) {
1261
1336
  scheduleTime ??= this.audioContext.currentTime;
1262
- return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
1337
+ return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
1338
+ }
1339
+ resetAllStates(channelNumber) {
1340
+ const channel = this.channels[channelNumber];
1341
+ const state = channel.state;
1342
+ for (const type of Object.keys(defaultControllerState)) {
1343
+ state[type] = defaultControllerState[type].defaultValue;
1344
+ }
1345
+ for (const type of Object.keys(this.constructor.channelSettings)) {
1346
+ channel[type] = this.constructor.channelSettings[type];
1347
+ }
1348
+ this.mode = "GM1";
1263
1349
  }
1350
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
1264
1351
  resetAllControllers(channelNumber) {
1265
1352
  const stateTypes = [
1353
+ "pitchWheel",
1266
1354
  "expression",
1267
1355
  "modulationDepth",
1268
1356
  "sustainPedal",
1269
- "pitchWheelSensitivity",
1270
1357
  ];
1271
1358
  const channel = this.channels[channelNumber];
1272
1359
  const state = channel.state;
@@ -1285,7 +1372,7 @@ class MidyGMLite {
1285
1372
  }
1286
1373
  allNotesOff(channelNumber, _value, scheduleTime) {
1287
1374
  scheduleTime ??= this.audioContext.currentTime;
1288
- return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
1375
+ return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
1289
1376
  }
1290
1377
  handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
1291
1378
  switch (data[2]) {
@@ -1355,6 +1442,7 @@ class MidyGMLite {
1355
1442
  console.warn(`Unsupported Exclusive Message: ${data}`);
1356
1443
  }
1357
1444
  }
1445
+ // https://github.com/marmooo/js-timer-benchmark
1358
1446
  scheduleTask(callback, scheduleTime) {
1359
1447
  return new Promise((resolve) => {
1360
1448
  const bufferSource = new AudioBufferSourceNode(this.audioContext, {
@@ -1380,10 +1468,8 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
1380
1468
  configurable: true,
1381
1469
  writable: true,
1382
1470
  value: {
1383
- currentBufferSource: null,
1384
- isDrum: false,
1385
1471
  detune: 0,
1386
- program: 0,
1472
+ programNumber: 0,
1387
1473
  bank: 0,
1388
1474
  dataMSB: 0,
1389
1475
  dataLSB: 0,