@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"}
@@ -83,13 +83,11 @@ const defaultControllerState = {
83
83
  pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
84
84
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
85
85
  link: { type: 127, defaultValue: 0 },
86
- // bankMSB: { type: 128 + 0, defaultValue: 121, },
87
86
  modulationDepth: { type: 128 + 1, defaultValue: 0 },
88
87
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
89
88
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
90
89
  pan: { type: 128 + 10, defaultValue: 64 / 127 },
91
90
  expression: { type: 128 + 11, defaultValue: 1 },
92
- // bankLSB: { type: 128 + 32, defaultValue: 0, },
93
91
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
94
92
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
95
93
  // rpnLSB: { type: 128 + 100, defaultValue: 127 },
@@ -118,6 +116,16 @@ class ControllerState {
118
116
  }
119
117
  }
120
118
  }
119
+ const volumeEnvelopeKeys = [
120
+ "volDelay",
121
+ "volAttack",
122
+ "volHold",
123
+ "volDecay",
124
+ "volSustain",
125
+ "volRelease",
126
+ "initialAttenuation",
127
+ ];
128
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
121
129
  const filterEnvelopeKeys = [
122
130
  "modEnvToPitch",
123
131
  "initialFilterFc",
@@ -127,20 +135,18 @@ const filterEnvelopeKeys = [
127
135
  "modHold",
128
136
  "modDecay",
129
137
  "modSustain",
130
- "modRelease",
131
- "playbackRate",
132
138
  ];
133
139
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
134
- const volumeEnvelopeKeys = [
135
- "volDelay",
136
- "volAttack",
137
- "volHold",
138
- "volDecay",
139
- "volSustain",
140
- "volRelease",
141
- "initialAttenuation",
140
+ const pitchEnvelopeKeys = [
141
+ "modEnvToPitch",
142
+ "modDelay",
143
+ "modAttack",
144
+ "modHold",
145
+ "modDecay",
146
+ "modSustain",
147
+ "playbackRate",
142
148
  ];
143
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
149
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
144
150
  export class MidyGMLite {
145
151
  constructor(audioContext) {
146
152
  Object.defineProperty(this, "mode", {
@@ -209,13 +215,13 @@ export class MidyGMLite {
209
215
  writable: true,
210
216
  value: this.initSoundFontTable()
211
217
  });
212
- Object.defineProperty(this, "audioBufferCounter", {
218
+ Object.defineProperty(this, "voiceCounter", {
213
219
  enumerable: true,
214
220
  configurable: true,
215
221
  writable: true,
216
222
  value: new Map()
217
223
  });
218
- Object.defineProperty(this, "audioBufferCache", {
224
+ Object.defineProperty(this, "voiceCache", {
219
225
  enumerable: true,
220
226
  configurable: true,
221
227
  writable: true,
@@ -308,13 +314,11 @@ export class MidyGMLite {
308
314
  const presetHeaders = soundFont.parsed.presetHeaders;
309
315
  for (let i = 0; i < presetHeaders.length; i++) {
310
316
  const presetHeader = presetHeaders[i];
311
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
312
- const banks = this.soundFontTable[presetHeader.preset];
313
- banks.set(presetHeader.bank, index);
314
- }
317
+ const banks = this.soundFontTable[presetHeader.preset];
318
+ banks.set(presetHeader.bank, index);
315
319
  }
316
320
  }
317
- async loadSoundFont(input) {
321
+ async toUint8Array(input) {
318
322
  let uint8Array;
319
323
  if (typeof input === "string") {
320
324
  const response = await fetch(input);
@@ -327,23 +331,32 @@ export class MidyGMLite {
327
331
  else {
328
332
  throw new TypeError("input must be a URL string or Uint8Array");
329
333
  }
330
- const parsed = parse(uint8Array);
331
- const soundFont = new SoundFont(parsed);
332
- this.addSoundFont(soundFont);
334
+ return uint8Array;
333
335
  }
334
- async loadMIDI(input) {
335
- let uint8Array;
336
- if (typeof input === "string") {
337
- const response = await fetch(input);
338
- const arrayBuffer = await response.arrayBuffer();
339
- uint8Array = new Uint8Array(arrayBuffer);
340
- }
341
- else if (input instanceof Uint8Array) {
342
- 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
+ }
343
349
  }
344
350
  else {
345
- 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);
346
355
  }
356
+ }
357
+ async loadMIDI(input) {
358
+ this.voiceCounter.clear();
359
+ const uint8Array = await this.toUint8Array(input);
347
360
  const midi = parseMidi(uint8Array);
348
361
  this.ticksPerBeat = midi.header.ticksPerBeat;
349
362
  const midiData = this.extractMidiData(midi);
@@ -351,7 +364,46 @@ export class MidyGMLite {
351
364
  this.timeline = midiData.timeline;
352
365
  this.totalTime = this.calcTotalTime();
353
366
  }
354
- setChannelAudioNodes(audioContext) {
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
+ }
406
+ createChannelAudioNodes(audioContext) {
355
407
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
356
408
  const gainL = new GainNode(audioContext, { gain: gainLeft });
357
409
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -372,41 +424,19 @@ export class MidyGMLite {
372
424
  isDrum: false,
373
425
  state: new ControllerState(),
374
426
  ...this.constructor.channelSettings,
375
- ...this.setChannelAudioNodes(audioContext),
427
+ ...this.createChannelAudioNodes(audioContext),
376
428
  scheduledNotes: [],
377
429
  sustainNotes: [],
378
430
  };
379
431
  });
380
432
  return channels;
381
433
  }
382
- async createNoteBuffer(voiceParams, isSF3) {
434
+ async createAudioBuffer(voiceParams) {
435
+ const sample = voiceParams.sample;
383
436
  const sampleStart = voiceParams.start;
384
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
385
- if (isSF3) {
386
- const sample = voiceParams.sample;
387
- const start = sample.byteOffset + sampleStart;
388
- const end = sample.byteOffset + sampleEnd;
389
- const buffer = sample.buffer.slice(start, end);
390
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
391
- return audioBuffer;
392
- }
393
- else {
394
- const sample = voiceParams.sample;
395
- const start = sample.byteOffset + sampleStart;
396
- const end = sample.byteOffset + sampleEnd;
397
- const buffer = sample.buffer.slice(start, end);
398
- const audioBuffer = new AudioBuffer({
399
- numberOfChannels: 1,
400
- length: sample.length,
401
- sampleRate: voiceParams.sampleRate,
402
- });
403
- const channelData = audioBuffer.getChannelData(0);
404
- const int16Array = new Int16Array(buffer);
405
- for (let i = 0; i < int16Array.length; i++) {
406
- channelData[i] = int16Array[i] / 32768;
407
- }
408
- return audioBuffer;
409
- }
437
+ const sampleEnd = sample.data.length + voiceParams.end;
438
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
439
+ return audioBuffer;
410
440
  }
411
441
  createBufferSource(channel, voiceParams, audioBuffer) {
412
442
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
@@ -438,10 +468,10 @@ export class MidyGMLite {
438
468
  break;
439
469
  }
440
470
  case "controller":
441
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
471
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
442
472
  break;
443
473
  case "programChange":
444
- this.handleProgramChange(event.channel, event.programNumber, startTime);
474
+ this.setProgramChange(event.channel, event.programNumber, startTime);
445
475
  break;
446
476
  case "pitchBend":
447
477
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -475,7 +505,7 @@ export class MidyGMLite {
475
505
  this.notePromises = [];
476
506
  this.exclusiveClassNotes.fill(undefined);
477
507
  this.drumExclusiveClassNotes.fill(undefined);
478
- this.audioBufferCache.clear();
508
+ this.voiceCache.clear();
479
509
  for (let i = 0; i < this.channels.length; i++) {
480
510
  this.resetAllStates(i);
481
511
  }
@@ -498,7 +528,7 @@ export class MidyGMLite {
498
528
  this.notePromises = [];
499
529
  this.exclusiveClassNotes.fill(undefined);
500
530
  this.drumExclusiveClassNotes.fill(undefined);
501
- this.audioBufferCache.clear();
531
+ this.voiceCache.clear();
502
532
  for (let i = 0; i < this.channels.length; i++) {
503
533
  this.resetAllStates(i);
504
534
  }
@@ -532,11 +562,7 @@ export class MidyGMLite {
532
562
  secondToTicks(second, secondsPerBeat) {
533
563
  return second * this.ticksPerBeat / secondsPerBeat;
534
564
  }
535
- getAudioBufferId(programNumber, noteNumber, velocity) {
536
- return `${programNumber}:${noteNumber}:${velocity}`;
537
- }
538
565
  extractMidiData(midi) {
539
- this.audioBufferCounter.clear();
540
566
  const instruments = new Set();
541
567
  const timeline = [];
542
568
  const tmpChannels = new Array(this.channels.length);
@@ -556,8 +582,6 @@ export class MidyGMLite {
556
582
  switch (event.type) {
557
583
  case "noteOn": {
558
584
  const channel = tmpChannels[event.channel];
559
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
560
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
561
585
  if (channel.programNumber < 0) {
562
586
  instruments.add(`${channel.bank}:0`);
563
587
  channel.programNumber = 0;
@@ -574,10 +598,6 @@ export class MidyGMLite {
574
598
  timeline.push(event);
575
599
  }
576
600
  }
577
- for (const [audioBufferId, count] of this.audioBufferCounter) {
578
- if (count === 1)
579
- this.audioBufferCounter.delete(audioBufferId);
580
- }
581
601
  const priority = {
582
602
  controller: 0,
583
603
  sysEx: 1,
@@ -620,7 +640,6 @@ export class MidyGMLite {
620
640
  this.notePromises.push(promise);
621
641
  promises.push(promise);
622
642
  });
623
- channel.scheduledNotes = [];
624
643
  return Promise.all(promises);
625
644
  }
626
645
  stopNotes(velocity, force, scheduleTime) {
@@ -634,6 +653,8 @@ export class MidyGMLite {
634
653
  if (this.isPlaying || this.isPaused)
635
654
  return;
636
655
  this.resumeTime = 0;
656
+ if (this.voiceCounter.size === 0)
657
+ this.cacheVoiceIds();
637
658
  await this.playNotes();
638
659
  this.isPlaying = false;
639
660
  }
@@ -676,7 +697,7 @@ export class MidyGMLite {
676
697
  }
677
698
  processScheduledNotes(channel, callback) {
678
699
  const scheduledNotes = channel.scheduledNotes;
679
- for (let i = 0; i < scheduledNotes.length; i++) {
700
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
680
701
  const note = scheduledNotes[i];
681
702
  if (!note)
682
703
  continue;
@@ -687,14 +708,14 @@ export class MidyGMLite {
687
708
  }
688
709
  processActiveNotes(channel, scheduleTime, callback) {
689
710
  const scheduledNotes = channel.scheduledNotes;
690
- for (let i = 0; i < scheduledNotes.length; i++) {
711
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
691
712
  const note = scheduledNotes[i];
692
713
  if (!note)
693
714
  continue;
694
715
  if (note.ending)
695
716
  continue;
696
717
  if (scheduleTime < note.startTime)
697
- continue;
718
+ break;
698
719
  callback(note);
699
720
  }
700
721
  }
@@ -809,32 +830,32 @@ export class MidyGMLite {
809
830
  note.modulationLFO.connect(note.volumeDepth);
810
831
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
811
832
  }
812
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
813
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
814
- 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);
815
836
  if (cache) {
816
837
  cache.counter += 1;
817
838
  if (cache.maxCount <= cache.counter) {
818
- this.audioBufferCache.delete(audioBufferId);
839
+ this.voiceCache.delete(audioBufferId);
819
840
  }
820
841
  return cache.audioBuffer;
821
842
  }
822
843
  else {
823
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
824
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
844
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
845
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
825
846
  const cache = { audioBuffer, maxCount, counter: 1 };
826
- this.audioBufferCache.set(audioBufferId, cache);
847
+ this.voiceCache.set(audioBufferId, cache);
827
848
  return audioBuffer;
828
849
  }
829
850
  }
830
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
851
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
831
852
  const now = this.audioContext.currentTime;
832
853
  const state = channel.state;
833
854
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
834
855
  const voiceParams = voice.getAllParams(controllerState);
835
856
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
836
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
837
- note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
857
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
858
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
838
859
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
839
860
  note.filterNode = new BiquadFilterNode(this.audioContext, {
840
861
  type: "lowpass",
@@ -881,19 +902,18 @@ export class MidyGMLite {
881
902
  }
882
903
  this.drumExclusiveClassNotes[index] = note;
883
904
  }
884
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
905
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
885
906
  const channel = this.channels[channelNumber];
886
907
  const bankNumber = channel.bank;
887
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
908
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
909
+ .get(bankNumber);
888
910
  if (soundFontIndex === undefined)
889
911
  return;
890
912
  const soundFont = this.soundFonts[soundFontIndex];
891
913
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
892
914
  if (!voice)
893
915
  return;
894
- const isSF3 = soundFont.parsed.info.version.major === 3;
895
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
896
- note.noteOffEvent = noteOffEvent;
916
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
897
917
  note.volumeEnvelopeNode.connect(channel.gainL);
898
918
  note.volumeEnvelopeNode.connect(channel.gainR);
899
919
  if (0.5 <= channel.state.sustainPedal) {
@@ -948,15 +968,29 @@ export class MidyGMLite {
948
968
  if (0.5 <= channel.state.sustainPedal)
949
969
  return;
950
970
  }
951
- const note = this.findNoteOffTarget(channel, noteNumber);
952
- if (!note)
971
+ const index = this.findNoteOffIndex(channel, noteNumber);
972
+ if (index < 0)
953
973
  return;
974
+ const note = channel.scheduledNotes[index];
954
975
  note.ending = true;
976
+ this.setNoteIndex(channel, index);
955
977
  this.releaseNote(channel, note, endTime);
956
978
  }
957
- 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) {
958
992
  const scheduledNotes = channel.scheduledNotes;
959
- for (let i = 0; i < scheduledNotes.length; i++) {
993
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
960
994
  const note = scheduledNotes[i];
961
995
  if (!note)
962
996
  continue;
@@ -964,8 +998,9 @@ export class MidyGMLite {
964
998
  continue;
965
999
  if (note.noteNumber !== noteNumber)
966
1000
  continue;
967
- return note;
1001
+ return i;
968
1002
  }
1003
+ return -1;
969
1004
  }
970
1005
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
971
1006
  scheduleTime ??= this.audioContext.currentTime;
@@ -991,16 +1026,16 @@ export class MidyGMLite {
991
1026
  case 0x90:
992
1027
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
993
1028
  case 0xB0:
994
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
1029
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
995
1030
  case 0xC0:
996
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
1031
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
997
1032
  case 0xE0:
998
1033
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
999
1034
  default:
1000
1035
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1001
1036
  }
1002
1037
  }
1003
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1038
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
1004
1039
  const channel = this.channels[channelNumber];
1005
1040
  channel.programNumber = programNumber;
1006
1041
  }
@@ -1093,8 +1128,9 @@ export class MidyGMLite {
1093
1128
  this.processScheduledNotes(channel, (note) => {
1094
1129
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1095
1130
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1096
- let appliedFilterEnvelope = false;
1097
- let appliedVolumeEnvelope = false;
1131
+ let applyVolumeEnvelope = false;
1132
+ let applyFilterEnvelope = false;
1133
+ let applyPitchEnvelope = false;
1098
1134
  for (const [key, value] of Object.entries(voiceParams)) {
1099
1135
  const prevValue = note.voiceParams[key];
1100
1136
  if (value === prevValue)
@@ -1103,32 +1139,21 @@ export class MidyGMLite {
1103
1139
  if (key in this.voiceParamsHandlers) {
1104
1140
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1105
1141
  }
1106
- else if (filterEnvelopeKeySet.has(key)) {
1107
- if (appliedFilterEnvelope)
1108
- continue;
1109
- appliedFilterEnvelope = true;
1110
- const noteVoiceParams = note.voiceParams;
1111
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1112
- const key = filterEnvelopeKeys[i];
1113
- if (key in voiceParams)
1114
- noteVoiceParams[key] = voiceParams[key];
1115
- }
1116
- this.setFilterEnvelope(note, scheduleTime);
1117
- this.setPitchEnvelope(note, scheduleTime);
1118
- }
1119
- else if (volumeEnvelopeKeySet.has(key)) {
1120
- if (appliedVolumeEnvelope)
1121
- continue;
1122
- appliedVolumeEnvelope = true;
1123
- const noteVoiceParams = note.voiceParams;
1124
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1125
- const key = volumeEnvelopeKeys[i];
1126
- if (key in voiceParams)
1127
- noteVoiceParams[key] = voiceParams[key];
1128
- }
1129
- this.setVolumeEnvelope(note, scheduleTime);
1142
+ else {
1143
+ if (volumeEnvelopeKeySet.has(key))
1144
+ applyVolumeEnvelope = true;
1145
+ if (filterEnvelopeKeySet.has(key))
1146
+ applyFilterEnvelope = true;
1147
+ if (pitchEnvelopeKeySet.has(key))
1148
+ applyPitchEnvelope = true;
1130
1149
  }
1131
1150
  }
1151
+ if (applyVolumeEnvelope)
1152
+ this.setVolumeEnvelope(note, scheduleTime);
1153
+ if (applyFilterEnvelope)
1154
+ this.setFilterEnvelope(note, scheduleTime);
1155
+ if (applyPitchEnvelope)
1156
+ this.setPitchEnvelope(note, scheduleTime);
1132
1157
  });
1133
1158
  }
1134
1159
  createControlChangeHandlers() {
@@ -1147,7 +1172,7 @@ export class MidyGMLite {
1147
1172
  handlers[123] = this.allNotesOff;
1148
1173
  return handlers;
1149
1174
  }
1150
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1175
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1151
1176
  const handler = this.controlChangeHandlers[controllerType];
1152
1177
  if (handler) {
1153
1178
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1296,7 +1321,7 @@ export class MidyGMLite {
1296
1321
  const entries = Object.entries(defaultControllerState);
1297
1322
  for (const [key, { type, defaultValue }] of entries) {
1298
1323
  if (128 <= type) {
1299
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1324
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1300
1325
  }
1301
1326
  else {
1302
1327
  state[key] = defaultValue;
@@ -1321,7 +1346,7 @@ export class MidyGMLite {
1321
1346
  const key = keys[i];
1322
1347
  const { type, defaultValue } = defaultControllerState[key];
1323
1348
  if (128 <= type) {
1324
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1349
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1325
1350
  }
1326
1351
  else {
1327
1352
  state[key] = defaultValue;
@@ -1434,6 +1459,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
1434
1459
  configurable: true,
1435
1460
  writable: true,
1436
1461
  value: {
1462
+ scheduleIndex: 0,
1437
1463
  detune: 0,
1438
1464
  programNumber: 0,
1439
1465
  bank: 0,