@marmooo/midy 0.3.3 → 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>;
58
- setChannelAudioNodes(audioContext: any): {
60
+ cacheVoiceIds(): void;
61
+ getVoiceId(channel: any, noteNumber: any, velocity: any): string | undefined;
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[];
@@ -97,20 +100,21 @@ 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
- scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any, noteOffEvent: any): Promise<void>;
107
+ scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
105
108
  noteOn(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<void>;
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":"AAoGA;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,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,+GA0BC;IAED,gHA6CC;IAED,0EAiBC;IAED,8EAiBC;IAED,qHAyCC;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,6EA2CC;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;AA19CD;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"}
@@ -86,13 +86,11 @@ const defaultControllerState = {
86
86
  pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
87
87
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
88
88
  link: { type: 127, defaultValue: 0 },
89
- // bankMSB: { type: 128 + 0, defaultValue: 121, },
90
89
  modulationDepth: { type: 128 + 1, defaultValue: 0 },
91
90
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
92
91
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
93
92
  pan: { type: 128 + 10, defaultValue: 64 / 127 },
94
93
  expression: { type: 128 + 11, defaultValue: 1 },
95
- // bankLSB: { type: 128 + 32, defaultValue: 0, },
96
94
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
97
95
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
98
96
  // rpnLSB: { type: 128 + 100, defaultValue: 127 },
@@ -121,6 +119,16 @@ class ControllerState {
121
119
  }
122
120
  }
123
121
  }
122
+ const volumeEnvelopeKeys = [
123
+ "volDelay",
124
+ "volAttack",
125
+ "volHold",
126
+ "volDecay",
127
+ "volSustain",
128
+ "volRelease",
129
+ "initialAttenuation",
130
+ ];
131
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
124
132
  const filterEnvelopeKeys = [
125
133
  "modEnvToPitch",
126
134
  "initialFilterFc",
@@ -130,20 +138,18 @@ const filterEnvelopeKeys = [
130
138
  "modHold",
131
139
  "modDecay",
132
140
  "modSustain",
133
- "modRelease",
134
- "playbackRate",
135
141
  ];
136
142
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
137
- const volumeEnvelopeKeys = [
138
- "volDelay",
139
- "volAttack",
140
- "volHold",
141
- "volDecay",
142
- "volSustain",
143
- "volRelease",
144
- "initialAttenuation",
143
+ const pitchEnvelopeKeys = [
144
+ "modEnvToPitch",
145
+ "modDelay",
146
+ "modAttack",
147
+ "modHold",
148
+ "modDecay",
149
+ "modSustain",
150
+ "playbackRate",
145
151
  ];
146
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
152
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
147
153
  class MidyGMLite {
148
154
  constructor(audioContext) {
149
155
  Object.defineProperty(this, "mode", {
@@ -212,13 +218,13 @@ class MidyGMLite {
212
218
  writable: true,
213
219
  value: this.initSoundFontTable()
214
220
  });
215
- Object.defineProperty(this, "audioBufferCounter", {
221
+ Object.defineProperty(this, "voiceCounter", {
216
222
  enumerable: true,
217
223
  configurable: true,
218
224
  writable: true,
219
225
  value: new Map()
220
226
  });
221
- Object.defineProperty(this, "audioBufferCache", {
227
+ Object.defineProperty(this, "voiceCache", {
222
228
  enumerable: true,
223
229
  configurable: true,
224
230
  writable: true,
@@ -311,13 +317,11 @@ class MidyGMLite {
311
317
  const presetHeaders = soundFont.parsed.presetHeaders;
312
318
  for (let i = 0; i < presetHeaders.length; i++) {
313
319
  const presetHeader = presetHeaders[i];
314
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
315
- const banks = this.soundFontTable[presetHeader.preset];
316
- banks.set(presetHeader.bank, index);
317
- }
320
+ const banks = this.soundFontTable[presetHeader.preset];
321
+ banks.set(presetHeader.bank, index);
318
322
  }
319
323
  }
320
- async loadSoundFont(input) {
324
+ async toUint8Array(input) {
321
325
  let uint8Array;
322
326
  if (typeof input === "string") {
323
327
  const response = await fetch(input);
@@ -330,23 +334,32 @@ class MidyGMLite {
330
334
  else {
331
335
  throw new TypeError("input must be a URL string or Uint8Array");
332
336
  }
333
- const parsed = (0, soundfont_parser_1.parse)(uint8Array);
334
- const soundFont = new soundfont_parser_1.SoundFont(parsed);
335
- this.addSoundFont(soundFont);
337
+ return uint8Array;
336
338
  }
337
- async loadMIDI(input) {
338
- let uint8Array;
339
- if (typeof input === "string") {
340
- const response = await fetch(input);
341
- const arrayBuffer = await response.arrayBuffer();
342
- uint8Array = new Uint8Array(arrayBuffer);
343
- }
344
- else if (input instanceof Uint8Array) {
345
- 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
+ }
346
352
  }
347
353
  else {
348
- 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);
349
358
  }
359
+ }
360
+ async loadMIDI(input) {
361
+ this.voiceCounter.clear();
362
+ const uint8Array = await this.toUint8Array(input);
350
363
  const midi = (0, midi_file_1.parseMidi)(uint8Array);
351
364
  this.ticksPerBeat = midi.header.ticksPerBeat;
352
365
  const midiData = this.extractMidiData(midi);
@@ -354,7 +367,46 @@ class MidyGMLite {
354
367
  this.timeline = midiData.timeline;
355
368
  this.totalTime = this.calcTotalTime();
356
369
  }
357
- setChannelAudioNodes(audioContext) {
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}:${instrument}:${sampleID}`;
408
+ }
409
+ createChannelAudioNodes(audioContext) {
358
410
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
359
411
  const gainL = new GainNode(audioContext, { gain: gainLeft });
360
412
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -375,41 +427,19 @@ class MidyGMLite {
375
427
  isDrum: false,
376
428
  state: new ControllerState(),
377
429
  ...this.constructor.channelSettings,
378
- ...this.setChannelAudioNodes(audioContext),
430
+ ...this.createChannelAudioNodes(audioContext),
379
431
  scheduledNotes: [],
380
432
  sustainNotes: [],
381
433
  };
382
434
  });
383
435
  return channels;
384
436
  }
385
- async createNoteBuffer(voiceParams, isSF3) {
437
+ async createAudioBuffer(voiceParams) {
438
+ const sample = voiceParams.sample;
386
439
  const sampleStart = voiceParams.start;
387
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
388
- if (isSF3) {
389
- const sample = voiceParams.sample;
390
- const start = sample.byteOffset + sampleStart;
391
- const end = sample.byteOffset + sampleEnd;
392
- const buffer = sample.buffer.slice(start, end);
393
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
394
- return audioBuffer;
395
- }
396
- else {
397
- const sample = voiceParams.sample;
398
- const start = sample.byteOffset + sampleStart;
399
- const end = sample.byteOffset + sampleEnd;
400
- const buffer = sample.buffer.slice(start, end);
401
- const audioBuffer = new AudioBuffer({
402
- numberOfChannels: 1,
403
- length: sample.length,
404
- sampleRate: voiceParams.sampleRate,
405
- });
406
- const channelData = audioBuffer.getChannelData(0);
407
- const int16Array = new Int16Array(buffer);
408
- for (let i = 0; i < int16Array.length; i++) {
409
- channelData[i] = int16Array[i] / 32768;
410
- }
411
- return audioBuffer;
412
- }
440
+ const sampleEnd = sample.data.length + voiceParams.end;
441
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
442
+ return audioBuffer;
413
443
  }
414
444
  createBufferSource(channel, voiceParams, audioBuffer) {
415
445
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
@@ -441,10 +471,10 @@ class MidyGMLite {
441
471
  break;
442
472
  }
443
473
  case "controller":
444
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
474
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
445
475
  break;
446
476
  case "programChange":
447
- this.handleProgramChange(event.channel, event.programNumber, startTime);
477
+ this.setProgramChange(event.channel, event.programNumber, startTime);
448
478
  break;
449
479
  case "pitchBend":
450
480
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -478,7 +508,7 @@ class MidyGMLite {
478
508
  this.notePromises = [];
479
509
  this.exclusiveClassNotes.fill(undefined);
480
510
  this.drumExclusiveClassNotes.fill(undefined);
481
- this.audioBufferCache.clear();
511
+ this.voiceCache.clear();
482
512
  for (let i = 0; i < this.channels.length; i++) {
483
513
  this.resetAllStates(i);
484
514
  }
@@ -501,7 +531,7 @@ class MidyGMLite {
501
531
  this.notePromises = [];
502
532
  this.exclusiveClassNotes.fill(undefined);
503
533
  this.drumExclusiveClassNotes.fill(undefined);
504
- this.audioBufferCache.clear();
534
+ this.voiceCache.clear();
505
535
  for (let i = 0; i < this.channels.length; i++) {
506
536
  this.resetAllStates(i);
507
537
  }
@@ -535,11 +565,7 @@ class MidyGMLite {
535
565
  secondToTicks(second, secondsPerBeat) {
536
566
  return second * this.ticksPerBeat / secondsPerBeat;
537
567
  }
538
- getAudioBufferId(programNumber, noteNumber, velocity) {
539
- return `${programNumber}:${noteNumber}:${velocity}`;
540
- }
541
568
  extractMidiData(midi) {
542
- this.audioBufferCounter.clear();
543
569
  const instruments = new Set();
544
570
  const timeline = [];
545
571
  const tmpChannels = new Array(this.channels.length);
@@ -559,8 +585,6 @@ class MidyGMLite {
559
585
  switch (event.type) {
560
586
  case "noteOn": {
561
587
  const channel = tmpChannels[event.channel];
562
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
563
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
564
588
  if (channel.programNumber < 0) {
565
589
  instruments.add(`${channel.bank}:0`);
566
590
  channel.programNumber = 0;
@@ -577,10 +601,6 @@ class MidyGMLite {
577
601
  timeline.push(event);
578
602
  }
579
603
  }
580
- for (const [audioBufferId, count] of this.audioBufferCounter) {
581
- if (count === 1)
582
- this.audioBufferCounter.delete(audioBufferId);
583
- }
584
604
  const priority = {
585
605
  controller: 0,
586
606
  sysEx: 1,
@@ -623,7 +643,6 @@ class MidyGMLite {
623
643
  this.notePromises.push(promise);
624
644
  promises.push(promise);
625
645
  });
626
- channel.scheduledNotes = [];
627
646
  return Promise.all(promises);
628
647
  }
629
648
  stopNotes(velocity, force, scheduleTime) {
@@ -637,6 +656,8 @@ class MidyGMLite {
637
656
  if (this.isPlaying || this.isPaused)
638
657
  return;
639
658
  this.resumeTime = 0;
659
+ if (this.voiceCounter.size === 0)
660
+ this.cacheVoiceIds();
640
661
  await this.playNotes();
641
662
  this.isPlaying = false;
642
663
  }
@@ -679,7 +700,7 @@ class MidyGMLite {
679
700
  }
680
701
  processScheduledNotes(channel, callback) {
681
702
  const scheduledNotes = channel.scheduledNotes;
682
- for (let i = 0; i < scheduledNotes.length; i++) {
703
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
683
704
  const note = scheduledNotes[i];
684
705
  if (!note)
685
706
  continue;
@@ -690,14 +711,14 @@ class MidyGMLite {
690
711
  }
691
712
  processActiveNotes(channel, scheduleTime, callback) {
692
713
  const scheduledNotes = channel.scheduledNotes;
693
- for (let i = 0; i < scheduledNotes.length; i++) {
714
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
694
715
  const note = scheduledNotes[i];
695
716
  if (!note)
696
717
  continue;
697
718
  if (note.ending)
698
719
  continue;
699
720
  if (scheduleTime < note.startTime)
700
- continue;
721
+ break;
701
722
  callback(note);
702
723
  }
703
724
  }
@@ -812,32 +833,32 @@ class MidyGMLite {
812
833
  note.modulationLFO.connect(note.volumeDepth);
813
834
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
814
835
  }
815
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
816
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
817
- 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);
818
839
  if (cache) {
819
840
  cache.counter += 1;
820
841
  if (cache.maxCount <= cache.counter) {
821
- this.audioBufferCache.delete(audioBufferId);
842
+ this.voiceCache.delete(audioBufferId);
822
843
  }
823
844
  return cache.audioBuffer;
824
845
  }
825
846
  else {
826
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
827
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
847
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
848
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
828
849
  const cache = { audioBuffer, maxCount, counter: 1 };
829
- this.audioBufferCache.set(audioBufferId, cache);
850
+ this.voiceCache.set(audioBufferId, cache);
830
851
  return audioBuffer;
831
852
  }
832
853
  }
833
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
854
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
834
855
  const now = this.audioContext.currentTime;
835
856
  const state = channel.state;
836
857
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
837
858
  const voiceParams = voice.getAllParams(controllerState);
838
859
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
839
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
840
- note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
860
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
861
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
841
862
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
842
863
  note.filterNode = new BiquadFilterNode(this.audioContext, {
843
864
  type: "lowpass",
@@ -884,19 +905,18 @@ class MidyGMLite {
884
905
  }
885
906
  this.drumExclusiveClassNotes[index] = note;
886
907
  }
887
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
908
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
888
909
  const channel = this.channels[channelNumber];
889
910
  const bankNumber = channel.bank;
890
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
911
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
912
+ .get(bankNumber);
891
913
  if (soundFontIndex === undefined)
892
914
  return;
893
915
  const soundFont = this.soundFonts[soundFontIndex];
894
916
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
895
917
  if (!voice)
896
918
  return;
897
- const isSF3 = soundFont.parsed.info.version.major === 3;
898
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
899
- note.noteOffEvent = noteOffEvent;
919
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
900
920
  note.volumeEnvelopeNode.connect(channel.gainL);
901
921
  note.volumeEnvelopeNode.connect(channel.gainR);
902
922
  if (0.5 <= channel.state.sustainPedal) {
@@ -951,15 +971,29 @@ class MidyGMLite {
951
971
  if (0.5 <= channel.state.sustainPedal)
952
972
  return;
953
973
  }
954
- const note = this.findNoteOffTarget(channel, noteNumber);
955
- if (!note)
974
+ const index = this.findNoteOffIndex(channel, noteNumber);
975
+ if (index < 0)
956
976
  return;
977
+ const note = channel.scheduledNotes[index];
957
978
  note.ending = true;
979
+ this.setNoteIndex(channel, index);
958
980
  this.releaseNote(channel, note, endTime);
959
981
  }
960
- 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) {
961
995
  const scheduledNotes = channel.scheduledNotes;
962
- for (let i = 0; i < scheduledNotes.length; i++) {
996
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
963
997
  const note = scheduledNotes[i];
964
998
  if (!note)
965
999
  continue;
@@ -967,8 +1001,9 @@ class MidyGMLite {
967
1001
  continue;
968
1002
  if (note.noteNumber !== noteNumber)
969
1003
  continue;
970
- return note;
1004
+ return i;
971
1005
  }
1006
+ return -1;
972
1007
  }
973
1008
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
974
1009
  scheduleTime ??= this.audioContext.currentTime;
@@ -994,16 +1029,16 @@ class MidyGMLite {
994
1029
  case 0x90:
995
1030
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
996
1031
  case 0xB0:
997
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1032
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
998
1033
  case 0xC0:
999
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1034
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
1000
1035
  case 0xE0:
1001
1036
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1002
1037
  default:
1003
1038
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1004
1039
  }
1005
1040
  }
1006
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1041
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1007
1042
  const channel = this.channels[channelNumber];
1008
1043
  channel.programNumber = programNumber;
1009
1044
  }
@@ -1096,8 +1131,9 @@ class MidyGMLite {
1096
1131
  this.processScheduledNotes(channel, (note) => {
1097
1132
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1098
1133
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1099
- let appliedFilterEnvelope = false;
1100
- let appliedVolumeEnvelope = false;
1134
+ let applyVolumeEnvelope = false;
1135
+ let applyFilterEnvelope = false;
1136
+ let applyPitchEnvelope = false;
1101
1137
  for (const [key, value] of Object.entries(voiceParams)) {
1102
1138
  const prevValue = note.voiceParams[key];
1103
1139
  if (value === prevValue)
@@ -1106,32 +1142,21 @@ class MidyGMLite {
1106
1142
  if (key in this.voiceParamsHandlers) {
1107
1143
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1108
1144
  }
1109
- else if (filterEnvelopeKeySet.has(key)) {
1110
- if (appliedFilterEnvelope)
1111
- continue;
1112
- appliedFilterEnvelope = true;
1113
- const noteVoiceParams = note.voiceParams;
1114
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1115
- const key = filterEnvelopeKeys[i];
1116
- if (key in voiceParams)
1117
- noteVoiceParams[key] = voiceParams[key];
1118
- }
1119
- this.setFilterEnvelope(note, scheduleTime);
1120
- this.setPitchEnvelope(note, scheduleTime);
1121
- }
1122
- else if (volumeEnvelopeKeySet.has(key)) {
1123
- if (appliedVolumeEnvelope)
1124
- continue;
1125
- appliedVolumeEnvelope = true;
1126
- const noteVoiceParams = note.voiceParams;
1127
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1128
- const key = volumeEnvelopeKeys[i];
1129
- if (key in voiceParams)
1130
- noteVoiceParams[key] = voiceParams[key];
1131
- }
1132
- this.setVolumeEnvelope(note, scheduleTime);
1145
+ else {
1146
+ if (volumeEnvelopeKeySet.has(key))
1147
+ applyVolumeEnvelope = true;
1148
+ if (filterEnvelopeKeySet.has(key))
1149
+ applyFilterEnvelope = true;
1150
+ if (pitchEnvelopeKeySet.has(key))
1151
+ applyPitchEnvelope = true;
1133
1152
  }
1134
1153
  }
1154
+ if (applyVolumeEnvelope)
1155
+ this.setVolumeEnvelope(note, scheduleTime);
1156
+ if (applyFilterEnvelope)
1157
+ this.setFilterEnvelope(note, scheduleTime);
1158
+ if (applyPitchEnvelope)
1159
+ this.setPitchEnvelope(note, scheduleTime);
1135
1160
  });
1136
1161
  }
1137
1162
  createControlChangeHandlers() {
@@ -1150,7 +1175,7 @@ class MidyGMLite {
1150
1175
  handlers[123] = this.allNotesOff;
1151
1176
  return handlers;
1152
1177
  }
1153
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1178
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1154
1179
  const handler = this.controlChangeHandlers[controllerType];
1155
1180
  if (handler) {
1156
1181
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1299,7 +1324,7 @@ class MidyGMLite {
1299
1324
  const entries = Object.entries(defaultControllerState);
1300
1325
  for (const [key, { type, defaultValue }] of entries) {
1301
1326
  if (128 <= type) {
1302
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1327
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1303
1328
  }
1304
1329
  else {
1305
1330
  state[key] = defaultValue;
@@ -1324,7 +1349,7 @@ class MidyGMLite {
1324
1349
  const key = keys[i];
1325
1350
  const { type, defaultValue } = defaultControllerState[key];
1326
1351
  if (128 <= type) {
1327
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1352
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1328
1353
  }
1329
1354
  else {
1330
1355
  state[key] = defaultValue;
@@ -1438,6 +1463,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
1438
1463
  configurable: true,
1439
1464
  writable: true,
1440
1465
  value: {
1466
+ scheduleIndex: 0,
1441
1467
  detune: 0,
1442
1468
  programNumber: 0,
1443
1469
  bank: 0,