@marmooo/midy 0.3.4 → 0.3.5

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,5 +1,6 @@
1
1
  export class MidyGMLite {
2
2
  static channelSettings: {
3
+ scheduleIndex: number;
3
4
  detune: number;
4
5
  programNumber: number;
5
6
  bank: number;
@@ -21,8 +22,8 @@ export class MidyGMLite {
21
22
  resumeTime: number;
22
23
  soundFonts: any[];
23
24
  soundFontTable: any[];
24
- audioBufferCounter: Map<any, any>;
25
- audioBufferCache: Map<any, any>;
25
+ voiceCounter: Map<any, any>;
26
+ voiceCache: Map<any, any>;
26
27
  isPlaying: boolean;
27
28
  isPausing: boolean;
28
29
  isPaused: boolean;
@@ -53,22 +54,24 @@ export class MidyGMLite {
53
54
  channels: any[];
54
55
  initSoundFontTable(): any[];
55
56
  addSoundFont(soundFont: any): void;
57
+ toUint8Array(input: any): Promise<Uint8Array<ArrayBuffer>>;
56
58
  loadSoundFont(input: any): Promise<void>;
57
59
  loadMIDI(input: any): Promise<void>;
60
+ cacheVoiceIds(): void;
61
+ getVoiceId(channel: any, noteNumber: any, velocity: any): string | undefined;
58
62
  createChannelAudioNodes(audioContext: any): {
59
63
  gainL: any;
60
64
  gainR: any;
61
65
  merger: any;
62
66
  };
63
67
  createChannels(audioContext: any): any[];
64
- createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
68
+ createAudioBuffer(voiceParams: any): Promise<any>;
65
69
  createBufferSource(channel: any, voiceParams: any, audioBuffer: any): any;
66
70
  scheduleTimelineEvents(t: any, resumeTime: any, queueIndex: any): Promise<any>;
67
71
  getQueueIndex(second: any): number;
68
72
  playNotes(): Promise<any>;
69
73
  ticksToSecond(ticks: any, secondsPerBeat: any): number;
70
74
  secondToTicks(second: any, secondsPerBeat: any): number;
71
- getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
72
75
  extractMidiData(midi: any): {
73
76
  instruments: Set<any>;
74
77
  timeline: any[];
@@ -83,7 +86,7 @@ export class MidyGMLite {
83
86
  seekTo(second: any): void;
84
87
  calcTotalTime(): number;
85
88
  currentTime(): number;
86
- processScheduledNotes(channel: any, scheduleTime: any, callback: any): void;
89
+ processScheduledNotes(channel: any, callback: any): void;
87
90
  processActiveNotes(channel: any, scheduleTime: any, callback: any): void;
88
91
  cbToRatio(cb: any): number;
89
92
  rateToCent(rate: any): number;
@@ -97,8 +100,8 @@ export class MidyGMLite {
97
100
  clampCutoffFrequency(frequency: any): number;
98
101
  setFilterEnvelope(note: any, scheduleTime: any): void;
99
102
  startModulation(channel: any, note: any, scheduleTime: any): void;
100
- getAudioBuffer(programNumber: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
101
- createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, isSF3: any): Promise<Note>;
103
+ getAudioBuffer(channel: any, noteNumber: any, velocity: any, voiceParams: any): Promise<any>;
104
+ createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any): Promise<Note>;
102
105
  handleExclusiveClass(note: any, channelNumber: any, startTime: any): void;
103
106
  handleDrumExclusiveClass(note: any, channelNumber: any, startTime: any): void;
104
107
  scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
@@ -106,11 +109,12 @@ export class MidyGMLite {
106
109
  disconnectNote(note: any): void;
107
110
  releaseNote(channel: any, note: any, endTime: any): Promise<any>;
108
111
  scheduleNoteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): void;
109
- findNoteOffTarget(channel: any, noteNumber: any): any;
112
+ setNoteIndex(channel: any, index: any): void;
113
+ findNoteOffIndex(channel: any, noteNumber: any): any;
110
114
  noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): void;
111
115
  releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): void[];
112
116
  handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
113
- handleProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
117
+ setProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
114
118
  handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
115
119
  setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
116
120
  setModLfoToPitch(channel: any, note: any, scheduleTime: any): void;
@@ -133,7 +137,7 @@ export class MidyGMLite {
133
137
  getControllerState(channel: any, noteNumber: any, velocity: any): Float32Array<any>;
134
138
  applyVoiceParams(channel: any, controllerType: any, scheduleTime: any): void;
135
139
  createControlChangeHandlers(): any[];
136
- handleControlChange(channelNumber: any, controllerType: any, value: any, scheduleTime: any): void;
140
+ setControlChange(channelNumber: any, controllerType: any, value: any, scheduleTime: any): void;
137
141
  updateModulation(channel: any, scheduleTime: any): void;
138
142
  setModulationDepth(channelNumber: any, modulation: any, scheduleTime: any): void;
139
143
  setVolume(channelNumber: any, volume: any, scheduleTime: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AA0GA;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,6BAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCAWC;IAED,yCAcC;IAED,oCAiBC;IAED;;;;MAeC;IAED,yCAaC;IAED,6DA2BC;IAED,0EAUC;IAED,+EAkDC;IAED,mCAOC;IAED,0BAiEC;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,wCAIC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,+GA0BC;IAED,gHA6CC;IAED,0EAiBC;IAED,8EAiBC;IAED,kGAuCC;IAED,6FASC;IAED,gCASC;IAED,iEAoBC;IAED,qGAgBC;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,qCAeC;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,yCAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AAp9CD;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"}
1
+ {"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AA0GA;IA2BE;;;;;;;;;;MAUE;IAEF,+BAcC;IApDD,aAAa;IACb,oBAAiB;IACjB,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,4BAAyB;IACzB,0BAAuB;IACvB,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAClB,2BAAqC;IACrC,+BAEE;IAeA,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCASC;IAED,2DAYC;IAED,yCAmBC;IAED,oCASC;IAED,sBAoCC;IAED,6EAcC;IAED;;;;MAeC;IAED,yCAaC;IAED,kDAUC;IAED,0EAUC;IAED,+EAkDC;IAED,mCAOC;IAED,0BAiEC;IAED,uDAEC;IAED,wDAEC;IAED;;;MAgEC;IAED,kGAeC;IAED,mGAeC;IAED,wEAMC;IAED,uBAMC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,yDAQC;IAED,yEASC;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,6FAyBC;IAED,oGAuCC;IAED,0EAiBC;IAED,8EAiBC;IAED,kGAoCC;IAED,6FASC;IAED,gCASC;IAED,iEAoBC;IAED,qGAkBC;IAED,6CAUC;IAED,qDAUC;IAED,qFASC;IAED,sFAeC;IAED,oGA2BC;IAED,mFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAQC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAgCC;IAED,qCAeC;IAED,+FAWC;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,yCAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AAt/CD;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"}
@@ -215,13 +215,13 @@ export class MidyGMLite {
215
215
  writable: true,
216
216
  value: this.initSoundFontTable()
217
217
  });
218
- Object.defineProperty(this, "audioBufferCounter", {
218
+ Object.defineProperty(this, "voiceCounter", {
219
219
  enumerable: true,
220
220
  configurable: true,
221
221
  writable: true,
222
222
  value: new Map()
223
223
  });
224
- Object.defineProperty(this, "audioBufferCache", {
224
+ Object.defineProperty(this, "voiceCache", {
225
225
  enumerable: true,
226
226
  configurable: true,
227
227
  writable: true,
@@ -314,13 +314,11 @@ export class MidyGMLite {
314
314
  const presetHeaders = soundFont.parsed.presetHeaders;
315
315
  for (let i = 0; i < presetHeaders.length; i++) {
316
316
  const presetHeader = presetHeaders[i];
317
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
318
- const banks = this.soundFontTable[presetHeader.preset];
319
- banks.set(presetHeader.bank, index);
320
- }
317
+ const banks = this.soundFontTable[presetHeader.preset];
318
+ banks.set(presetHeader.bank, index);
321
319
  }
322
320
  }
323
- async loadSoundFont(input) {
321
+ async toUint8Array(input) {
324
322
  let uint8Array;
325
323
  if (typeof input === "string") {
326
324
  const response = await fetch(input);
@@ -333,23 +331,32 @@ export class MidyGMLite {
333
331
  else {
334
332
  throw new TypeError("input must be a URL string or Uint8Array");
335
333
  }
336
- const parsed = parse(uint8Array);
337
- const soundFont = new SoundFont(parsed);
338
- this.addSoundFont(soundFont);
334
+ return uint8Array;
339
335
  }
340
- async loadMIDI(input) {
341
- let uint8Array;
342
- if (typeof input === "string") {
343
- const response = await fetch(input);
344
- const arrayBuffer = await response.arrayBuffer();
345
- uint8Array = new Uint8Array(arrayBuffer);
346
- }
347
- else if (input instanceof Uint8Array) {
348
- uint8Array = input;
336
+ async loadSoundFont(input) {
337
+ this.voiceCounter.clear();
338
+ if (Array.isArray(input)) {
339
+ const promises = new Array(input.length);
340
+ for (let i = 0; i < input.length; i++) {
341
+ promises[i] = this.toUint8Array(input[i]);
342
+ }
343
+ const uint8Arrays = await Promise.all(promises);
344
+ for (let i = 0; i < uint8Arrays.length; i++) {
345
+ const parsed = parse(uint8Arrays[i]);
346
+ const soundFont = new SoundFont(parsed);
347
+ this.addSoundFont(soundFont);
348
+ }
349
349
  }
350
350
  else {
351
- throw new TypeError("input must be a URL string or Uint8Array");
351
+ const uint8Array = await this.toUint8Array(input);
352
+ const parsed = parse(uint8Array);
353
+ const soundFont = new SoundFont(parsed);
354
+ this.addSoundFont(soundFont);
352
355
  }
356
+ }
357
+ async loadMIDI(input) {
358
+ this.voiceCounter.clear();
359
+ const uint8Array = await this.toUint8Array(input);
353
360
  const midi = parseMidi(uint8Array);
354
361
  this.ticksPerBeat = midi.header.ticksPerBeat;
355
362
  const midiData = this.extractMidiData(midi);
@@ -357,6 +364,45 @@ export class MidyGMLite {
357
364
  this.timeline = midiData.timeline;
358
365
  this.totalTime = this.calcTotalTime();
359
366
  }
367
+ cacheVoiceIds() {
368
+ const timeline = this.timeline;
369
+ for (let i = 0; i < timeline.length; i++) {
370
+ const event = timeline[i];
371
+ switch (event.type) {
372
+ case "noteOn": {
373
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
374
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
375
+ break;
376
+ }
377
+ case "controller":
378
+ if (event.controllerType === 0) {
379
+ this.setBankMSB(event.channel, event.value);
380
+ }
381
+ else if (event.controllerType === 32) {
382
+ this.setBankLSB(event.channel, event.value);
383
+ }
384
+ break;
385
+ case "programChange":
386
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
387
+ }
388
+ }
389
+ for (const [audioBufferId, count] of this.voiceCounter) {
390
+ if (count === 1)
391
+ this.voiceCounter.delete(audioBufferId);
392
+ }
393
+ this.GM1SystemOn();
394
+ }
395
+ getVoiceId(channel, noteNumber, velocity) {
396
+ const bankNumber = this.calcBank(channel);
397
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
398
+ .get(bankNumber);
399
+ if (soundFontIndex === undefined)
400
+ return;
401
+ const soundFont = this.soundFonts[soundFontIndex];
402
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
403
+ const { instrument, sampleID } = voice.generators;
404
+ return `${soundFontIndex}:${instrument}:${sampleID}`;
405
+ }
360
406
  createChannelAudioNodes(audioContext) {
361
407
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
362
408
  const gainL = new GainNode(audioContext, { gain: gainLeft });
@@ -385,34 +431,12 @@ export class MidyGMLite {
385
431
  });
386
432
  return channels;
387
433
  }
388
- async createNoteBuffer(voiceParams, isSF3) {
434
+ async createAudioBuffer(voiceParams) {
435
+ const sample = voiceParams.sample;
389
436
  const sampleStart = voiceParams.start;
390
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
391
- if (isSF3) {
392
- const sample = voiceParams.sample;
393
- const start = sample.byteOffset + sampleStart;
394
- const end = sample.byteOffset + sampleEnd;
395
- const buffer = sample.buffer.slice(start, end);
396
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
397
- return audioBuffer;
398
- }
399
- else {
400
- const sample = voiceParams.sample;
401
- const start = sample.byteOffset + sampleStart;
402
- const end = sample.byteOffset + sampleEnd;
403
- const buffer = sample.buffer.slice(start, end);
404
- const audioBuffer = new AudioBuffer({
405
- numberOfChannels: 1,
406
- length: sample.length,
407
- sampleRate: voiceParams.sampleRate,
408
- });
409
- const channelData = audioBuffer.getChannelData(0);
410
- const int16Array = new Int16Array(buffer);
411
- for (let i = 0; i < int16Array.length; i++) {
412
- channelData[i] = int16Array[i] / 32768;
413
- }
414
- return audioBuffer;
415
- }
437
+ const sampleEnd = sample.data.length + voiceParams.end;
438
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
439
+ return audioBuffer;
416
440
  }
417
441
  createBufferSource(channel, voiceParams, audioBuffer) {
418
442
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
@@ -444,10 +468,10 @@ export class MidyGMLite {
444
468
  break;
445
469
  }
446
470
  case "controller":
447
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
471
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
448
472
  break;
449
473
  case "programChange":
450
- this.handleProgramChange(event.channel, event.programNumber, startTime);
474
+ this.setProgramChange(event.channel, event.programNumber, startTime);
451
475
  break;
452
476
  case "pitchBend":
453
477
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -481,7 +505,7 @@ export class MidyGMLite {
481
505
  this.notePromises = [];
482
506
  this.exclusiveClassNotes.fill(undefined);
483
507
  this.drumExclusiveClassNotes.fill(undefined);
484
- this.audioBufferCache.clear();
508
+ this.voiceCache.clear();
485
509
  for (let i = 0; i < this.channels.length; i++) {
486
510
  this.resetAllStates(i);
487
511
  }
@@ -504,7 +528,7 @@ export class MidyGMLite {
504
528
  this.notePromises = [];
505
529
  this.exclusiveClassNotes.fill(undefined);
506
530
  this.drumExclusiveClassNotes.fill(undefined);
507
- this.audioBufferCache.clear();
531
+ this.voiceCache.clear();
508
532
  for (let i = 0; i < this.channels.length; i++) {
509
533
  this.resetAllStates(i);
510
534
  }
@@ -538,11 +562,7 @@ export class MidyGMLite {
538
562
  secondToTicks(second, secondsPerBeat) {
539
563
  return second * this.ticksPerBeat / secondsPerBeat;
540
564
  }
541
- getAudioBufferId(programNumber, noteNumber, velocity) {
542
- return `${programNumber}:${noteNumber}:${velocity}`;
543
- }
544
565
  extractMidiData(midi) {
545
- this.audioBufferCounter.clear();
546
566
  const instruments = new Set();
547
567
  const timeline = [];
548
568
  const tmpChannels = new Array(this.channels.length);
@@ -562,8 +582,6 @@ export class MidyGMLite {
562
582
  switch (event.type) {
563
583
  case "noteOn": {
564
584
  const channel = tmpChannels[event.channel];
565
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
566
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
567
585
  if (channel.programNumber < 0) {
568
586
  instruments.add(`${channel.bank}:0`);
569
587
  channel.programNumber = 0;
@@ -580,10 +598,6 @@ export class MidyGMLite {
580
598
  timeline.push(event);
581
599
  }
582
600
  }
583
- for (const [audioBufferId, count] of this.audioBufferCounter) {
584
- if (count === 1)
585
- this.audioBufferCounter.delete(audioBufferId);
586
- }
587
601
  const priority = {
588
602
  controller: 0,
589
603
  sysEx: 1,
@@ -621,12 +635,11 @@ export class MidyGMLite {
621
635
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
622
636
  const channel = this.channels[channelNumber];
623
637
  const promises = [];
624
- this.processScheduledNotes(channel, scheduleTime, (note) => {
638
+ this.processScheduledNotes(channel, (note) => {
625
639
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
626
640
  this.notePromises.push(promise);
627
641
  promises.push(promise);
628
642
  });
629
- channel.scheduledNotes = [];
630
643
  return Promise.all(promises);
631
644
  }
632
645
  stopNotes(velocity, force, scheduleTime) {
@@ -640,6 +653,8 @@ export class MidyGMLite {
640
653
  if (this.isPlaying || this.isPaused)
641
654
  return;
642
655
  this.resumeTime = 0;
656
+ if (this.voiceCounter.size === 0)
657
+ this.cacheVoiceIds();
643
658
  await this.playNotes();
644
659
  this.isPlaying = false;
645
660
  }
@@ -680,22 +695,20 @@ export class MidyGMLite {
680
695
  const now = this.audioContext.currentTime;
681
696
  return this.resumeTime + now - this.startTime - this.startDelay;
682
697
  }
683
- processScheduledNotes(channel, scheduleTime, callback) {
698
+ processScheduledNotes(channel, callback) {
684
699
  const scheduledNotes = channel.scheduledNotes;
685
- for (let i = 0; i < scheduledNotes.length; i++) {
700
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
686
701
  const note = scheduledNotes[i];
687
702
  if (!note)
688
703
  continue;
689
704
  if (note.ending)
690
705
  continue;
691
- if (note.startTime < scheduleTime)
692
- continue;
693
706
  callback(note);
694
707
  }
695
708
  }
696
709
  processActiveNotes(channel, scheduleTime, callback) {
697
710
  const scheduledNotes = channel.scheduledNotes;
698
- for (let i = 0; i < scheduledNotes.length; i++) {
711
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
699
712
  const note = scheduledNotes[i];
700
713
  if (!note)
701
714
  continue;
@@ -724,7 +737,7 @@ export class MidyGMLite {
724
737
  return pitchWheel * pitchWheelSensitivity;
725
738
  }
726
739
  updateChannelDetune(channel, scheduleTime) {
727
- this.processScheduledNotes(channel, scheduleTime, (note) => {
740
+ this.processScheduledNotes(channel, (note) => {
728
741
  this.updateDetune(channel, note, scheduleTime);
729
742
  });
730
743
  }
@@ -817,32 +830,32 @@ export class MidyGMLite {
817
830
  note.modulationLFO.connect(note.volumeDepth);
818
831
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
819
832
  }
820
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
821
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
822
- const cache = this.audioBufferCache.get(audioBufferId);
833
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
834
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
835
+ const cache = this.voiceCache.get(audioBufferId);
823
836
  if (cache) {
824
837
  cache.counter += 1;
825
838
  if (cache.maxCount <= cache.counter) {
826
- this.audioBufferCache.delete(audioBufferId);
839
+ this.voiceCache.delete(audioBufferId);
827
840
  }
828
841
  return cache.audioBuffer;
829
842
  }
830
843
  else {
831
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
832
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
844
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
845
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
833
846
  const cache = { audioBuffer, maxCount, counter: 1 };
834
- this.audioBufferCache.set(audioBufferId, cache);
847
+ this.voiceCache.set(audioBufferId, cache);
835
848
  return audioBuffer;
836
849
  }
837
850
  }
838
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
851
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
839
852
  const now = this.audioContext.currentTime;
840
853
  const state = channel.state;
841
854
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
842
855
  const voiceParams = voice.getAllParams(controllerState);
843
856
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
844
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
845
- note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
857
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
858
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
846
859
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
847
860
  note.filterNode = new BiquadFilterNode(this.audioContext, {
848
861
  type: "lowpass",
@@ -892,15 +905,15 @@ export class MidyGMLite {
892
905
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
893
906
  const channel = this.channels[channelNumber];
894
907
  const bankNumber = channel.bank;
895
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
908
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
909
+ .get(bankNumber);
896
910
  if (soundFontIndex === undefined)
897
911
  return;
898
912
  const soundFont = this.soundFonts[soundFontIndex];
899
913
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
900
914
  if (!voice)
901
915
  return;
902
- const isSF3 = soundFont.parsed.info.version.major === 3;
903
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
916
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
904
917
  note.volumeEnvelopeNode.connect(channel.gainL);
905
918
  note.volumeEnvelopeNode.connect(channel.gainR);
906
919
  if (0.5 <= channel.state.sustainPedal) {
@@ -955,15 +968,29 @@ export class MidyGMLite {
955
968
  if (0.5 <= channel.state.sustainPedal)
956
969
  return;
957
970
  }
958
- const note = this.findNoteOffTarget(channel, noteNumber);
959
- if (!note)
971
+ const index = this.findNoteOffIndex(channel, noteNumber);
972
+ if (index < 0)
960
973
  return;
974
+ const note = channel.scheduledNotes[index];
961
975
  note.ending = true;
976
+ this.setNoteIndex(channel, index);
962
977
  this.releaseNote(channel, note, endTime);
963
978
  }
964
- findNoteOffTarget(channel, noteNumber) {
979
+ setNoteIndex(channel, index) {
980
+ let allEnds = true;
981
+ for (let i = channel.scheduleIndex; i < index; i++) {
982
+ const note = channel.scheduledNotes[i];
983
+ if (note && !note.ending) {
984
+ allEnds = false;
985
+ break;
986
+ }
987
+ }
988
+ if (allEnds)
989
+ channel.scheduleIndex = index + 1;
990
+ }
991
+ findNoteOffIndex(channel, noteNumber) {
965
992
  const scheduledNotes = channel.scheduledNotes;
966
- for (let i = 0; i < scheduledNotes.length; i++) {
993
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
967
994
  const note = scheduledNotes[i];
968
995
  if (!note)
969
996
  continue;
@@ -971,8 +998,9 @@ export class MidyGMLite {
971
998
  continue;
972
999
  if (note.noteNumber !== noteNumber)
973
1000
  continue;
974
- return note;
1001
+ return i;
975
1002
  }
1003
+ return -1;
976
1004
  }
977
1005
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
978
1006
  scheduleTime ??= this.audioContext.currentTime;
@@ -998,16 +1026,16 @@ export class MidyGMLite {
998
1026
  case 0x90:
999
1027
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1000
1028
  case 0xB0:
1001
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1029
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1002
1030
  case 0xC0:
1003
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1031
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1004
1032
  case 0xE0:
1005
1033
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1006
1034
  default:
1007
1035
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1008
1036
  }
1009
1037
  }
1010
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1038
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1011
1039
  const channel = this.channels[channelNumber];
1012
1040
  channel.programNumber = programNumber;
1013
1041
  }
@@ -1097,7 +1125,7 @@ export class MidyGMLite {
1097
1125
  return state;
1098
1126
  }
1099
1127
  applyVoiceParams(channel, controllerType, scheduleTime) {
1100
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1128
+ this.processScheduledNotes(channel, (note) => {
1101
1129
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1102
1130
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1103
1131
  let applyVolumeEnvelope = false;
@@ -1144,7 +1172,7 @@ export class MidyGMLite {
1144
1172
  handlers[123] = this.allNotesOff;
1145
1173
  return handlers;
1146
1174
  }
1147
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1175
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1148
1176
  const handler = this.controlChangeHandlers[controllerType];
1149
1177
  if (handler) {
1150
1178
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1157,7 +1185,7 @@ export class MidyGMLite {
1157
1185
  }
1158
1186
  updateModulation(channel, scheduleTime) {
1159
1187
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1160
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1188
+ this.processScheduledNotes(channel, (note) => {
1161
1189
  if (note.modulationDepth) {
1162
1190
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1163
1191
  }
@@ -1218,7 +1246,7 @@ export class MidyGMLite {
1218
1246
  scheduleTime ??= this.audioContext.currentTime;
1219
1247
  channel.state.sustainPedal = value / 127;
1220
1248
  if (64 <= value) {
1221
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1249
+ this.processScheduledNotes(channel, (note) => {
1222
1250
  channel.sustainNotes.push(note);
1223
1251
  });
1224
1252
  }
@@ -1293,7 +1321,7 @@ export class MidyGMLite {
1293
1321
  const entries = Object.entries(defaultControllerState);
1294
1322
  for (const [key, { type, defaultValue }] of entries) {
1295
1323
  if (128 <= type) {
1296
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1324
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1297
1325
  }
1298
1326
  else {
1299
1327
  state[key] = defaultValue;
@@ -1318,7 +1346,7 @@ export class MidyGMLite {
1318
1346
  const key = keys[i];
1319
1347
  const { type, defaultValue } = defaultControllerState[key];
1320
1348
  if (128 <= type) {
1321
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1349
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1322
1350
  }
1323
1351
  else {
1324
1352
  state[key] = defaultValue;
@@ -1431,6 +1459,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
1431
1459
  configurable: true,
1432
1460
  writable: true,
1433
1461
  value: {
1462
+ scheduleIndex: 0,
1434
1463
  detune: 0,
1435
1464
  programNumber: 0,
1436
1465
  bank: 0,