@marmooo/midy 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,7 +53,7 @@ functions.
53
53
  ```js
54
54
  midy.handleMIDIMessage(statusByte, data1, data2, scheduleTime);
55
55
  midy.noteOn(channelNumber, noteNumber, velocity);
56
- midy.handleProgramChange(channelNumber, program);
56
+ midy.setProgramChange(channelNumber, program);
57
57
  ```
58
58
 
59
59
  ### Control Change
@@ -62,7 +62,7 @@ There are functions that handle control changes as they are, as well as
62
62
  simplified functions.
63
63
 
64
64
  ```js
65
- midy.handleControlChange(
65
+ midy.setControlChange(
66
66
  channelNumber,
67
67
  controller,
68
68
  value,
@@ -91,17 +91,25 @@ optimized for playback on the web. The following example loads only the minimum
91
91
  presets required for playback.
92
92
 
93
93
  ```js
94
- const baseUrl = "https://soundfonts.pages.dev/GeneralUser_GS_v1.471";
95
- for (const instrument of midy.instruments) {
96
- const [bankNumber, programNumber] = instrument.split(":").map(Number);
97
- if (midy.soundFontTable[programNumber].has(bankNumber)) continue;
98
- const program = programNumber.toString().padStart(3, "0");
99
- if (bankNumber === 128) {
100
- await midy.loadSoundFont(`${baseUrl}/128.sf3`);
101
- } else {
102
- await midy.loadSoundFont(`${baseUrl}/${program}.sf3`);
94
+ const soundFontURL = "https://soundfonts.pages.dev/GeneralUser_GS_v1.471";
95
+
96
+ function getSoundFontPaths() {
97
+ const paths = [];
98
+ for (const instrument of midy.instruments) {
99
+ const [bankNumber, programNumber] = instrument.split(":").map(Number);
100
+ const table = midy.soundFontTable[programNumber];
101
+ if (table.has(bankNumber)) continue;
102
+ const program = programNumber.toString().padStart(3, "0");
103
+ const path = bankNumber === 128
104
+ ? `${soundFontURL}/128.sf3`
105
+ : `${soundFontURL}/${program}.sf3`;
106
+ paths.push(path);
103
107
  }
108
+ return paths;
104
109
  }
110
+
111
+ const paths = this.getSoundFontPaths();
112
+ await midy.loadSoundFont(paths);
105
113
  ```
106
114
 
107
115
  ## Build
package/esm/midy-GM1.d.ts CHANGED
@@ -23,8 +23,8 @@ export class MidyGM1 {
23
23
  resumeTime: number;
24
24
  soundFonts: any[];
25
25
  soundFontTable: any[];
26
- audioBufferCounter: Map<any, any>;
27
- audioBufferCache: Map<any, any>;
26
+ voiceCounter: Map<any, any>;
27
+ voiceCache: Map<any, any>;
28
28
  isPlaying: boolean;
29
29
  isPausing: boolean;
30
30
  isPaused: boolean;
@@ -54,22 +54,24 @@ export class MidyGM1 {
54
54
  channels: any[];
55
55
  initSoundFontTable(): any[];
56
56
  addSoundFont(soundFont: any): void;
57
+ toUint8Array(input: any): Promise<Uint8Array<ArrayBuffer>>;
57
58
  loadSoundFont(input: any): Promise<void>;
58
59
  loadMIDI(input: any): Promise<void>;
60
+ cacheVoiceIds(): void;
61
+ getVoiceId(channel: any, noteNumber: any, velocity: any): string | undefined;
59
62
  createChannelAudioNodes(audioContext: any): {
60
63
  gainL: any;
61
64
  gainR: any;
62
65
  merger: any;
63
66
  };
64
67
  createChannels(audioContext: any): any[];
65
- createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
68
+ createAudioBuffer(voiceParams: any): Promise<any>;
66
69
  createBufferSource(voiceParams: any, audioBuffer: any): any;
67
70
  scheduleTimelineEvents(t: any, resumeTime: any, queueIndex: any): Promise<any>;
68
71
  getQueueIndex(second: any): number;
69
72
  playNotes(): Promise<any>;
70
73
  ticksToSecond(ticks: any, secondsPerBeat: any): number;
71
74
  secondToTicks(second: any, secondsPerBeat: any): number;
72
- getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
73
75
  extractMidiData(midi: any): {
74
76
  instruments: Set<any>;
75
77
  timeline: any[];
@@ -84,7 +86,7 @@ export class MidyGM1 {
84
86
  seekTo(second: any): void;
85
87
  calcTotalTime(): number;
86
88
  currentTime(): number;
87
- processScheduledNotes(channel: any, scheduleTime: any, callback: any): void;
89
+ processScheduledNotes(channel: any, callback: any): void;
88
90
  processActiveNotes(channel: any, scheduleTime: any, callback: any): void;
89
91
  cbToRatio(cb: any): number;
90
92
  rateToCent(rate: any): number;
@@ -98,19 +100,20 @@ export class MidyGM1 {
98
100
  clampCutoffFrequency(frequency: any): number;
99
101
  setFilterEnvelope(note: any, scheduleTime: any): void;
100
102
  startModulation(channel: any, note: any, scheduleTime: any): void;
101
- getAudioBuffer(programNumber: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
102
- 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>;
103
105
  handleExclusiveClass(note: any, channelNumber: any, startTime: any): void;
104
106
  scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
105
107
  noteOn(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<void>;
106
108
  disconnectNote(note: any): void;
107
109
  releaseNote(channel: any, note: any, endTime: any): Promise<any>;
108
110
  scheduleNoteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): void;
109
- findNoteOffTarget(channel: any, noteNumber: any): any;
111
+ setNoteIndex(channel: any, index: any): void;
112
+ findNoteOffIndex(channel: any, noteNumber: any): any;
110
113
  noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): void;
111
114
  releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): void[];
112
115
  handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
113
- handleProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
116
+ setProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
114
117
  handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
115
118
  setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
116
119
  setModLfoToPitch(channel: any, note: any, scheduleTime: any): void;
@@ -133,7 +136,7 @@ export class MidyGM1 {
133
136
  getControllerState(channel: any, noteNumber: any, velocity: any): Float32Array<any>;
134
137
  applyVoiceParams(channel: any, controllerType: any, scheduleTime: any): void;
135
138
  createControlChangeHandlers(): any[];
136
- handleControlChange(channelNumber: any, controllerType: any, value: any, scheduleTime: any): void;
139
+ setControlChange(channelNumber: any, controllerType: any, value: any, scheduleTime: any): void;
137
140
  updateModulation(channel: any, scheduleTime: any): void;
138
141
  setModulationDepth(channelNumber: any, modulation: any, scheduleTime: any): void;
139
142
  setVolume(channelNumber: any, volume: any, scheduleTime: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AA4FA;IAwBE;;;;;;;;;;;MAWE;IAEF,+BAcC;IAlDD,aAAa;IACb,oBAAiB;IACjB,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,kCAA+B;IAC/B,gCAA6B;IAC7B,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAClB,2BAAqC;IAgBnC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCAWC;IAED,yCAcC;IAED,oCAiBC;IAED;;;;MAeC;IAED,yCAaC;IAED,6DA2BC;IAED,4DASC;IAED,+EAkDC;IAED,mCAOC;IAED,0BA8DC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MA6EC;IAED,kGAeC;IAED,mGAgBC;IAED,wEAMC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,4EASC;IAED,yEASC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,qCAMC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,+GA0BC;IAED,gHAyCC;IAED,0EAiBC;IAED,kGAsCC;IAED,6FASC;IAED,gCASC;IAED,iEAoBC;IAED,qGAaC;IAED,sDASC;IAED,qFASC;IAED,sFAeC;IAED,oGA2BC;IAED,sFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAQC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAgCC;IAED,qCAcC;IAED,kGAWC;IAED,wDAUC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,iEAKC;IAED,uEAQC;IAED,mEAKC;IAED,yEAQC;IAED,gFAGC;IAED,yCAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AAv9CD;IAWE,0FAMC;IAhBD,cAAW;IACX,gBAAe;IACf,kBAAa;IACb,gBAAW;IACX,iBAAY;IACZ,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
1
+ {"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AA4FA;IAwBE;;;;;;;;;;;MAWE;IAEF,+BAcC;IAlDD,aAAa;IACb,oBAAiB;IACjB,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,4BAAyB;IACzB,0BAAuB;IACvB,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAClB,2BAAqC;IAgBnC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCASC;IAED,2DAYC;IAED,yCAmBC;IAED,oCASC;IAED,sBAoCC;IAED,6EAcC;IAED;;;;MAeC;IAED,yCAaC;IAED,kDAUC;IAED,4DASC;IAED,+EAkDC;IAED,mCAOC;IAED,0BAgEC;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,qCAMC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,6FAyBC;IAED,oGAuCC;IAED,0EAiBC;IAED,kGAmCC;IAED,6FASC;IAED,gCASC;IAED,iEAoBC;IAED,qGAeC;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,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,iEAKC;IAED,uEAQC;IAED,mEAKC;IAED,yEAQC;IAED,gFAGC;IAED,yCAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AA//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"}
package/esm/midy-GM1.js CHANGED
@@ -202,13 +202,13 @@ export class MidyGM1 {
202
202
  writable: true,
203
203
  value: this.initSoundFontTable()
204
204
  });
205
- Object.defineProperty(this, "audioBufferCounter", {
205
+ Object.defineProperty(this, "voiceCounter", {
206
206
  enumerable: true,
207
207
  configurable: true,
208
208
  writable: true,
209
209
  value: new Map()
210
210
  });
211
- Object.defineProperty(this, "audioBufferCache", {
211
+ Object.defineProperty(this, "voiceCache", {
212
212
  enumerable: true,
213
213
  configurable: true,
214
214
  writable: true,
@@ -295,13 +295,11 @@ export class MidyGM1 {
295
295
  const presetHeaders = soundFont.parsed.presetHeaders;
296
296
  for (let i = 0; i < presetHeaders.length; i++) {
297
297
  const presetHeader = presetHeaders[i];
298
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
299
- const banks = this.soundFontTable[presetHeader.preset];
300
- banks.set(presetHeader.bank, index);
301
- }
298
+ const banks = this.soundFontTable[presetHeader.preset];
299
+ banks.set(presetHeader.bank, index);
302
300
  }
303
301
  }
304
- async loadSoundFont(input) {
302
+ async toUint8Array(input) {
305
303
  let uint8Array;
306
304
  if (typeof input === "string") {
307
305
  const response = await fetch(input);
@@ -314,23 +312,32 @@ export class MidyGM1 {
314
312
  else {
315
313
  throw new TypeError("input must be a URL string or Uint8Array");
316
314
  }
317
- const parsed = parse(uint8Array);
318
- const soundFont = new SoundFont(parsed);
319
- this.addSoundFont(soundFont);
315
+ return uint8Array;
320
316
  }
321
- async loadMIDI(input) {
322
- let uint8Array;
323
- if (typeof input === "string") {
324
- const response = await fetch(input);
325
- const arrayBuffer = await response.arrayBuffer();
326
- uint8Array = new Uint8Array(arrayBuffer);
327
- }
328
- else if (input instanceof Uint8Array) {
329
- uint8Array = input;
317
+ async loadSoundFont(input) {
318
+ this.voiceCounter.clear();
319
+ if (Array.isArray(input)) {
320
+ const promises = new Array(input.length);
321
+ for (let i = 0; i < input.length; i++) {
322
+ promises[i] = this.toUint8Array(input[i]);
323
+ }
324
+ const uint8Arrays = await Promise.all(promises);
325
+ for (let i = 0; i < uint8Arrays.length; i++) {
326
+ const parsed = parse(uint8Arrays[i]);
327
+ const soundFont = new SoundFont(parsed);
328
+ this.addSoundFont(soundFont);
329
+ }
330
330
  }
331
331
  else {
332
- throw new TypeError("input must be a URL string or Uint8Array");
332
+ const uint8Array = await this.toUint8Array(input);
333
+ const parsed = parse(uint8Array);
334
+ const soundFont = new SoundFont(parsed);
335
+ this.addSoundFont(soundFont);
333
336
  }
337
+ }
338
+ async loadMIDI(input) {
339
+ this.voiceCounter.clear();
340
+ const uint8Array = await this.toUint8Array(input);
334
341
  const midi = parseMidi(uint8Array);
335
342
  this.ticksPerBeat = midi.header.ticksPerBeat;
336
343
  const midiData = this.extractMidiData(midi);
@@ -338,6 +345,45 @@ export class MidyGM1 {
338
345
  this.timeline = midiData.timeline;
339
346
  this.totalTime = this.calcTotalTime();
340
347
  }
348
+ cacheVoiceIds() {
349
+ const timeline = this.timeline;
350
+ for (let i = 0; i < timeline.length; i++) {
351
+ const event = timeline[i];
352
+ switch (event.type) {
353
+ case "noteOn": {
354
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
355
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
356
+ break;
357
+ }
358
+ case "controller":
359
+ if (event.controllerType === 0) {
360
+ this.setBankMSB(event.channel, event.value);
361
+ }
362
+ else if (event.controllerType === 32) {
363
+ this.setBankLSB(event.channel, event.value);
364
+ }
365
+ break;
366
+ case "programChange":
367
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
368
+ }
369
+ }
370
+ for (const [audioBufferId, count] of this.voiceCounter) {
371
+ if (count === 1)
372
+ this.voiceCounter.delete(audioBufferId);
373
+ }
374
+ this.GM1SystemOn();
375
+ }
376
+ getVoiceId(channel, noteNumber, velocity) {
377
+ const bankNumber = this.calcBank(channel);
378
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
379
+ .get(bankNumber);
380
+ if (soundFontIndex === undefined)
381
+ return;
382
+ const soundFont = this.soundFonts[soundFontIndex];
383
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
384
+ const { instrument, sampleID } = voice.generators;
385
+ return `${soundFontIndex}:${instrument}:${sampleID}`;
386
+ }
341
387
  createChannelAudioNodes(audioContext) {
342
388
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
343
389
  const gainL = new GainNode(audioContext, { gain: gainLeft });
@@ -366,34 +412,12 @@ export class MidyGM1 {
366
412
  });
367
413
  return channels;
368
414
  }
369
- async createNoteBuffer(voiceParams, isSF3) {
415
+ async createAudioBuffer(voiceParams) {
416
+ const sample = voiceParams.sample;
370
417
  const sampleStart = voiceParams.start;
371
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
372
- if (isSF3) {
373
- const sample = voiceParams.sample;
374
- const start = sample.byteOffset + sampleStart;
375
- const end = sample.byteOffset + sampleEnd;
376
- const buffer = sample.buffer.slice(start, end);
377
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
378
- return audioBuffer;
379
- }
380
- else {
381
- const sample = voiceParams.sample;
382
- const start = sample.byteOffset + sampleStart;
383
- const end = sample.byteOffset + sampleEnd;
384
- const buffer = sample.buffer.slice(start, end);
385
- const audioBuffer = new AudioBuffer({
386
- numberOfChannels: 1,
387
- length: sample.length,
388
- sampleRate: voiceParams.sampleRate,
389
- });
390
- const channelData = audioBuffer.getChannelData(0);
391
- const int16Array = new Int16Array(buffer);
392
- for (let i = 0; i < int16Array.length; i++) {
393
- channelData[i] = int16Array[i] / 32768;
394
- }
395
- return audioBuffer;
396
- }
418
+ const sampleEnd = sample.data.length + voiceParams.end;
419
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
420
+ return audioBuffer;
397
421
  }
398
422
  createBufferSource(voiceParams, audioBuffer) {
399
423
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
@@ -423,10 +447,10 @@ export class MidyGM1 {
423
447
  break;
424
448
  }
425
449
  case "controller":
426
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
450
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
427
451
  break;
428
452
  case "programChange":
429
- this.handleProgramChange(event.channel, event.programNumber, startTime);
453
+ this.setProgramChange(event.channel, event.programNumber, startTime);
430
454
  break;
431
455
  case "pitchBend":
432
456
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -459,8 +483,9 @@ export class MidyGM1 {
459
483
  await Promise.all(this.notePromises);
460
484
  this.notePromises = [];
461
485
  this.exclusiveClassNotes.fill(undefined);
462
- this.audioBufferCache.clear();
486
+ this.voiceCache.clear();
463
487
  for (let i = 0; i < this.channels.length; i++) {
488
+ this.channels[i].scheduledNotes = [];
464
489
  this.resetAllStates(i);
465
490
  }
466
491
  resolve();
@@ -481,8 +506,9 @@ export class MidyGM1 {
481
506
  await this.stopNotes(0, true, now);
482
507
  this.notePromises = [];
483
508
  this.exclusiveClassNotes.fill(undefined);
484
- this.audioBufferCache.clear();
509
+ this.voiceCache.clear();
485
510
  for (let i = 0; i < this.channels.length; i++) {
511
+ this.channels[i].scheduledNotes = [];
486
512
  this.resetAllStates(i);
487
513
  }
488
514
  this.isStopping = false;
@@ -514,11 +540,7 @@ export class MidyGM1 {
514
540
  secondToTicks(second, secondsPerBeat) {
515
541
  return second * this.ticksPerBeat / secondsPerBeat;
516
542
  }
517
- getAudioBufferId(programNumber, noteNumber, velocity) {
518
- return `${programNumber}:${noteNumber}:${velocity}`;
519
- }
520
543
  extractMidiData(midi) {
521
- this.audioBufferCounter.clear();
522
544
  const instruments = new Set();
523
545
  const timeline = [];
524
546
  const tmpChannels = new Array(this.channels.length);
@@ -538,8 +560,6 @@ export class MidyGM1 {
538
560
  switch (event.type) {
539
561
  case "noteOn": {
540
562
  const channel = tmpChannels[event.channel];
541
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
542
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
543
563
  if (channel.programNumber < 0) {
544
564
  instruments.add(`${channel.bank}:0`);
545
565
  channel.programNumber = 0;
@@ -556,10 +576,6 @@ export class MidyGM1 {
556
576
  timeline.push(event);
557
577
  }
558
578
  }
559
- for (const [audioBufferId, count] of this.audioBufferCounter) {
560
- if (count === 1)
561
- this.audioBufferCounter.delete(audioBufferId);
562
- }
563
579
  const priority = {
564
580
  controller: 0,
565
581
  sysEx: 1,
@@ -597,12 +613,11 @@ export class MidyGM1 {
597
613
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
598
614
  const channel = this.channels[channelNumber];
599
615
  const promises = [];
600
- this.processScheduledNotes(channel, scheduleTime, (note) => {
616
+ this.processScheduledNotes(channel, (note) => {
601
617
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
602
618
  this.notePromises.push(promise);
603
619
  promises.push(promise);
604
620
  });
605
- channel.scheduledNotes = [];
606
621
  return Promise.all(promises);
607
622
  }
608
623
  stopNotes(velocity, force, scheduleTime) {
@@ -616,6 +631,8 @@ export class MidyGM1 {
616
631
  if (this.isPlaying || this.isPaused)
617
632
  return;
618
633
  this.resumeTime = 0;
634
+ if (this.voiceCounter.size === 0)
635
+ this.cacheVoiceIds();
619
636
  await this.playNotes();
620
637
  this.isPlaying = false;
621
638
  }
@@ -656,22 +673,20 @@ export class MidyGM1 {
656
673
  const now = this.audioContext.currentTime;
657
674
  return this.resumeTime + now - this.startTime - this.startDelay;
658
675
  }
659
- processScheduledNotes(channel, scheduleTime, callback) {
676
+ processScheduledNotes(channel, callback) {
660
677
  const scheduledNotes = channel.scheduledNotes;
661
- for (let i = 0; i < scheduledNotes.length; i++) {
678
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
662
679
  const note = scheduledNotes[i];
663
680
  if (!note)
664
681
  continue;
665
682
  if (note.ending)
666
683
  continue;
667
- if (note.startTime < scheduleTime)
668
- continue;
669
684
  callback(note);
670
685
  }
671
686
  }
672
687
  processActiveNotes(channel, scheduleTime, callback) {
673
688
  const scheduledNotes = channel.scheduledNotes;
674
- for (let i = 0; i < scheduledNotes.length; i++) {
689
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
675
690
  const note = scheduledNotes[i];
676
691
  if (!note)
677
692
  continue;
@@ -702,7 +717,7 @@ export class MidyGM1 {
702
717
  return tuning + pitch;
703
718
  }
704
719
  updateChannelDetune(channel, scheduleTime) {
705
- this.processScheduledNotes(channel, scheduleTime, (note) => {
720
+ this.processScheduledNotes(channel, (note) => {
706
721
  this.updateDetune(channel, note, scheduleTime);
707
722
  });
708
723
  }
@@ -795,31 +810,31 @@ export class MidyGM1 {
795
810
  note.modulationLFO.connect(note.volumeDepth);
796
811
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
797
812
  }
798
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
799
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
800
- const cache = this.audioBufferCache.get(audioBufferId);
813
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
814
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
815
+ const cache = this.voiceCache.get(audioBufferId);
801
816
  if (cache) {
802
817
  cache.counter += 1;
803
818
  if (cache.maxCount <= cache.counter) {
804
- this.audioBufferCache.delete(audioBufferId);
819
+ this.voiceCache.delete(audioBufferId);
805
820
  }
806
821
  return cache.audioBuffer;
807
822
  }
808
823
  else {
809
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
810
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
824
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
825
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
811
826
  const cache = { audioBuffer, maxCount, counter: 1 };
812
- this.audioBufferCache.set(audioBufferId, cache);
827
+ this.voiceCache.set(audioBufferId, cache);
813
828
  return audioBuffer;
814
829
  }
815
830
  }
816
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
831
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
817
832
  const now = this.audioContext.currentTime;
818
833
  const state = channel.state;
819
834
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
820
835
  const voiceParams = voice.getAllParams(controllerState);
821
836
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
822
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
837
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
823
838
  note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
824
839
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
825
840
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -855,15 +870,15 @@ export class MidyGM1 {
855
870
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
856
871
  const channel = this.channels[channelNumber];
857
872
  const bankNumber = channel.bank;
858
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
873
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
874
+ .get(bankNumber);
859
875
  if (soundFontIndex === undefined)
860
876
  return;
861
877
  const soundFont = this.soundFonts[soundFontIndex];
862
878
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
863
879
  if (!voice)
864
880
  return;
865
- const isSF3 = soundFont.parsed.info.version.major === 3;
866
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
881
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
867
882
  note.volumeEnvelopeNode.connect(channel.gainL);
868
883
  note.volumeEnvelopeNode.connect(channel.gainR);
869
884
  if (0.5 <= channel.state.sustainPedal) {
@@ -913,15 +928,29 @@ export class MidyGM1 {
913
928
  const channel = this.channels[channelNumber];
914
929
  if (!force && 0.5 <= channel.state.sustainPedal)
915
930
  return;
916
- const note = this.findNoteOffTarget(channel, noteNumber);
917
- if (!note)
931
+ const index = this.findNoteOffIndex(channel, noteNumber);
932
+ if (index < 0)
918
933
  return;
934
+ const note = channel.scheduledNotes[index];
919
935
  note.ending = true;
936
+ this.setNoteIndex(channel, index);
920
937
  this.releaseNote(channel, note, endTime);
921
938
  }
922
- findNoteOffTarget(channel, noteNumber) {
939
+ setNoteIndex(channel, index) {
940
+ let allEnds = true;
941
+ for (let i = channel.scheduleIndex; i < index; i++) {
942
+ const note = channel.scheduledNotes[i];
943
+ if (note && !note.ending) {
944
+ allEnds = false;
945
+ break;
946
+ }
947
+ }
948
+ if (allEnds)
949
+ channel.scheduleIndex = index + 1;
950
+ }
951
+ findNoteOffIndex(channel, noteNumber) {
923
952
  const scheduledNotes = channel.scheduledNotes;
924
- for (let i = 0; i < scheduledNotes.length; i++) {
953
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
925
954
  const note = scheduledNotes[i];
926
955
  if (!note)
927
956
  continue;
@@ -929,8 +958,9 @@ export class MidyGM1 {
929
958
  continue;
930
959
  if (note.noteNumber !== noteNumber)
931
960
  continue;
932
- return note;
961
+ return i;
933
962
  }
963
+ return -1;
934
964
  }
935
965
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
936
966
  scheduleTime ??= this.audioContext.currentTime;
@@ -956,16 +986,16 @@ export class MidyGM1 {
956
986
  case 0x90:
957
987
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
958
988
  case 0xB0:
959
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
989
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
960
990
  case 0xC0:
961
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
991
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
962
992
  case 0xE0:
963
993
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
964
994
  default:
965
995
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
966
996
  }
967
997
  }
968
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
998
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
969
999
  const channel = this.channels[channelNumber];
970
1000
  channel.programNumber = programNumber;
971
1001
  }
@@ -1055,7 +1085,7 @@ export class MidyGM1 {
1055
1085
  return state;
1056
1086
  }
1057
1087
  applyVoiceParams(channel, controllerType, scheduleTime) {
1058
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1088
+ this.processScheduledNotes(channel, (note) => {
1059
1089
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1060
1090
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1061
1091
  let applyVolumeEnvelope = false;
@@ -1099,9 +1129,10 @@ export class MidyGM1 {
1099
1129
  handlers[101] = this.setRPNMSB;
1100
1130
  handlers[120] = this.allSoundOff;
1101
1131
  handlers[121] = this.resetAllControllers;
1132
+ handlers[123] = this.allNotesOff;
1102
1133
  return handlers;
1103
1134
  }
1104
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1135
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1105
1136
  const handler = this.controlChangeHandlers[controllerType];
1106
1137
  if (handler) {
1107
1138
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1114,7 +1145,7 @@ export class MidyGM1 {
1114
1145
  }
1115
1146
  updateModulation(channel, scheduleTime) {
1116
1147
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1117
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1148
+ this.processScheduledNotes(channel, (note) => {
1118
1149
  if (note.modulationDepth) {
1119
1150
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1120
1151
  }
@@ -1175,7 +1206,7 @@ export class MidyGM1 {
1175
1206
  scheduleTime ??= this.audioContext.currentTime;
1176
1207
  channel.state.sustainPedal = value / 127;
1177
1208
  if (64 <= value) {
1178
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1209
+ this.processScheduledNotes(channel, (note) => {
1179
1210
  channel.sustainNotes.push(note);
1180
1211
  });
1181
1212
  }
@@ -1294,7 +1325,7 @@ export class MidyGM1 {
1294
1325
  const entries = Object.entries(defaultControllerState);
1295
1326
  for (const [key, { type, defaultValue }] of entries) {
1296
1327
  if (128 <= type) {
1297
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1328
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1298
1329
  }
1299
1330
  else {
1300
1331
  state[key] = defaultValue;
@@ -1319,7 +1350,7 @@ export class MidyGM1 {
1319
1350
  const key = keys[i];
1320
1351
  const { type, defaultValue } = defaultControllerState[key];
1321
1352
  if (128 <= type) {
1322
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1353
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1323
1354
  }
1324
1355
  else {
1325
1356
  state[key] = defaultValue;