@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"}
@@ -103,6 +103,19 @@ class Note {
103
103
  this.voiceParams = voiceParams;
104
104
  }
105
105
  }
106
+ const drumExclusiveClasses = new Uint8Array(128);
107
+ drumExclusiveClasses[42] = 1;
108
+ drumExclusiveClasses[44] = 1;
109
+ drumExclusiveClasses[46] = 1, // HH
110
+ drumExclusiveClasses[71] = 2;
111
+ drumExclusiveClasses[72] = 2; // Whistle
112
+ drumExclusiveClasses[73] = 3;
113
+ drumExclusiveClasses[74] = 3; // Guiro
114
+ drumExclusiveClasses[78] = 4;
115
+ drumExclusiveClasses[79] = 4; // Cuica
116
+ drumExclusiveClasses[80] = 5;
117
+ drumExclusiveClasses[81] = 5; // Triangle
118
+ const drumExclusiveClassCount = 5;
106
119
  // normalized to 0-1 for use with the SF2 modulator model
107
120
  const defaultControllerState = {
108
121
  noteOnVelocity: { type: 2, defaultValue: 0 },
@@ -176,6 +189,12 @@ export class MidyGMLite {
176
189
  writable: true,
177
190
  value: "GM1"
178
191
  });
192
+ Object.defineProperty(this, "numChannels", {
193
+ enumerable: true,
194
+ configurable: true,
195
+ writable: true,
196
+ value: 16
197
+ });
179
198
  Object.defineProperty(this, "ticksPerBeat", {
180
199
  enumerable: true,
181
200
  configurable: true,
@@ -290,11 +309,17 @@ export class MidyGMLite {
290
309
  writable: true,
291
310
  value: []
292
311
  });
293
- Object.defineProperty(this, "exclusiveClassMap", {
312
+ Object.defineProperty(this, "exclusiveClassNotes", {
313
+ enumerable: true,
314
+ configurable: true,
315
+ writable: true,
316
+ value: new Array(128)
317
+ });
318
+ Object.defineProperty(this, "drumExclusiveClassNotes", {
294
319
  enumerable: true,
295
320
  configurable: true,
296
321
  writable: true,
297
- value: new SparseMap(128)
322
+ value: new Array(this.numChannels * drumExclusiveClassCount)
298
323
  });
299
324
  this.audioContext = audioContext;
300
325
  this.masterVolume = new GainNode(audioContext);
@@ -361,8 +386,10 @@ export class MidyGMLite {
361
386
  };
362
387
  }
363
388
  createChannels(audioContext) {
364
- const channels = Array.from({ length: 16 }, () => {
389
+ const channels = Array.from({ length: this.numChannels }, () => {
365
390
  return {
391
+ currentBufferSource: null,
392
+ isDrum: false,
366
393
  ...this.constructor.channelSettings,
367
394
  state: new ControllerState(),
368
395
  ...this.setChannelAudioNodes(audioContext),
@@ -401,18 +428,10 @@ export class MidyGMLite {
401
428
  return audioBuffer;
402
429
  }
403
430
  }
404
- calcLoopMode(channel, voiceParams) {
405
- if (channel.isDrum) {
406
- return false;
407
- }
408
- else {
409
- return voiceParams.sampleModes % 2 !== 0;
410
- }
411
- }
412
- createBufferSource(channel, voiceParams, audioBuffer) {
431
+ createBufferSource(voiceParams, audioBuffer) {
413
432
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
414
433
  bufferSource.buffer = audioBuffer;
415
- bufferSource.loop = this.calcLoopMode(channel, voiceParams);
434
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
416
435
  if (bufferSource.loop) {
417
436
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
418
437
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -427,7 +446,7 @@ export class MidyGMLite {
427
446
  const startTime = event.startTime + this.startDelay - offset;
428
447
  switch (event.type) {
429
448
  case "noteOn":
430
- if (event.velocity !== 0) {
449
+ if (0 < event.velocity) {
431
450
  await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
432
451
  break;
433
452
  }
@@ -475,7 +494,7 @@ export class MidyGMLite {
475
494
  if (queueIndex >= this.timeline.length) {
476
495
  await Promise.all(this.notePromises);
477
496
  this.notePromises = [];
478
- this.exclusiveClassMap.clear();
497
+ this.exclusiveClassNotes.fill(undefined);
479
498
  this.audioBufferCache.clear();
480
499
  resolve();
481
500
  return;
@@ -494,7 +513,7 @@ export class MidyGMLite {
494
513
  else if (this.isStopping) {
495
514
  await this.stopNotes(0, true, now);
496
515
  this.notePromises = [];
497
- this.exclusiveClassMap.clear();
516
+ this.exclusiveClassNotes.fill(undefined);
498
517
  this.audioBufferCache.clear();
499
518
  resolve();
500
519
  this.isStopping = false;
@@ -503,7 +522,7 @@ export class MidyGMLite {
503
522
  }
504
523
  else if (this.isSeeking) {
505
524
  this.stopNotes(0, true, now);
506
- this.exclusiveClassMap.clear();
525
+ this.exclusiveClassNotes.fill(undefined);
507
526
  this.startTime = this.audioContext.currentTime;
508
527
  queueIndex = this.getQueueIndex(this.resumeTime);
509
528
  offset = this.resumeTime - this.startTime;
@@ -531,7 +550,7 @@ export class MidyGMLite {
531
550
  extractMidiData(midi) {
532
551
  const instruments = new Set();
533
552
  const timeline = [];
534
- const tmpChannels = new Array(16);
553
+ const tmpChannels = new Array(this.channels.length);
535
554
  for (let i = 0; i < tmpChannels.length; i++) {
536
555
  tmpChannels[i] = {
537
556
  programNumber: -1,
@@ -594,6 +613,17 @@ export class MidyGMLite {
594
613
  }
595
614
  return { instruments, timeline };
596
615
  }
616
+ stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
617
+ const channel = this.channels[channelNumber];
618
+ const promises = [];
619
+ const activeNotes = this.getActiveNotes(channel, scheduleTime);
620
+ activeNotes.forEach((note) => {
621
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
622
+ this.notePromises.push(promise);
623
+ promises.push(promise);
624
+ });
625
+ return Promise.all(promises);
626
+ }
597
627
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
598
628
  const channel = this.channels[channelNumber];
599
629
  const promises = [];
@@ -623,6 +653,9 @@ export class MidyGMLite {
623
653
  if (!this.isPlaying)
624
654
  return;
625
655
  this.isStopping = true;
656
+ for (let i = 0; i < this.channels.length; i++) {
657
+ this.resetAllStates(i);
658
+ }
626
659
  }
627
660
  pause() {
628
661
  if (!this.isPlaying || this.isPaused)
@@ -662,6 +695,8 @@ export class MidyGMLite {
662
695
  const note = noteList[i];
663
696
  if (!note)
664
697
  continue;
698
+ if (note.ending)
699
+ continue;
665
700
  callback(note);
666
701
  }
667
702
  });
@@ -798,8 +833,8 @@ export class MidyGMLite {
798
833
  note.modulationLFO.connect(note.volumeDepth);
799
834
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
800
835
  }
801
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
802
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
836
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
837
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
803
838
  const cache = this.audioBufferCache.get(audioBufferId);
804
839
  if (cache) {
805
840
  cache.counter += 1;
@@ -822,8 +857,8 @@ export class MidyGMLite {
822
857
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
823
858
  const voiceParams = voice.getAllParams(controllerState);
824
859
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
825
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
826
- note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
860
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
861
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
827
862
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
828
863
  note.filterNode = new BiquadFilterNode(this.audioContext, {
829
864
  type: "lowpass",
@@ -840,14 +875,43 @@ export class MidyGMLite {
840
875
  note.bufferSource.start(startTime);
841
876
  return note;
842
877
  }
878
+ handleExclusiveClass(note, channelNumber, startTime) {
879
+ const exclusiveClass = note.voiceParams.exclusiveClass;
880
+ if (exclusiveClass === 0)
881
+ return;
882
+ const prev = this.exclusiveClassNotes[exclusiveClass];
883
+ if (prev) {
884
+ const [prevNote, prevChannelNumber] = prev;
885
+ if (prevNote && !prevNote.ending) {
886
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
887
+ startTime, true);
888
+ }
889
+ }
890
+ this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
891
+ }
892
+ handleDrumExclusiveClass(note, channelNumber, startTime) {
893
+ const channel = this.channels[channelNumber];
894
+ if (!channel.isDrum)
895
+ return;
896
+ const drumExclusiveClass = drumExclusiveClasses[noteNumber];
897
+ if (drumExclusiveClass === 0)
898
+ return;
899
+ const index = drumExclusiveClass * this.channels.length + channelNumber;
900
+ const prevNote = this.drumExclusiveClassNotes[index];
901
+ if (prevNote && !prevNote.ending) {
902
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
903
+ startTime, true);
904
+ }
905
+ this.drumExclusiveClassNotes[index] = note;
906
+ }
843
907
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
844
908
  const channel = this.channels[channelNumber];
845
909
  const bankNumber = channel.bank;
846
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
910
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
847
911
  if (soundFontIndex === undefined)
848
912
  return;
849
913
  const soundFont = this.soundFonts[soundFontIndex];
850
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
914
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
851
915
  if (!voice)
852
916
  return;
853
917
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -857,32 +921,47 @@ export class MidyGMLite {
857
921
  if (0.5 <= channel.state.sustainPedal) {
858
922
  channel.sustainNotes.push(note);
859
923
  }
860
- const exclusiveClass = note.voiceParams.exclusiveClass;
861
- if (exclusiveClass !== 0) {
862
- if (this.exclusiveClassMap.has(exclusiveClass)) {
863
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
864
- const [prevNote, prevChannelNumber] = prevEntry;
865
- if (prevNote && !prevNote.ending) {
866
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
867
- startTime, true);
868
- }
869
- }
870
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
871
- }
924
+ this.handleExclusiveClass(note, channelNumber, startTime);
925
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
872
926
  const scheduledNotes = channel.scheduledNotes;
873
- if (scheduledNotes.has(noteNumber)) {
874
- scheduledNotes.get(noteNumber).push(note);
927
+ let noteList = scheduledNotes.get(noteNumber);
928
+ if (noteList) {
929
+ noteList.push(note);
875
930
  }
876
931
  else {
877
- scheduledNotes.set(noteNumber, [note]);
932
+ noteList = [note];
933
+ scheduledNotes.set(noteNumber, noteList);
934
+ }
935
+ if (channel.isDrum) {
936
+ const stopTime = startTime + note.bufferSource.buffer.duration;
937
+ const index = noteList.length - 1;
938
+ const promise = new Promise((resolve) => {
939
+ note.bufferSource.onended = () => {
940
+ noteList[index] = undefined;
941
+ this.disconnectNote(note);
942
+ resolve();
943
+ };
944
+ note.bufferSource.stop(stopTime);
945
+ });
946
+ this.notePromises.push(promise);
878
947
  }
879
948
  }
880
949
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
881
950
  scheduleTime ??= this.audioContext.currentTime;
882
951
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
883
952
  }
884
- stopNote(endTime, stopTime, scheduledNotes, index) {
885
- const note = scheduledNotes[index];
953
+ disconnectNote(note) {
954
+ note.bufferSource.disconnect();
955
+ note.filterNode.disconnect();
956
+ note.volumeEnvelopeNode.disconnect();
957
+ if (note.modulationDepth) {
958
+ note.volumeDepth.disconnect();
959
+ note.modulationDepth.disconnect();
960
+ note.modulationLFO.stop();
961
+ }
962
+ }
963
+ stopNote(endTime, stopTime, noteList, index) {
964
+ const note = noteList[index];
886
965
  note.volumeEnvelopeNode.gain
887
966
  .cancelScheduledValues(endTime)
888
967
  .linearRampToValueAtTime(0, stopTime);
@@ -892,41 +971,45 @@ export class MidyGMLite {
892
971
  }, stopTime);
893
972
  return new Promise((resolve) => {
894
973
  note.bufferSource.onended = () => {
895
- scheduledNotes[index] = null;
896
- note.bufferSource.disconnect();
897
- note.filterNode.disconnect();
898
- note.volumeEnvelopeNode.disconnect();
899
- if (note.modulationDepth) {
900
- note.volumeDepth.disconnect();
901
- note.modulationDepth.disconnect();
902
- note.modulationLFO.stop();
903
- }
974
+ noteList[index] = undefined;
975
+ this.disconnectNote(note);
904
976
  resolve();
905
977
  };
906
978
  note.bufferSource.stop(stopTime);
907
979
  });
908
980
  }
981
+ findNoteOffTarget(noteList) {
982
+ for (let i = 0; i < noteList.length; i++) {
983
+ const note = noteList[i];
984
+ if (!note)
985
+ continue;
986
+ if (note.ending)
987
+ continue;
988
+ return [note, i];
989
+ }
990
+ }
909
991
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
910
992
  const channel = this.channels[channelNumber];
993
+ if (channel.isDrum)
994
+ return;
911
995
  if (!force && 0.5 <= channel.state.sustainPedal)
912
996
  return;
913
997
  if (!channel.scheduledNotes.has(noteNumber))
914
998
  return;
915
- const scheduledNotes = channel.scheduledNotes.get(noteNumber);
916
- for (let i = 0; i < scheduledNotes.length; i++) {
917
- const note = scheduledNotes[i];
918
- if (!note)
919
- continue;
920
- if (note.ending)
921
- continue;
922
- const volRelease = endTime + note.voiceParams.volRelease;
923
- const modRelease = endTime + note.voiceParams.modRelease;
924
- note.filterNode.frequency
925
- .cancelScheduledValues(endTime)
926
- .linearRampToValueAtTime(0, modRelease);
927
- const stopTime = Math.min(volRelease, modRelease);
928
- return this.stopNote(endTime, stopTime, scheduledNotes, i);
929
- }
999
+ const noteList = channel.scheduledNotes.get(noteNumber);
1000
+ if (!noteList)
1001
+ return; // be careful with drum channel
1002
+ const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1003
+ if (!noteOffTarget)
1004
+ return;
1005
+ const [note, i] = noteOffTarget;
1006
+ const volRelease = endTime + note.voiceParams.volRelease;
1007
+ const modRelease = endTime + note.voiceParams.modRelease;
1008
+ note.filterNode.frequency
1009
+ .cancelScheduledValues(endTime)
1010
+ .linearRampToValueAtTime(0, modRelease);
1011
+ const stopTime = Math.min(volRelease, modRelease);
1012
+ return this.stopNote(endTime, stopTime, noteList, i);
930
1013
  }
931
1014
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
932
1015
  scheduleTime ??= this.audioContext.currentTime;
@@ -961,9 +1044,9 @@ export class MidyGMLite {
961
1044
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
962
1045
  }
963
1046
  }
964
- handleProgramChange(channelNumber, program, _scheduleTime) {
1047
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
965
1048
  const channel = this.channels[channelNumber];
966
- channel.program = program;
1049
+ channel.programNumber = programNumber;
967
1050
  }
968
1051
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
969
1052
  const pitchBend = msb * 128 + lsb;
@@ -971,8 +1054,6 @@ export class MidyGMLite {
971
1054
  }
972
1055
  setPitchBend(channelNumber, value, scheduleTime) {
973
1056
  const channel = this.channels[channelNumber];
974
- if (channel.isDrum)
975
- return;
976
1057
  scheduleTime ??= this.audioContext.currentTime;
977
1058
  const state = channel.state;
978
1059
  const prev = state.pitchWheel * 2 - 1;
@@ -1135,8 +1216,6 @@ export class MidyGMLite {
1135
1216
  }
1136
1217
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1137
1218
  const channel = this.channels[channelNumber];
1138
- if (channel.isDrum)
1139
- return;
1140
1219
  scheduleTime ??= this.audioContext.currentTime;
1141
1220
  channel.state.modulationDepth = modulation / 127;
1142
1221
  this.updateModulation(channel, scheduleTime);
@@ -1183,8 +1262,6 @@ export class MidyGMLite {
1183
1262
  }
1184
1263
  setSustainPedal(channelNumber, value, scheduleTime) {
1185
1264
  const channel = this.channels[channelNumber];
1186
- if (channel.isDrum)
1187
- return;
1188
1265
  scheduleTime ??= this.audioContext.currentTime;
1189
1266
  channel.state.sustainPedal = value / 127;
1190
1267
  if (64 <= value) {
@@ -1243,8 +1320,6 @@ export class MidyGMLite {
1243
1320
  }
1244
1321
  setPitchBendRange(channelNumber, value, scheduleTime) {
1245
1322
  const channel = this.channels[channelNumber];
1246
- if (channel.isDrum)
1247
- return;
1248
1323
  scheduleTime ??= this.audioContext.currentTime;
1249
1324
  const state = channel.state;
1250
1325
  const prev = state.pitchWheelSensitivity;
@@ -1256,14 +1331,26 @@ export class MidyGMLite {
1256
1331
  }
1257
1332
  allSoundOff(channelNumber, _value, scheduleTime) {
1258
1333
  scheduleTime ??= this.audioContext.currentTime;
1259
- return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
1334
+ return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
1335
+ }
1336
+ resetAllStates(channelNumber) {
1337
+ const channel = this.channels[channelNumber];
1338
+ const state = channel.state;
1339
+ for (const type of Object.keys(defaultControllerState)) {
1340
+ state[type] = defaultControllerState[type].defaultValue;
1341
+ }
1342
+ for (const type of Object.keys(this.constructor.channelSettings)) {
1343
+ channel[type] = this.constructor.channelSettings[type];
1344
+ }
1345
+ this.mode = "GM1";
1260
1346
  }
1347
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
1261
1348
  resetAllControllers(channelNumber) {
1262
1349
  const stateTypes = [
1350
+ "pitchWheel",
1263
1351
  "expression",
1264
1352
  "modulationDepth",
1265
1353
  "sustainPedal",
1266
- "pitchWheelSensitivity",
1267
1354
  ];
1268
1355
  const channel = this.channels[channelNumber];
1269
1356
  const state = channel.state;
@@ -1282,7 +1369,7 @@ export class MidyGMLite {
1282
1369
  }
1283
1370
  allNotesOff(channelNumber, _value, scheduleTime) {
1284
1371
  scheduleTime ??= this.audioContext.currentTime;
1285
- return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
1372
+ return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
1286
1373
  }
1287
1374
  handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
1288
1375
  switch (data[2]) {
@@ -1352,6 +1439,7 @@ export class MidyGMLite {
1352
1439
  console.warn(`Unsupported Exclusive Message: ${data}`);
1353
1440
  }
1354
1441
  }
1442
+ // https://github.com/marmooo/js-timer-benchmark
1355
1443
  scheduleTask(callback, scheduleTime) {
1356
1444
  return new Promise((resolve) => {
1357
1445
  const bufferSource = new AudioBufferSourceNode(this.audioContext, {
@@ -1376,10 +1464,8 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
1376
1464
  configurable: true,
1377
1465
  writable: true,
1378
1466
  value: {
1379
- currentBufferSource: null,
1380
- isDrum: false,
1381
1467
  detune: 0,
1382
- program: 0,
1468
+ programNumber: 0,
1383
1469
  bank: 0,
1384
1470
  dataMSB: 0,
1385
1471
  dataLSB: 0,