@marmooo/midy 0.3.2 → 0.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marmooo/midy",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "A MIDI player/synthesizer written in JavaScript that supports GM-Lite/GM1 and SF2/SF3.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -54,9 +54,9 @@ export class MidyGM1 {
54
54
  channels: any[];
55
55
  initSoundFontTable(): any[];
56
56
  addSoundFont(soundFont: any): void;
57
- loadSoundFont(soundFontUrl: any): Promise<void>;
58
- loadMIDI(midiUrl: any): Promise<void>;
59
- setChannelAudioNodes(audioContext: any): {
57
+ loadSoundFont(input: any): Promise<void>;
58
+ loadMIDI(input: any): Promise<void>;
59
+ createChannelAudioNodes(audioContext: any): {
60
60
  gainL: any;
61
61
  gainR: any;
62
62
  merger: any;
@@ -84,7 +84,7 @@ export class MidyGM1 {
84
84
  seekTo(second: any): void;
85
85
  calcTotalTime(): number;
86
86
  currentTime(): number;
87
- processScheduledNotes(channel: any, callback: any): void;
87
+ processScheduledNotes(channel: any, scheduleTime: any, callback: any): void;
88
88
  processActiveNotes(channel: any, scheduleTime: any, callback: any): void;
89
89
  cbToRatio(cb: any): number;
90
90
  rateToCent(rate: any): number;
@@ -101,15 +101,15 @@ export class MidyGM1 {
101
101
  getAudioBuffer(programNumber: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
102
102
  createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, isSF3: any): Promise<Note>;
103
103
  handleExclusiveClass(note: any, channelNumber: any, startTime: any): void;
104
- scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any, noteOffEvent: any): Promise<void>;
104
+ scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
105
105
  noteOn(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<void>;
106
106
  disconnectNote(note: any): void;
107
- stopNote(channel: any, note: any, endTime: any, stopTime: any): Promise<any>;
108
- scheduleNoteOff(channelNumber: any, note: any, _velocity: any, endTime: any, force: any): Promise<any> | undefined;
107
+ releaseNote(channel: any, note: any, endTime: any): Promise<any>;
108
+ scheduleNoteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): void;
109
109
  findNoteOffTarget(channel: any, noteNumber: any): any;
110
- noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<any> | undefined;
111
- releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): (Promise<any> | undefined)[];
112
- handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<any>;
110
+ noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): void;
111
+ releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): void[];
112
+ handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
113
113
  handleProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
114
114
  handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
115
115
  setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
@@ -173,6 +173,7 @@ export class MidyGM1 {
173
173
  declare class Note {
174
174
  constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
175
175
  index: number;
176
+ ending: boolean;
176
177
  bufferSource: any;
177
178
  filterNode: any;
178
179
  filterDepth: any;
@@ -1 +1 @@
1
- {"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AAqFA;IAwBE;;;;;;;;;;;MAWE;IAEF,+BAcC;IAlDD,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;IAgBnC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCAWC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAeC;IAED,yCAaC;IAED,6DA2BC;IAED,4DASC;IAED,+EA6CC;IAED,mCAOC;IAED,0BA8DC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MAoGC;IAED,kGAgBC;IAED,mGAgBC;IAED,wEAMC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,yDAQC;IAED,yEAWC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,qCAMC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,+GA0BC;IAED,gHAyCC;IAED,0EAiBC;IAED,qHAoDC;IAED,6FASC;IAED,gCASC;IAED,6EAgBC;IAED,mHAgBC;IAED,sDASC;IAED,yGAWC;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,qCAcC;IAED,kGAWC;IAED,wDAUC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,iEAKC;IAED,uEAQC;IAED,mEAKC;IAED,yEAQC;IAED,gFAGC;IAED,yCAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AA9+CD;IAUE,0FAMC;IAfD,cAAW;IACX,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-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AA4FA;IAwBE;;;;;;;;;;;MAWE;IAEF,+BAcC;IAlDD,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;IAgBnC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCAWC;IAED,yCAcC;IAED,oCAiBC;IAED;;;;MAeC;IAED,yCAaC;IAED,6DA2BC;IAED,4DASC;IAED,+EAkDC;IAED,mCAOC;IAED,0BA8DC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MA6EC;IAED,kGAeC;IAED,mGAgBC;IAED,wEAMC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,4EASC;IAED,yEASC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,qCAMC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,+GA0BC;IAED,gHAyCC;IAED,0EAiBC;IAED,kGAsCC;IAED,6FASC;IAED,gCASC;IAED,iEAoBC;IAED,qGAaC;IAED,sDASC;IAED,qFASC;IAED,sFAeC;IAED,oGA2BC;IAED,sFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAQC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAgCC;IAED,qCAcC;IAED,kGAWC;IAED,wDAUC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,iEAKC;IAED,uEAQC;IAED,mEAKC;IAED,yEAQC;IAED,gFAGC;IAED,yCAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AAv9CD;IAWE,0FAMC;IAhBD,cAAW;IACX,gBAAe;IACf,kBAAa;IACb,gBAAW;IACX,iBAAY;IACZ,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
@@ -11,6 +11,12 @@ class Note {
11
11
  writable: true,
12
12
  value: -1
13
13
  });
14
+ Object.defineProperty(this, "ending", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: false
19
+ });
14
20
  Object.defineProperty(this, "bufferSource", {
15
21
  enumerable: true,
16
22
  configurable: true,
@@ -67,13 +73,11 @@ const defaultControllerState = {
67
73
  pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
68
74
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
69
75
  link: { type: 127, defaultValue: 0 },
70
- // bankMSB: { type: 128 + 0, defaultValue: 121, },
71
76
  modulationDepth: { type: 128 + 1, defaultValue: 0 },
72
77
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
73
78
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
74
79
  pan: { type: 128 + 10, defaultValue: 64 / 127 },
75
80
  expression: { type: 128 + 11, defaultValue: 1 },
76
- // bankLSB: { type: 128 + 32, defaultValue: 0, },
77
81
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
78
82
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
79
83
  // rpnLSB: { type: 128 + 100, defaultValue: 127 },
@@ -102,6 +106,16 @@ class ControllerState {
102
106
  }
103
107
  }
104
108
  }
109
+ const volumeEnvelopeKeys = [
110
+ "volDelay",
111
+ "volAttack",
112
+ "volHold",
113
+ "volDecay",
114
+ "volSustain",
115
+ "volRelease",
116
+ "initialAttenuation",
117
+ ];
118
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
105
119
  const filterEnvelopeKeys = [
106
120
  "modEnvToPitch",
107
121
  "initialFilterFc",
@@ -111,20 +125,18 @@ const filterEnvelopeKeys = [
111
125
  "modHold",
112
126
  "modDecay",
113
127
  "modSustain",
114
- "modRelease",
115
- "playbackRate",
116
128
  ];
117
129
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
118
- const volumeEnvelopeKeys = [
119
- "volDelay",
120
- "volAttack",
121
- "volHold",
122
- "volDecay",
123
- "volSustain",
124
- "volRelease",
125
- "initialAttenuation",
130
+ const pitchEnvelopeKeys = [
131
+ "modEnvToPitch",
132
+ "modDelay",
133
+ "modAttack",
134
+ "modHold",
135
+ "modDecay",
136
+ "modSustain",
137
+ "playbackRate",
126
138
  ];
127
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
139
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
128
140
  class MidyGM1 {
129
141
  constructor(audioContext) {
130
142
  Object.defineProperty(this, "mode", {
@@ -292,24 +304,44 @@ class MidyGM1 {
292
304
  }
293
305
  }
294
306
  }
295
- async loadSoundFont(soundFontUrl) {
296
- const response = await fetch(soundFontUrl);
297
- const arrayBuffer = await response.arrayBuffer();
298
- const parsed = (0, soundfont_parser_1.parse)(new Uint8Array(arrayBuffer));
307
+ async loadSoundFont(input) {
308
+ let uint8Array;
309
+ if (typeof input === "string") {
310
+ const response = await fetch(input);
311
+ const arrayBuffer = await response.arrayBuffer();
312
+ uint8Array = new Uint8Array(arrayBuffer);
313
+ }
314
+ else if (input instanceof Uint8Array) {
315
+ uint8Array = input;
316
+ }
317
+ else {
318
+ throw new TypeError("input must be a URL string or Uint8Array");
319
+ }
320
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
299
321
  const soundFont = new soundfont_parser_1.SoundFont(parsed);
300
322
  this.addSoundFont(soundFont);
301
323
  }
302
- async loadMIDI(midiUrl) {
303
- const response = await fetch(midiUrl);
304
- const arrayBuffer = await response.arrayBuffer();
305
- const midi = (0, midi_file_1.parseMidi)(new Uint8Array(arrayBuffer));
324
+ async loadMIDI(input) {
325
+ let uint8Array;
326
+ if (typeof input === "string") {
327
+ const response = await fetch(input);
328
+ const arrayBuffer = await response.arrayBuffer();
329
+ uint8Array = new Uint8Array(arrayBuffer);
330
+ }
331
+ else if (input instanceof Uint8Array) {
332
+ uint8Array = input;
333
+ }
334
+ else {
335
+ throw new TypeError("input must be a URL string or Uint8Array");
336
+ }
337
+ const midi = (0, midi_file_1.parseMidi)(uint8Array);
306
338
  this.ticksPerBeat = midi.header.ticksPerBeat;
307
339
  const midiData = this.extractMidiData(midi);
308
340
  this.instruments = midiData.instruments;
309
341
  this.timeline = midiData.timeline;
310
342
  this.totalTime = this.calcTotalTime();
311
343
  }
312
- setChannelAudioNodes(audioContext) {
344
+ createChannelAudioNodes(audioContext) {
313
345
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
314
346
  const gainL = new GainNode(audioContext, { gain: gainLeft });
315
347
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -330,7 +362,7 @@ class MidyGM1 {
330
362
  isDrum: false,
331
363
  state: new ControllerState(),
332
364
  ...this.constructor.channelSettings,
333
- ...this.setChannelAudioNodes(audioContext),
365
+ ...this.createChannelAudioNodes(audioContext),
334
366
  scheduledNotes: [],
335
367
  sustainNotes: [],
336
368
  };
@@ -384,12 +416,13 @@ class MidyGM1 {
384
416
  const delay = this.startDelay - resumeTime;
385
417
  const startTime = event.startTime + delay;
386
418
  switch (event.type) {
387
- case "noteOn": {
388
- const noteOffEvent = {
389
- ...event.noteOffEvent,
390
- startTime: event.noteOffEvent.startTime + delay,
391
- };
392
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
419
+ case "noteOn":
420
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
421
+ break;
422
+ case "noteOff": {
423
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
424
+ if (notePromise)
425
+ this.notePromises.push(notePromise);
393
426
  break;
394
427
  }
395
428
  case "controller":
@@ -488,6 +521,7 @@ class MidyGM1 {
488
521
  return `${programNumber}:${noteNumber}:${velocity}`;
489
522
  }
490
523
  extractMidiData(midi) {
524
+ this.audioBufferCounter.clear();
491
525
  const instruments = new Set();
492
526
  const timeline = [];
493
527
  const tmpChannels = new Array(this.channels.length);
@@ -551,38 +585,13 @@ class MidyGM1 {
551
585
  prevTempoTicks = event.ticks;
552
586
  }
553
587
  }
554
- const activeNotes = new Array(this.channels.length * 128);
555
- for (let i = 0; i < activeNotes.length; i++) {
556
- activeNotes[i] = [];
557
- }
558
- for (let i = 0; i < timeline.length; i++) {
559
- const event = timeline[i];
560
- switch (event.type) {
561
- case "noteOn": {
562
- const index = event.channel * 128 + event.noteNumber;
563
- activeNotes[index].push(event);
564
- break;
565
- }
566
- case "noteOff": {
567
- const index = event.channel * 128 + event.noteNumber;
568
- const noteOn = activeNotes[index].pop();
569
- if (noteOn) {
570
- noteOn.noteOffEvent = event;
571
- }
572
- else {
573
- const eventString = JSON.stringify(event, null, 2);
574
- console.warn(`noteOff without matching noteOn: ${eventString}`);
575
- }
576
- }
577
- }
578
- }
579
588
  return { instruments, timeline };
580
589
  }
581
590
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
582
591
  const channel = this.channels[channelNumber];
583
592
  const promises = [];
584
593
  this.processActiveNotes(channel, scheduleTime, (note) => {
585
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
594
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
586
595
  this.notePromises.push(promise);
587
596
  promises.push(promise);
588
597
  });
@@ -591,8 +600,8 @@ class MidyGM1 {
591
600
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
592
601
  const channel = this.channels[channelNumber];
593
602
  const promises = [];
594
- this.processScheduledNotes(channel, (note) => {
595
- const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
603
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
604
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
596
605
  this.notePromises.push(promise);
597
606
  promises.push(promise);
598
607
  });
@@ -650,7 +659,7 @@ class MidyGM1 {
650
659
  const now = this.audioContext.currentTime;
651
660
  return this.resumeTime + now - this.startTime - this.startDelay;
652
661
  }
653
- processScheduledNotes(channel, callback) {
662
+ processScheduledNotes(channel, scheduleTime, callback) {
654
663
  const scheduledNotes = channel.scheduledNotes;
655
664
  for (let i = 0; i < scheduledNotes.length; i++) {
656
665
  const note = scheduledNotes[i];
@@ -658,6 +667,8 @@ class MidyGM1 {
658
667
  continue;
659
668
  if (note.ending)
660
669
  continue;
670
+ if (note.startTime < scheduleTime)
671
+ continue;
661
672
  callback(note);
662
673
  }
663
674
  }
@@ -669,11 +680,8 @@ class MidyGM1 {
669
680
  continue;
670
681
  if (note.ending)
671
682
  continue;
672
- const noteOffEvent = note.noteOffEvent;
673
- if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
674
- continue;
675
683
  if (scheduleTime < note.startTime)
676
- continue;
684
+ break;
677
685
  callback(note);
678
686
  }
679
687
  }
@@ -697,7 +705,7 @@ class MidyGM1 {
697
705
  return tuning + pitch;
698
706
  }
699
707
  updateChannelDetune(channel, scheduleTime) {
700
- this.processScheduledNotes(channel, (note) => {
708
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
701
709
  this.updateDetune(channel, note, scheduleTime);
702
710
  });
703
711
  }
@@ -841,13 +849,13 @@ class MidyGM1 {
841
849
  if (prev) {
842
850
  const [prevNote, prevChannelNumber] = prev;
843
851
  if (prevNote && !prevNote.ending) {
844
- this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
852
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
845
853
  startTime, true);
846
854
  }
847
855
  }
848
856
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
849
857
  }
850
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
858
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
851
859
  const channel = this.channels[channelNumber];
852
860
  const bankNumber = channel.bank;
853
861
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -859,7 +867,6 @@ class MidyGM1 {
859
867
  return;
860
868
  const isSF3 = soundFont.parsed.info.version.major === 3;
861
869
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
862
- note.noteOffEvent = noteOffEvent;
863
870
  note.volumeEnvelopeNode.connect(channel.gainL);
864
871
  note.volumeEnvelopeNode.connect(channel.gainR);
865
872
  if (0.5 <= channel.state.sustainPedal) {
@@ -869,12 +876,6 @@ class MidyGM1 {
869
876
  const scheduledNotes = channel.scheduledNotes;
870
877
  note.index = scheduledNotes.length;
871
878
  scheduledNotes.push(note);
872
- if (noteOffEvent) {
873
- const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
874
- if (notePromise) {
875
- this.notePromises.push(notePromise);
876
- }
877
- }
878
879
  }
879
880
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
880
881
  scheduleTime ??= this.audioContext.currentTime;
@@ -890,34 +891,36 @@ class MidyGM1 {
890
891
  note.modulationLFO.stop();
891
892
  }
892
893
  }
893
- stopNote(channel, note, endTime, stopTime) {
894
+ releaseNote(channel, note, endTime) {
895
+ const volRelease = endTime + note.voiceParams.volRelease;
896
+ const modRelease = endTime + note.voiceParams.modRelease;
897
+ const stopTime = Math.min(volRelease, modRelease);
898
+ note.filterNode.frequency
899
+ .cancelScheduledValues(endTime)
900
+ .linearRampToValueAtTime(0, modRelease);
894
901
  note.volumeEnvelopeNode.gain
895
902
  .cancelScheduledValues(endTime)
896
- .linearRampToValueAtTime(0, stopTime);
897
- note.ending = true;
898
- this.scheduleTask(() => {
899
- note.bufferSource.loop = false;
900
- }, stopTime);
903
+ .linearRampToValueAtTime(0, volRelease);
901
904
  return new Promise((resolve) => {
902
- note.bufferSource.onended = () => {
903
- channel.scheduledNotes[note.index] = undefined;
905
+ this.scheduleTask(() => {
906
+ const bufferSource = note.bufferSource;
907
+ bufferSource.loop = false;
908
+ bufferSource.stop(stopTime);
904
909
  this.disconnectNote(note);
910
+ channel.scheduledNotes[note.index] = undefined;
905
911
  resolve();
906
- };
907
- note.bufferSource.stop(stopTime);
912
+ }, stopTime);
908
913
  });
909
914
  }
910
- scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
915
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
911
916
  const channel = this.channels[channelNumber];
912
917
  if (!force && 0.5 <= channel.state.sustainPedal)
913
918
  return;
914
- const volRelease = endTime + note.voiceParams.volRelease;
915
- const modRelease = endTime + note.voiceParams.modRelease;
916
- note.filterNode.frequency
917
- .cancelScheduledValues(endTime)
918
- .linearRampToValueAtTime(0, modRelease);
919
- const stopTime = Math.min(volRelease, modRelease);
920
- return this.stopNote(channel, note, endTime, stopTime);
919
+ const note = this.findNoteOffTarget(channel, noteNumber);
920
+ if (!note)
921
+ return;
922
+ note.ending = true;
923
+ this.releaseNote(channel, note, endTime);
921
924
  }
922
925
  findNoteOffTarget(channel, noteNumber) {
923
926
  const scheduledNotes = channel.scheduledNotes;
@@ -934,16 +937,14 @@ class MidyGM1 {
934
937
  }
935
938
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
936
939
  scheduleTime ??= this.audioContext.currentTime;
937
- const channel = this.channels[channelNumber];
938
- const note = this.findNoteOffTarget(channel, noteNumber);
939
- return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
940
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
940
941
  }
941
942
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
942
943
  const velocity = halfVelocity * 2;
943
944
  const channel = this.channels[channelNumber];
944
945
  const promises = [];
945
946
  for (let i = 0; i < channel.sustainNotes.length; i++) {
946
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
947
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
947
948
  promises.push(promise);
948
949
  }
949
950
  channel.sustainNotes = [];
@@ -1057,11 +1058,12 @@ class MidyGM1 {
1057
1058
  return state;
1058
1059
  }
1059
1060
  applyVoiceParams(channel, controllerType, scheduleTime) {
1060
- this.processScheduledNotes(channel, (note) => {
1061
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1061
1062
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1062
1063
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1063
- let appliedFilterEnvelope = false;
1064
- let appliedVolumeEnvelope = false;
1064
+ let applyVolumeEnvelope = false;
1065
+ let applyFilterEnvelope = false;
1066
+ let applyPitchEnvelope = false;
1065
1067
  for (const [key, value] of Object.entries(voiceParams)) {
1066
1068
  const prevValue = note.voiceParams[key];
1067
1069
  if (value === prevValue)
@@ -1070,32 +1072,21 @@ class MidyGM1 {
1070
1072
  if (key in this.voiceParamsHandlers) {
1071
1073
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1072
1074
  }
1073
- else if (filterEnvelopeKeySet.has(key)) {
1074
- if (appliedFilterEnvelope)
1075
- continue;
1076
- appliedFilterEnvelope = true;
1077
- const noteVoiceParams = note.voiceParams;
1078
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1079
- const key = filterEnvelopeKeys[i];
1080
- if (key in voiceParams)
1081
- noteVoiceParams[key] = voiceParams[key];
1082
- }
1083
- this.setFilterEnvelope(note, scheduleTime);
1084
- this.setPitchEnvelope(note, scheduleTime);
1085
- }
1086
- else if (volumeEnvelopeKeySet.has(key)) {
1087
- if (appliedVolumeEnvelope)
1088
- continue;
1089
- appliedVolumeEnvelope = true;
1090
- const noteVoiceParams = note.voiceParams;
1091
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1092
- const key = volumeEnvelopeKeys[i];
1093
- if (key in voiceParams)
1094
- noteVoiceParams[key] = voiceParams[key];
1095
- }
1096
- this.setVolumeEnvelope(note, scheduleTime);
1075
+ else {
1076
+ if (volumeEnvelopeKeySet.has(key))
1077
+ applyVolumeEnvelope = true;
1078
+ if (filterEnvelopeKeySet.has(key))
1079
+ applyFilterEnvelope = true;
1080
+ if (pitchEnvelopeKeySet.has(key))
1081
+ applyPitchEnvelope = true;
1097
1082
  }
1098
1083
  }
1084
+ if (applyVolumeEnvelope)
1085
+ this.setVolumeEnvelope(note, scheduleTime);
1086
+ if (applyFilterEnvelope)
1087
+ this.setFilterEnvelope(note, scheduleTime);
1088
+ if (applyPitchEnvelope)
1089
+ this.setPitchEnvelope(note, scheduleTime);
1099
1090
  });
1100
1091
  }
1101
1092
  createControlChangeHandlers() {
@@ -1126,7 +1117,7 @@ class MidyGM1 {
1126
1117
  }
1127
1118
  updateModulation(channel, scheduleTime) {
1128
1119
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1129
- this.processScheduledNotes(channel, (note) => {
1120
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1130
1121
  if (note.modulationDepth) {
1131
1122
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1132
1123
  }
@@ -1187,7 +1178,7 @@ class MidyGM1 {
1187
1178
  scheduleTime ??= this.audioContext.currentTime;
1188
1179
  channel.state.sustainPedal = value / 127;
1189
1180
  if (64 <= value) {
1190
- this.processScheduledNotes(channel, (note) => {
1181
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1191
1182
  channel.sustainNotes.push(note);
1192
1183
  });
1193
1184
  }