@marmooo/midy 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,16 +22,16 @@ 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;
29
30
  isStopping: boolean;
30
31
  isSeeking: boolean;
31
32
  timeline: any[];
32
- instruments: any[];
33
33
  notePromises: any[];
34
+ instruments: Set<any>;
34
35
  exclusiveClassNotes: any[];
35
36
  drumExclusiveClassNotes: any[];
36
37
  audioContext: any;
@@ -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): any;
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,oBAAkB;IAClB,sBAAwB;IACxB,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,8DAcC;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,mEAWC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAgCC;IAED,qCAeC;IAED,+FAWC;IAED,wDASC;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;AAx/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"}
@@ -218,13 +218,13 @@ class MidyGMLite {
218
218
  writable: true,
219
219
  value: this.initSoundFontTable()
220
220
  });
221
- Object.defineProperty(this, "audioBufferCounter", {
221
+ Object.defineProperty(this, "voiceCounter", {
222
222
  enumerable: true,
223
223
  configurable: true,
224
224
  writable: true,
225
225
  value: new Map()
226
226
  });
227
- Object.defineProperty(this, "audioBufferCache", {
227
+ Object.defineProperty(this, "voiceCache", {
228
228
  enumerable: true,
229
229
  configurable: true,
230
230
  writable: true,
@@ -266,17 +266,17 @@ class MidyGMLite {
266
266
  writable: true,
267
267
  value: []
268
268
  });
269
- Object.defineProperty(this, "instruments", {
269
+ Object.defineProperty(this, "notePromises", {
270
270
  enumerable: true,
271
271
  configurable: true,
272
272
  writable: true,
273
273
  value: []
274
274
  });
275
- Object.defineProperty(this, "notePromises", {
275
+ Object.defineProperty(this, "instruments", {
276
276
  enumerable: true,
277
277
  configurable: true,
278
278
  writable: true,
279
- value: []
279
+ value: new Set()
280
280
  });
281
281
  Object.defineProperty(this, "exclusiveClassNotes", {
282
282
  enumerable: true,
@@ -317,13 +317,11 @@ class MidyGMLite {
317
317
  const presetHeaders = soundFont.parsed.presetHeaders;
318
318
  for (let i = 0; i < presetHeaders.length; i++) {
319
319
  const presetHeader = presetHeaders[i];
320
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
321
- const banks = this.soundFontTable[presetHeader.preset];
322
- banks.set(presetHeader.bank, index);
323
- }
320
+ const banks = this.soundFontTable[presetHeader.preset];
321
+ banks.set(presetHeader.bank, index);
324
322
  }
325
323
  }
326
- async loadSoundFont(input) {
324
+ async toUint8Array(input) {
327
325
  let uint8Array;
328
326
  if (typeof input === "string") {
329
327
  const response = await fetch(input);
@@ -336,23 +334,32 @@ class MidyGMLite {
336
334
  else {
337
335
  throw new TypeError("input must be a URL string or Uint8Array");
338
336
  }
339
- const parsed = (0, soundfont_parser_1.parse)(uint8Array);
340
- const soundFont = new soundfont_parser_1.SoundFont(parsed);
341
- this.addSoundFont(soundFont);
337
+ return uint8Array;
342
338
  }
343
- async loadMIDI(input) {
344
- let uint8Array;
345
- if (typeof input === "string") {
346
- const response = await fetch(input);
347
- const arrayBuffer = await response.arrayBuffer();
348
- uint8Array = new Uint8Array(arrayBuffer);
349
- }
350
- else if (input instanceof Uint8Array) {
351
- uint8Array = input;
339
+ async loadSoundFont(input) {
340
+ this.voiceCounter.clear();
341
+ if (Array.isArray(input)) {
342
+ const promises = new Array(input.length);
343
+ for (let i = 0; i < input.length; i++) {
344
+ promises[i] = this.toUint8Array(input[i]);
345
+ }
346
+ const uint8Arrays = await Promise.all(promises);
347
+ for (let i = 0; i < uint8Arrays.length; i++) {
348
+ const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
349
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
350
+ this.addSoundFont(soundFont);
351
+ }
352
352
  }
353
353
  else {
354
- throw new TypeError("input must be a URL string or Uint8Array");
354
+ const uint8Array = await this.toUint8Array(input);
355
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
356
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
357
+ this.addSoundFont(soundFont);
355
358
  }
359
+ }
360
+ async loadMIDI(input) {
361
+ this.voiceCounter.clear();
362
+ const uint8Array = await this.toUint8Array(input);
356
363
  const midi = (0, midi_file_1.parseMidi)(uint8Array);
357
364
  this.ticksPerBeat = midi.header.ticksPerBeat;
358
365
  const midiData = this.extractMidiData(midi);
@@ -360,6 +367,45 @@ class MidyGMLite {
360
367
  this.timeline = midiData.timeline;
361
368
  this.totalTime = this.calcTotalTime();
362
369
  }
370
+ cacheVoiceIds() {
371
+ const timeline = this.timeline;
372
+ for (let i = 0; i < timeline.length; i++) {
373
+ const event = timeline[i];
374
+ switch (event.type) {
375
+ case "noteOn": {
376
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
377
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
378
+ break;
379
+ }
380
+ case "controller":
381
+ if (event.controllerType === 0) {
382
+ this.setBankMSB(event.channel, event.value);
383
+ }
384
+ else if (event.controllerType === 32) {
385
+ this.setBankLSB(event.channel, event.value);
386
+ }
387
+ break;
388
+ case "programChange":
389
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
390
+ }
391
+ }
392
+ for (const [audioBufferId, count] of this.voiceCounter) {
393
+ if (count === 1)
394
+ this.voiceCounter.delete(audioBufferId);
395
+ }
396
+ this.GM1SystemOn();
397
+ }
398
+ getVoiceId(channel, noteNumber, velocity) {
399
+ const bankNumber = this.calcBank(channel);
400
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
401
+ .get(bankNumber);
402
+ if (soundFontIndex === undefined)
403
+ return;
404
+ const soundFont = this.soundFonts[soundFontIndex];
405
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
406
+ const { instrument, sampleID } = voice.generators;
407
+ return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
408
+ }
363
409
  createChannelAudioNodes(audioContext) {
364
410
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
365
411
  const gainL = new GainNode(audioContext, { gain: gainLeft });
@@ -388,34 +434,12 @@ class MidyGMLite {
388
434
  });
389
435
  return channels;
390
436
  }
391
- async createNoteBuffer(voiceParams, isSF3) {
437
+ async createAudioBuffer(voiceParams) {
438
+ const sample = voiceParams.sample;
392
439
  const sampleStart = voiceParams.start;
393
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
394
- if (isSF3) {
395
- const sample = voiceParams.sample;
396
- const start = sample.byteOffset + sampleStart;
397
- const end = sample.byteOffset + sampleEnd;
398
- const buffer = sample.buffer.slice(start, end);
399
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
400
- return audioBuffer;
401
- }
402
- else {
403
- const sample = voiceParams.sample;
404
- const start = sample.byteOffset + sampleStart;
405
- const end = sample.byteOffset + sampleEnd;
406
- const buffer = sample.buffer.slice(start, end);
407
- const audioBuffer = new AudioBuffer({
408
- numberOfChannels: 1,
409
- length: sample.length,
410
- sampleRate: voiceParams.sampleRate,
411
- });
412
- const channelData = audioBuffer.getChannelData(0);
413
- const int16Array = new Int16Array(buffer);
414
- for (let i = 0; i < int16Array.length; i++) {
415
- channelData[i] = int16Array[i] / 32768;
416
- }
417
- return audioBuffer;
418
- }
440
+ const sampleEnd = sample.data.length + voiceParams.end;
441
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
442
+ return audioBuffer;
419
443
  }
420
444
  createBufferSource(channel, voiceParams, audioBuffer) {
421
445
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
@@ -447,10 +471,10 @@ class MidyGMLite {
447
471
  break;
448
472
  }
449
473
  case "controller":
450
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
474
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
451
475
  break;
452
476
  case "programChange":
453
- this.handleProgramChange(event.channel, event.programNumber, startTime);
477
+ this.setProgramChange(event.channel, event.programNumber, startTime);
454
478
  break;
455
479
  case "pitchBend":
456
480
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -484,7 +508,7 @@ class MidyGMLite {
484
508
  this.notePromises = [];
485
509
  this.exclusiveClassNotes.fill(undefined);
486
510
  this.drumExclusiveClassNotes.fill(undefined);
487
- this.audioBufferCache.clear();
511
+ this.voiceCache.clear();
488
512
  for (let i = 0; i < this.channels.length; i++) {
489
513
  this.resetAllStates(i);
490
514
  }
@@ -507,7 +531,7 @@ class MidyGMLite {
507
531
  this.notePromises = [];
508
532
  this.exclusiveClassNotes.fill(undefined);
509
533
  this.drumExclusiveClassNotes.fill(undefined);
510
- this.audioBufferCache.clear();
534
+ this.voiceCache.clear();
511
535
  for (let i = 0; i < this.channels.length; i++) {
512
536
  this.resetAllStates(i);
513
537
  }
@@ -541,11 +565,7 @@ class MidyGMLite {
541
565
  secondToTicks(second, secondsPerBeat) {
542
566
  return second * this.ticksPerBeat / secondsPerBeat;
543
567
  }
544
- getAudioBufferId(programNumber, noteNumber, velocity) {
545
- return `${programNumber}:${noteNumber}:${velocity}`;
546
- }
547
568
  extractMidiData(midi) {
548
- this.audioBufferCounter.clear();
549
569
  const instruments = new Set();
550
570
  const timeline = [];
551
571
  const tmpChannels = new Array(this.channels.length);
@@ -565,8 +585,6 @@ class MidyGMLite {
565
585
  switch (event.type) {
566
586
  case "noteOn": {
567
587
  const channel = tmpChannels[event.channel];
568
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
569
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
570
588
  if (channel.programNumber < 0) {
571
589
  instruments.add(`${channel.bank}:0`);
572
590
  channel.programNumber = 0;
@@ -583,10 +601,6 @@ class MidyGMLite {
583
601
  timeline.push(event);
584
602
  }
585
603
  }
586
- for (const [audioBufferId, count] of this.audioBufferCounter) {
587
- if (count === 1)
588
- this.audioBufferCounter.delete(audioBufferId);
589
- }
590
604
  const priority = {
591
605
  controller: 0,
592
606
  sysEx: 1,
@@ -624,12 +638,11 @@ class MidyGMLite {
624
638
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
625
639
  const channel = this.channels[channelNumber];
626
640
  const promises = [];
627
- this.processScheduledNotes(channel, scheduleTime, (note) => {
641
+ this.processScheduledNotes(channel, (note) => {
628
642
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
629
643
  this.notePromises.push(promise);
630
644
  promises.push(promise);
631
645
  });
632
- channel.scheduledNotes = [];
633
646
  return Promise.all(promises);
634
647
  }
635
648
  stopNotes(velocity, force, scheduleTime) {
@@ -643,6 +656,8 @@ class MidyGMLite {
643
656
  if (this.isPlaying || this.isPaused)
644
657
  return;
645
658
  this.resumeTime = 0;
659
+ if (this.voiceCounter.size === 0)
660
+ this.cacheVoiceIds();
646
661
  await this.playNotes();
647
662
  this.isPlaying = false;
648
663
  }
@@ -683,22 +698,20 @@ class MidyGMLite {
683
698
  const now = this.audioContext.currentTime;
684
699
  return this.resumeTime + now - this.startTime - this.startDelay;
685
700
  }
686
- processScheduledNotes(channel, scheduleTime, callback) {
701
+ processScheduledNotes(channel, callback) {
687
702
  const scheduledNotes = channel.scheduledNotes;
688
- for (let i = 0; i < scheduledNotes.length; i++) {
703
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
689
704
  const note = scheduledNotes[i];
690
705
  if (!note)
691
706
  continue;
692
707
  if (note.ending)
693
708
  continue;
694
- if (note.startTime < scheduleTime)
695
- continue;
696
709
  callback(note);
697
710
  }
698
711
  }
699
712
  processActiveNotes(channel, scheduleTime, callback) {
700
713
  const scheduledNotes = channel.scheduledNotes;
701
- for (let i = 0; i < scheduledNotes.length; i++) {
714
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
702
715
  const note = scheduledNotes[i];
703
716
  if (!note)
704
717
  continue;
@@ -727,7 +740,7 @@ class MidyGMLite {
727
740
  return pitchWheel * pitchWheelSensitivity;
728
741
  }
729
742
  updateChannelDetune(channel, scheduleTime) {
730
- this.processScheduledNotes(channel, scheduleTime, (note) => {
743
+ this.processScheduledNotes(channel, (note) => {
731
744
  this.updateDetune(channel, note, scheduleTime);
732
745
  });
733
746
  }
@@ -820,32 +833,32 @@ class MidyGMLite {
820
833
  note.modulationLFO.connect(note.volumeDepth);
821
834
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
822
835
  }
823
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
824
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
825
- const cache = this.audioBufferCache.get(audioBufferId);
836
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
837
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
838
+ const cache = this.voiceCache.get(audioBufferId);
826
839
  if (cache) {
827
840
  cache.counter += 1;
828
841
  if (cache.maxCount <= cache.counter) {
829
- this.audioBufferCache.delete(audioBufferId);
842
+ this.voiceCache.delete(audioBufferId);
830
843
  }
831
844
  return cache.audioBuffer;
832
845
  }
833
846
  else {
834
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
835
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
847
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
848
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
836
849
  const cache = { audioBuffer, maxCount, counter: 1 };
837
- this.audioBufferCache.set(audioBufferId, cache);
850
+ this.voiceCache.set(audioBufferId, cache);
838
851
  return audioBuffer;
839
852
  }
840
853
  }
841
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
854
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
842
855
  const now = this.audioContext.currentTime;
843
856
  const state = channel.state;
844
857
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
845
858
  const voiceParams = voice.getAllParams(controllerState);
846
859
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
847
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
848
- note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
860
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
861
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
849
862
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
850
863
  note.filterNode = new BiquadFilterNode(this.audioContext, {
851
864
  type: "lowpass",
@@ -895,15 +908,15 @@ class MidyGMLite {
895
908
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
896
909
  const channel = this.channels[channelNumber];
897
910
  const bankNumber = channel.bank;
898
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
911
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
912
+ .get(bankNumber);
899
913
  if (soundFontIndex === undefined)
900
914
  return;
901
915
  const soundFont = this.soundFonts[soundFontIndex];
902
916
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
903
917
  if (!voice)
904
918
  return;
905
- const isSF3 = soundFont.parsed.info.version.major === 3;
906
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
919
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
907
920
  note.volumeEnvelopeNode.connect(channel.gainL);
908
921
  note.volumeEnvelopeNode.connect(channel.gainR);
909
922
  if (0.5 <= channel.state.sustainPedal) {
@@ -958,15 +971,29 @@ class MidyGMLite {
958
971
  if (0.5 <= channel.state.sustainPedal)
959
972
  return;
960
973
  }
961
- const note = this.findNoteOffTarget(channel, noteNumber);
962
- if (!note)
974
+ const index = this.findNoteOffIndex(channel, noteNumber);
975
+ if (index < 0)
963
976
  return;
977
+ const note = channel.scheduledNotes[index];
964
978
  note.ending = true;
979
+ this.setNoteIndex(channel, index);
965
980
  this.releaseNote(channel, note, endTime);
966
981
  }
967
- findNoteOffTarget(channel, noteNumber) {
982
+ setNoteIndex(channel, index) {
983
+ let allEnds = true;
984
+ for (let i = channel.scheduleIndex; i < index; i++) {
985
+ const note = channel.scheduledNotes[i];
986
+ if (note && !note.ending) {
987
+ allEnds = false;
988
+ break;
989
+ }
990
+ }
991
+ if (allEnds)
992
+ channel.scheduleIndex = index + 1;
993
+ }
994
+ findNoteOffIndex(channel, noteNumber) {
968
995
  const scheduledNotes = channel.scheduledNotes;
969
- for (let i = 0; i < scheduledNotes.length; i++) {
996
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
970
997
  const note = scheduledNotes[i];
971
998
  if (!note)
972
999
  continue;
@@ -974,8 +1001,9 @@ class MidyGMLite {
974
1001
  continue;
975
1002
  if (note.noteNumber !== noteNumber)
976
1003
  continue;
977
- return note;
1004
+ return i;
978
1005
  }
1006
+ return -1;
979
1007
  }
980
1008
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
981
1009
  scheduleTime ??= this.audioContext.currentTime;
@@ -1001,16 +1029,16 @@ class MidyGMLite {
1001
1029
  case 0x90:
1002
1030
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
1003
1031
  case 0xB0:
1004
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1032
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1005
1033
  case 0xC0:
1006
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1034
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1007
1035
  case 0xE0:
1008
1036
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1009
1037
  default:
1010
1038
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1011
1039
  }
1012
1040
  }
1013
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1041
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1014
1042
  const channel = this.channels[channelNumber];
1015
1043
  channel.programNumber = programNumber;
1016
1044
  }
@@ -1030,13 +1058,17 @@ class MidyGMLite {
1030
1058
  this.applyVoiceParams(channel, 14, scheduleTime);
1031
1059
  }
1032
1060
  setModLfoToPitch(channel, note, scheduleTime) {
1033
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
1034
- const baseDepth = Math.abs(modLfoToPitch) +
1035
- channel.state.modulationDepth;
1036
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1037
- note.modulationDepth.gain
1038
- .cancelScheduledValues(scheduleTime)
1039
- .setValueAtTime(modulationDepth, scheduleTime);
1061
+ if (note.modulationDepth) {
1062
+ const modLfoToPitch = note.voiceParams.modLfoToPitch;
1063
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1064
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1065
+ note.modulationDepth.gain
1066
+ .cancelScheduledValues(scheduleTime)
1067
+ .setValueAtTime(modulationDepth, scheduleTime);
1068
+ }
1069
+ else {
1070
+ this.startModulation(channel, note, scheduleTime);
1071
+ }
1040
1072
  }
1041
1073
  setModLfoToFilterFc(note, scheduleTime) {
1042
1074
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
@@ -1100,7 +1132,7 @@ class MidyGMLite {
1100
1132
  return state;
1101
1133
  }
1102
1134
  applyVoiceParams(channel, controllerType, scheduleTime) {
1103
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1135
+ this.processScheduledNotes(channel, (note) => {
1104
1136
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1105
1137
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1106
1138
  let applyVolumeEnvelope = false;
@@ -1147,7 +1179,7 @@ class MidyGMLite {
1147
1179
  handlers[123] = this.allNotesOff;
1148
1180
  return handlers;
1149
1181
  }
1150
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1182
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1151
1183
  const handler = this.controlChangeHandlers[controllerType];
1152
1184
  if (handler) {
1153
1185
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1160,12 +1192,11 @@ class MidyGMLite {
1160
1192
  }
1161
1193
  updateModulation(channel, scheduleTime) {
1162
1194
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1163
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1195
+ this.processScheduledNotes(channel, (note) => {
1164
1196
  if (note.modulationDepth) {
1165
1197
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1166
1198
  }
1167
1199
  else {
1168
- this.setPitchEnvelope(note, scheduleTime);
1169
1200
  this.startModulation(channel, note, scheduleTime);
1170
1201
  }
1171
1202
  });
@@ -1221,7 +1252,7 @@ class MidyGMLite {
1221
1252
  scheduleTime ??= this.audioContext.currentTime;
1222
1253
  channel.state.sustainPedal = value / 127;
1223
1254
  if (64 <= value) {
1224
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1255
+ this.processScheduledNotes(channel, (note) => {
1225
1256
  channel.sustainNotes.push(note);
1226
1257
  });
1227
1258
  }
@@ -1296,7 +1327,7 @@ class MidyGMLite {
1296
1327
  const entries = Object.entries(defaultControllerState);
1297
1328
  for (const [key, { type, defaultValue }] of entries) {
1298
1329
  if (128 <= type) {
1299
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1330
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1300
1331
  }
1301
1332
  else {
1302
1333
  state[key] = defaultValue;
@@ -1321,7 +1352,7 @@ class MidyGMLite {
1321
1352
  const key = keys[i];
1322
1353
  const { type, defaultValue } = defaultControllerState[key];
1323
1354
  if (128 <= type) {
1324
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1355
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1325
1356
  }
1326
1357
  else {
1327
1358
  state[key] = defaultValue;
@@ -1435,6 +1466,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
1435
1466
  configurable: true,
1436
1467
  writable: true,
1437
1468
  value: {
1469
+ scheduleIndex: 0,
1438
1470
  detune: 0,
1439
1471
  programNumber: 0,
1440
1472
  bank: 0,