@marmooo/midy 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marmooo/midy",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "A MIDI player/synthesizer written in JavaScript that supports GM-Lite/GM1 and SF2/SF3.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,7 @@
22
22
  "test": "node test_runner.js"
23
23
  },
24
24
  "dependencies": {
25
- "@marmooo/soundfont-parser": "^0.1.1",
25
+ "@marmooo/soundfont-parser": "^0.1.3",
26
26
  "midi-file": "^1.2.4"
27
27
  },
28
28
  "devDependencies": {
@@ -23,16 +23,16 @@ 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;
31
31
  isStopping: boolean;
32
32
  isSeeking: boolean;
33
33
  timeline: any[];
34
- instruments: any[];
35
34
  notePromises: any[];
35
+ instruments: Set<any>;
36
36
  exclusiveClassNotes: any[];
37
37
  audioContext: any;
38
38
  masterVolume: any;
@@ -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): any;
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,oBAAkB;IAClB,sBAAwB;IACxB,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,8DAcC;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,mEAWC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAgCC;IAED,qCAeC;IAED,+FAWC;IAED,wDASC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,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;AAjgDD;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"}
@@ -205,13 +205,13 @@ class MidyGM1 {
205
205
  writable: true,
206
206
  value: this.initSoundFontTable()
207
207
  });
208
- Object.defineProperty(this, "audioBufferCounter", {
208
+ Object.defineProperty(this, "voiceCounter", {
209
209
  enumerable: true,
210
210
  configurable: true,
211
211
  writable: true,
212
212
  value: new Map()
213
213
  });
214
- Object.defineProperty(this, "audioBufferCache", {
214
+ Object.defineProperty(this, "voiceCache", {
215
215
  enumerable: true,
216
216
  configurable: true,
217
217
  writable: true,
@@ -253,17 +253,17 @@ class MidyGM1 {
253
253
  writable: true,
254
254
  value: []
255
255
  });
256
- Object.defineProperty(this, "instruments", {
256
+ Object.defineProperty(this, "notePromises", {
257
257
  enumerable: true,
258
258
  configurable: true,
259
259
  writable: true,
260
260
  value: []
261
261
  });
262
- Object.defineProperty(this, "notePromises", {
262
+ Object.defineProperty(this, "instruments", {
263
263
  enumerable: true,
264
264
  configurable: true,
265
265
  writable: true,
266
- value: []
266
+ value: new Set()
267
267
  });
268
268
  Object.defineProperty(this, "exclusiveClassNotes", {
269
269
  enumerable: true,
@@ -298,13 +298,11 @@ class MidyGM1 {
298
298
  const presetHeaders = soundFont.parsed.presetHeaders;
299
299
  for (let i = 0; i < presetHeaders.length; i++) {
300
300
  const presetHeader = presetHeaders[i];
301
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
302
- const banks = this.soundFontTable[presetHeader.preset];
303
- banks.set(presetHeader.bank, index);
304
- }
301
+ const banks = this.soundFontTable[presetHeader.preset];
302
+ banks.set(presetHeader.bank, index);
305
303
  }
306
304
  }
307
- async loadSoundFont(input) {
305
+ async toUint8Array(input) {
308
306
  let uint8Array;
309
307
  if (typeof input === "string") {
310
308
  const response = await fetch(input);
@@ -317,23 +315,32 @@ class MidyGM1 {
317
315
  else {
318
316
  throw new TypeError("input must be a URL string or Uint8Array");
319
317
  }
320
- const parsed = (0, soundfont_parser_1.parse)(uint8Array);
321
- const soundFont = new soundfont_parser_1.SoundFont(parsed);
322
- this.addSoundFont(soundFont);
318
+ return uint8Array;
323
319
  }
324
- async loadMIDI(input) {
325
- let uint8Array;
326
- if (typeof input === "string") {
327
- const response = await fetch(input);
328
- const arrayBuffer = await response.arrayBuffer();
329
- uint8Array = new Uint8Array(arrayBuffer);
330
- }
331
- else if (input instanceof Uint8Array) {
332
- uint8Array = input;
320
+ async loadSoundFont(input) {
321
+ this.voiceCounter.clear();
322
+ if (Array.isArray(input)) {
323
+ const promises = new Array(input.length);
324
+ for (let i = 0; i < input.length; i++) {
325
+ promises[i] = this.toUint8Array(input[i]);
326
+ }
327
+ const uint8Arrays = await Promise.all(promises);
328
+ for (let i = 0; i < uint8Arrays.length; i++) {
329
+ const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
330
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
331
+ this.addSoundFont(soundFont);
332
+ }
333
333
  }
334
334
  else {
335
- throw new TypeError("input must be a URL string or Uint8Array");
335
+ const uint8Array = await this.toUint8Array(input);
336
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
337
+ const soundFont = new soundfont_parser_1.SoundFont(parsed);
338
+ this.addSoundFont(soundFont);
336
339
  }
340
+ }
341
+ async loadMIDI(input) {
342
+ this.voiceCounter.clear();
343
+ const uint8Array = await this.toUint8Array(input);
337
344
  const midi = (0, midi_file_1.parseMidi)(uint8Array);
338
345
  this.ticksPerBeat = midi.header.ticksPerBeat;
339
346
  const midiData = this.extractMidiData(midi);
@@ -341,6 +348,45 @@ class MidyGM1 {
341
348
  this.timeline = midiData.timeline;
342
349
  this.totalTime = this.calcTotalTime();
343
350
  }
351
+ cacheVoiceIds() {
352
+ const timeline = this.timeline;
353
+ for (let i = 0; i < timeline.length; i++) {
354
+ const event = timeline[i];
355
+ switch (event.type) {
356
+ case "noteOn": {
357
+ const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
358
+ this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
359
+ break;
360
+ }
361
+ case "controller":
362
+ if (event.controllerType === 0) {
363
+ this.setBankMSB(event.channel, event.value);
364
+ }
365
+ else if (event.controllerType === 32) {
366
+ this.setBankLSB(event.channel, event.value);
367
+ }
368
+ break;
369
+ case "programChange":
370
+ this.setProgramChange(event.channel, event.programNumber, event.startTime);
371
+ }
372
+ }
373
+ for (const [audioBufferId, count] of this.voiceCounter) {
374
+ if (count === 1)
375
+ this.voiceCounter.delete(audioBufferId);
376
+ }
377
+ this.GM1SystemOn();
378
+ }
379
+ getVoiceId(channel, noteNumber, velocity) {
380
+ const bankNumber = this.calcBank(channel);
381
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
382
+ .get(bankNumber);
383
+ if (soundFontIndex === undefined)
384
+ return;
385
+ const soundFont = this.soundFonts[soundFontIndex];
386
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
387
+ const { instrument, sampleID } = voice.generators;
388
+ return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
389
+ }
344
390
  createChannelAudioNodes(audioContext) {
345
391
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
346
392
  const gainL = new GainNode(audioContext, { gain: gainLeft });
@@ -369,34 +415,12 @@ class MidyGM1 {
369
415
  });
370
416
  return channels;
371
417
  }
372
- async createNoteBuffer(voiceParams, isSF3) {
418
+ async createAudioBuffer(voiceParams) {
419
+ const sample = voiceParams.sample;
373
420
  const sampleStart = voiceParams.start;
374
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
375
- if (isSF3) {
376
- const sample = voiceParams.sample;
377
- const start = sample.byteOffset + sampleStart;
378
- const end = sample.byteOffset + sampleEnd;
379
- const buffer = sample.buffer.slice(start, end);
380
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
381
- return audioBuffer;
382
- }
383
- else {
384
- const sample = voiceParams.sample;
385
- const start = sample.byteOffset + sampleStart;
386
- const end = sample.byteOffset + sampleEnd;
387
- const buffer = sample.buffer.slice(start, end);
388
- const audioBuffer = new AudioBuffer({
389
- numberOfChannels: 1,
390
- length: sample.length,
391
- sampleRate: voiceParams.sampleRate,
392
- });
393
- const channelData = audioBuffer.getChannelData(0);
394
- const int16Array = new Int16Array(buffer);
395
- for (let i = 0; i < int16Array.length; i++) {
396
- channelData[i] = int16Array[i] / 32768;
397
- }
398
- return audioBuffer;
399
- }
421
+ const sampleEnd = sample.data.length + voiceParams.end;
422
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
423
+ return audioBuffer;
400
424
  }
401
425
  createBufferSource(voiceParams, audioBuffer) {
402
426
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
@@ -426,10 +450,10 @@ class MidyGM1 {
426
450
  break;
427
451
  }
428
452
  case "controller":
429
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
453
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
430
454
  break;
431
455
  case "programChange":
432
- this.handleProgramChange(event.channel, event.programNumber, startTime);
456
+ this.setProgramChange(event.channel, event.programNumber, startTime);
433
457
  break;
434
458
  case "pitchBend":
435
459
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -462,8 +486,9 @@ class MidyGM1 {
462
486
  await Promise.all(this.notePromises);
463
487
  this.notePromises = [];
464
488
  this.exclusiveClassNotes.fill(undefined);
465
- this.audioBufferCache.clear();
489
+ this.voiceCache.clear();
466
490
  for (let i = 0; i < this.channels.length; i++) {
491
+ this.channels[i].scheduledNotes = [];
467
492
  this.resetAllStates(i);
468
493
  }
469
494
  resolve();
@@ -484,8 +509,9 @@ class MidyGM1 {
484
509
  await this.stopNotes(0, true, now);
485
510
  this.notePromises = [];
486
511
  this.exclusiveClassNotes.fill(undefined);
487
- this.audioBufferCache.clear();
512
+ this.voiceCache.clear();
488
513
  for (let i = 0; i < this.channels.length; i++) {
514
+ this.channels[i].scheduledNotes = [];
489
515
  this.resetAllStates(i);
490
516
  }
491
517
  this.isStopping = false;
@@ -517,11 +543,7 @@ class MidyGM1 {
517
543
  secondToTicks(second, secondsPerBeat) {
518
544
  return second * this.ticksPerBeat / secondsPerBeat;
519
545
  }
520
- getAudioBufferId(programNumber, noteNumber, velocity) {
521
- return `${programNumber}:${noteNumber}:${velocity}`;
522
- }
523
546
  extractMidiData(midi) {
524
- this.audioBufferCounter.clear();
525
547
  const instruments = new Set();
526
548
  const timeline = [];
527
549
  const tmpChannels = new Array(this.channels.length);
@@ -541,8 +563,6 @@ class MidyGM1 {
541
563
  switch (event.type) {
542
564
  case "noteOn": {
543
565
  const channel = tmpChannels[event.channel];
544
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
545
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
546
566
  if (channel.programNumber < 0) {
547
567
  instruments.add(`${channel.bank}:0`);
548
568
  channel.programNumber = 0;
@@ -559,10 +579,6 @@ class MidyGM1 {
559
579
  timeline.push(event);
560
580
  }
561
581
  }
562
- for (const [audioBufferId, count] of this.audioBufferCounter) {
563
- if (count === 1)
564
- this.audioBufferCounter.delete(audioBufferId);
565
- }
566
582
  const priority = {
567
583
  controller: 0,
568
584
  sysEx: 1,
@@ -600,12 +616,11 @@ class MidyGM1 {
600
616
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
601
617
  const channel = this.channels[channelNumber];
602
618
  const promises = [];
603
- this.processScheduledNotes(channel, scheduleTime, (note) => {
619
+ this.processScheduledNotes(channel, (note) => {
604
620
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
605
621
  this.notePromises.push(promise);
606
622
  promises.push(promise);
607
623
  });
608
- channel.scheduledNotes = [];
609
624
  return Promise.all(promises);
610
625
  }
611
626
  stopNotes(velocity, force, scheduleTime) {
@@ -619,6 +634,8 @@ class MidyGM1 {
619
634
  if (this.isPlaying || this.isPaused)
620
635
  return;
621
636
  this.resumeTime = 0;
637
+ if (this.voiceCounter.size === 0)
638
+ this.cacheVoiceIds();
622
639
  await this.playNotes();
623
640
  this.isPlaying = false;
624
641
  }
@@ -659,22 +676,20 @@ class MidyGM1 {
659
676
  const now = this.audioContext.currentTime;
660
677
  return this.resumeTime + now - this.startTime - this.startDelay;
661
678
  }
662
- processScheduledNotes(channel, scheduleTime, callback) {
679
+ processScheduledNotes(channel, callback) {
663
680
  const scheduledNotes = channel.scheduledNotes;
664
- for (let i = 0; i < scheduledNotes.length; i++) {
681
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
665
682
  const note = scheduledNotes[i];
666
683
  if (!note)
667
684
  continue;
668
685
  if (note.ending)
669
686
  continue;
670
- if (note.startTime < scheduleTime)
671
- continue;
672
687
  callback(note);
673
688
  }
674
689
  }
675
690
  processActiveNotes(channel, scheduleTime, callback) {
676
691
  const scheduledNotes = channel.scheduledNotes;
677
- for (let i = 0; i < scheduledNotes.length; i++) {
692
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
678
693
  const note = scheduledNotes[i];
679
694
  if (!note)
680
695
  continue;
@@ -705,7 +720,7 @@ class MidyGM1 {
705
720
  return tuning + pitch;
706
721
  }
707
722
  updateChannelDetune(channel, scheduleTime) {
708
- this.processScheduledNotes(channel, scheduleTime, (note) => {
723
+ this.processScheduledNotes(channel, (note) => {
709
724
  this.updateDetune(channel, note, scheduleTime);
710
725
  });
711
726
  }
@@ -798,31 +813,31 @@ class MidyGM1 {
798
813
  note.modulationLFO.connect(note.volumeDepth);
799
814
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
800
815
  }
801
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
802
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
803
- const cache = this.audioBufferCache.get(audioBufferId);
816
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
817
+ const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
818
+ const cache = this.voiceCache.get(audioBufferId);
804
819
  if (cache) {
805
820
  cache.counter += 1;
806
821
  if (cache.maxCount <= cache.counter) {
807
- this.audioBufferCache.delete(audioBufferId);
822
+ this.voiceCache.delete(audioBufferId);
808
823
  }
809
824
  return cache.audioBuffer;
810
825
  }
811
826
  else {
812
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
813
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
827
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
828
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
814
829
  const cache = { audioBuffer, maxCount, counter: 1 };
815
- this.audioBufferCache.set(audioBufferId, cache);
830
+ this.voiceCache.set(audioBufferId, cache);
816
831
  return audioBuffer;
817
832
  }
818
833
  }
819
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
834
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
820
835
  const now = this.audioContext.currentTime;
821
836
  const state = channel.state;
822
837
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
823
838
  const voiceParams = voice.getAllParams(controllerState);
824
839
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
825
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
840
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
826
841
  note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
827
842
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
828
843
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -858,15 +873,15 @@ class MidyGM1 {
858
873
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
859
874
  const channel = this.channels[channelNumber];
860
875
  const bankNumber = channel.bank;
861
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
876
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
877
+ .get(bankNumber);
862
878
  if (soundFontIndex === undefined)
863
879
  return;
864
880
  const soundFont = this.soundFonts[soundFontIndex];
865
881
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
866
882
  if (!voice)
867
883
  return;
868
- const isSF3 = soundFont.parsed.info.version.major === 3;
869
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
884
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
870
885
  note.volumeEnvelopeNode.connect(channel.gainL);
871
886
  note.volumeEnvelopeNode.connect(channel.gainR);
872
887
  if (0.5 <= channel.state.sustainPedal) {
@@ -916,15 +931,29 @@ class MidyGM1 {
916
931
  const channel = this.channels[channelNumber];
917
932
  if (!force && 0.5 <= channel.state.sustainPedal)
918
933
  return;
919
- const note = this.findNoteOffTarget(channel, noteNumber);
920
- if (!note)
934
+ const index = this.findNoteOffIndex(channel, noteNumber);
935
+ if (index < 0)
921
936
  return;
937
+ const note = channel.scheduledNotes[index];
922
938
  note.ending = true;
939
+ this.setNoteIndex(channel, index);
923
940
  this.releaseNote(channel, note, endTime);
924
941
  }
925
- findNoteOffTarget(channel, noteNumber) {
942
+ setNoteIndex(channel, index) {
943
+ let allEnds = true;
944
+ for (let i = channel.scheduleIndex; i < index; i++) {
945
+ const note = channel.scheduledNotes[i];
946
+ if (note && !note.ending) {
947
+ allEnds = false;
948
+ break;
949
+ }
950
+ }
951
+ if (allEnds)
952
+ channel.scheduleIndex = index + 1;
953
+ }
954
+ findNoteOffIndex(channel, noteNumber) {
926
955
  const scheduledNotes = channel.scheduledNotes;
927
- for (let i = 0; i < scheduledNotes.length; i++) {
956
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
928
957
  const note = scheduledNotes[i];
929
958
  if (!note)
930
959
  continue;
@@ -932,8 +961,9 @@ class MidyGM1 {
932
961
  continue;
933
962
  if (note.noteNumber !== noteNumber)
934
963
  continue;
935
- return note;
964
+ return i;
936
965
  }
966
+ return -1;
937
967
  }
938
968
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
939
969
  scheduleTime ??= this.audioContext.currentTime;
@@ -959,16 +989,16 @@ class MidyGM1 {
959
989
  case 0x90:
960
990
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
961
991
  case 0xB0:
962
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
992
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
963
993
  case 0xC0:
964
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
994
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
965
995
  case 0xE0:
966
996
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
967
997
  default:
968
998
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
969
999
  }
970
1000
  }
971
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1001
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
972
1002
  const channel = this.channels[channelNumber];
973
1003
  channel.programNumber = programNumber;
974
1004
  }
@@ -988,13 +1018,17 @@ class MidyGM1 {
988
1018
  this.applyVoiceParams(channel, 14, scheduleTime);
989
1019
  }
990
1020
  setModLfoToPitch(channel, note, scheduleTime) {
991
- const modLfoToPitch = note.voiceParams.modLfoToPitch;
992
- const baseDepth = Math.abs(modLfoToPitch) +
993
- channel.state.modulationDepth;
994
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
995
- note.modulationDepth.gain
996
- .cancelScheduledValues(scheduleTime)
997
- .setValueAtTime(modulationDepth, scheduleTime);
1021
+ if (note.modulationDepth) {
1022
+ const modLfoToPitch = note.voiceParams.modLfoToPitch;
1023
+ const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1024
+ const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1025
+ note.modulationDepth.gain
1026
+ .cancelScheduledValues(scheduleTime)
1027
+ .setValueAtTime(modulationDepth, scheduleTime);
1028
+ }
1029
+ else {
1030
+ this.startModulation(channel, note, scheduleTime);
1031
+ }
998
1032
  }
999
1033
  setModLfoToFilterFc(note, scheduleTime) {
1000
1034
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
@@ -1058,7 +1092,7 @@ class MidyGM1 {
1058
1092
  return state;
1059
1093
  }
1060
1094
  applyVoiceParams(channel, controllerType, scheduleTime) {
1061
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1095
+ this.processScheduledNotes(channel, (note) => {
1062
1096
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1063
1097
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1064
1098
  let applyVolumeEnvelope = false;
@@ -1102,9 +1136,10 @@ class MidyGM1 {
1102
1136
  handlers[101] = this.setRPNMSB;
1103
1137
  handlers[120] = this.allSoundOff;
1104
1138
  handlers[121] = this.resetAllControllers;
1139
+ handlers[123] = this.allNotesOff;
1105
1140
  return handlers;
1106
1141
  }
1107
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1142
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1108
1143
  const handler = this.controlChangeHandlers[controllerType];
1109
1144
  if (handler) {
1110
1145
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1117,12 +1152,11 @@ class MidyGM1 {
1117
1152
  }
1118
1153
  updateModulation(channel, scheduleTime) {
1119
1154
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1120
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1155
+ this.processScheduledNotes(channel, (note) => {
1121
1156
  if (note.modulationDepth) {
1122
1157
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1123
1158
  }
1124
1159
  else {
1125
- this.setPitchEnvelope(note, scheduleTime);
1126
1160
  this.startModulation(channel, note, scheduleTime);
1127
1161
  }
1128
1162
  });
@@ -1178,7 +1212,7 @@ class MidyGM1 {
1178
1212
  scheduleTime ??= this.audioContext.currentTime;
1179
1213
  channel.state.sustainPedal = value / 127;
1180
1214
  if (64 <= value) {
1181
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1215
+ this.processScheduledNotes(channel, (note) => {
1182
1216
  channel.sustainNotes.push(note);
1183
1217
  });
1184
1218
  }
@@ -1297,7 +1331,7 @@ class MidyGM1 {
1297
1331
  const entries = Object.entries(defaultControllerState);
1298
1332
  for (const [key, { type, defaultValue }] of entries) {
1299
1333
  if (128 <= type) {
1300
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1334
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1301
1335
  }
1302
1336
  else {
1303
1337
  state[key] = defaultValue;
@@ -1322,7 +1356,7 @@ class MidyGM1 {
1322
1356
  const key = keys[i];
1323
1357
  const { type, defaultValue } = defaultControllerState[key];
1324
1358
  if (128 <= type) {
1325
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1359
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1326
1360
  }
1327
1361
  else {
1328
1362
  state[key] = defaultValue;