@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marmooo/midy",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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.2",
26
26
  "midi-file": "^1.2.4"
27
27
  },
28
28
  "devDependencies": {
@@ -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>;
59
- setChannelAudioNodes(audioContext: any): {
60
+ cacheVoiceIds(): void;
61
+ getVoiceId(channel: any, noteNumber: any, velocity: any): string | undefined;
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[];
@@ -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
- scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any, noteOffEvent: any): Promise<void>;
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":"AAsFA;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,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,+GA0BC;IAED,gHAyCC;IAED,0EAiBC;IAED,qHAwCC;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,6EA2CC;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;AA79CD;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"}
@@ -73,13 +73,11 @@ const defaultControllerState = {
73
73
  pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
74
74
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
75
75
  link: { type: 127, defaultValue: 0 },
76
- // bankMSB: { type: 128 + 0, defaultValue: 121, },
77
76
  modulationDepth: { type: 128 + 1, defaultValue: 0 },
78
77
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
79
78
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
80
79
  pan: { type: 128 + 10, defaultValue: 64 / 127 },
81
80
  expression: { type: 128 + 11, defaultValue: 1 },
82
- // bankLSB: { type: 128 + 32, defaultValue: 0, },
83
81
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
84
82
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
85
83
  // rpnLSB: { type: 128 + 100, defaultValue: 127 },
@@ -108,6 +106,16 @@ class ControllerState {
108
106
  }
109
107
  }
110
108
  }
109
+ const volumeEnvelopeKeys = [
110
+ "volDelay",
111
+ "volAttack",
112
+ "volHold",
113
+ "volDecay",
114
+ "volSustain",
115
+ "volRelease",
116
+ "initialAttenuation",
117
+ ];
118
+ const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
111
119
  const filterEnvelopeKeys = [
112
120
  "modEnvToPitch",
113
121
  "initialFilterFc",
@@ -117,20 +125,18 @@ const filterEnvelopeKeys = [
117
125
  "modHold",
118
126
  "modDecay",
119
127
  "modSustain",
120
- "modRelease",
121
- "playbackRate",
122
128
  ];
123
129
  const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
124
- const volumeEnvelopeKeys = [
125
- "volDelay",
126
- "volAttack",
127
- "volHold",
128
- "volDecay",
129
- "volSustain",
130
- "volRelease",
131
- "initialAttenuation",
130
+ const pitchEnvelopeKeys = [
131
+ "modEnvToPitch",
132
+ "modDelay",
133
+ "modAttack",
134
+ "modHold",
135
+ "modDecay",
136
+ "modSustain",
137
+ "playbackRate",
132
138
  ];
133
- const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
139
+ const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
134
140
  class MidyGM1 {
135
141
  constructor(audioContext) {
136
142
  Object.defineProperty(this, "mode", {
@@ -199,13 +205,13 @@ class MidyGM1 {
199
205
  writable: true,
200
206
  value: this.initSoundFontTable()
201
207
  });
202
- Object.defineProperty(this, "audioBufferCounter", {
208
+ Object.defineProperty(this, "voiceCounter", {
203
209
  enumerable: true,
204
210
  configurable: true,
205
211
  writable: true,
206
212
  value: new Map()
207
213
  });
208
- Object.defineProperty(this, "audioBufferCache", {
214
+ Object.defineProperty(this, "voiceCache", {
209
215
  enumerable: true,
210
216
  configurable: true,
211
217
  writable: true,
@@ -292,13 +298,11 @@ class MidyGM1 {
292
298
  const presetHeaders = soundFont.parsed.presetHeaders;
293
299
  for (let i = 0; i < presetHeaders.length; i++) {
294
300
  const presetHeader = presetHeaders[i];
295
- if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
296
- const banks = this.soundFontTable[presetHeader.preset];
297
- banks.set(presetHeader.bank, index);
298
- }
301
+ const banks = this.soundFontTable[presetHeader.preset];
302
+ banks.set(presetHeader.bank, index);
299
303
  }
300
304
  }
301
- async loadSoundFont(input) {
305
+ async toUint8Array(input) {
302
306
  let uint8Array;
303
307
  if (typeof input === "string") {
304
308
  const response = await fetch(input);
@@ -311,23 +315,32 @@ class MidyGM1 {
311
315
  else {
312
316
  throw new TypeError("input must be a URL string or Uint8Array");
313
317
  }
314
- const parsed = (0, soundfont_parser_1.parse)(uint8Array);
315
- const soundFont = new soundfont_parser_1.SoundFont(parsed);
316
- this.addSoundFont(soundFont);
318
+ return uint8Array;
317
319
  }
318
- async loadMIDI(input) {
319
- let uint8Array;
320
- if (typeof input === "string") {
321
- const response = await fetch(input);
322
- const arrayBuffer = await response.arrayBuffer();
323
- uint8Array = new Uint8Array(arrayBuffer);
324
- }
325
- else if (input instanceof Uint8Array) {
326
- 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
+ }
327
333
  }
328
334
  else {
329
- 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);
330
339
  }
340
+ }
341
+ async loadMIDI(input) {
342
+ this.voiceCounter.clear();
343
+ const uint8Array = await this.toUint8Array(input);
331
344
  const midi = (0, midi_file_1.parseMidi)(uint8Array);
332
345
  this.ticksPerBeat = midi.header.ticksPerBeat;
333
346
  const midiData = this.extractMidiData(midi);
@@ -335,7 +348,46 @@ class MidyGM1 {
335
348
  this.timeline = midiData.timeline;
336
349
  this.totalTime = this.calcTotalTime();
337
350
  }
338
- setChannelAudioNodes(audioContext) {
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}:${instrument}:${sampleID}`;
389
+ }
390
+ createChannelAudioNodes(audioContext) {
339
391
  const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
340
392
  const gainL = new GainNode(audioContext, { gain: gainLeft });
341
393
  const gainR = new GainNode(audioContext, { gain: gainRight });
@@ -356,41 +408,19 @@ class MidyGM1 {
356
408
  isDrum: false,
357
409
  state: new ControllerState(),
358
410
  ...this.constructor.channelSettings,
359
- ...this.setChannelAudioNodes(audioContext),
411
+ ...this.createChannelAudioNodes(audioContext),
360
412
  scheduledNotes: [],
361
413
  sustainNotes: [],
362
414
  };
363
415
  });
364
416
  return channels;
365
417
  }
366
- async createNoteBuffer(voiceParams, isSF3) {
418
+ async createAudioBuffer(voiceParams) {
419
+ const sample = voiceParams.sample;
367
420
  const sampleStart = voiceParams.start;
368
- const sampleEnd = voiceParams.sample.length + voiceParams.end;
369
- if (isSF3) {
370
- const sample = voiceParams.sample;
371
- const start = sample.byteOffset + sampleStart;
372
- const end = sample.byteOffset + sampleEnd;
373
- const buffer = sample.buffer.slice(start, end);
374
- const audioBuffer = await this.audioContext.decodeAudioData(buffer);
375
- return audioBuffer;
376
- }
377
- else {
378
- const sample = voiceParams.sample;
379
- const start = sample.byteOffset + sampleStart;
380
- const end = sample.byteOffset + sampleEnd;
381
- const buffer = sample.buffer.slice(start, end);
382
- const audioBuffer = new AudioBuffer({
383
- numberOfChannels: 1,
384
- length: sample.length,
385
- sampleRate: voiceParams.sampleRate,
386
- });
387
- const channelData = audioBuffer.getChannelData(0);
388
- const int16Array = new Int16Array(buffer);
389
- for (let i = 0; i < int16Array.length; i++) {
390
- channelData[i] = int16Array[i] / 32768;
391
- }
392
- return audioBuffer;
393
- }
421
+ const sampleEnd = sample.data.length + voiceParams.end;
422
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
423
+ return audioBuffer;
394
424
  }
395
425
  createBufferSource(voiceParams, audioBuffer) {
396
426
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
@@ -420,10 +450,10 @@ class MidyGM1 {
420
450
  break;
421
451
  }
422
452
  case "controller":
423
- this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
453
+ this.setControlChange(event.channel, event.controllerType, event.value, startTime);
424
454
  break;
425
455
  case "programChange":
426
- this.handleProgramChange(event.channel, event.programNumber, startTime);
456
+ this.setProgramChange(event.channel, event.programNumber, startTime);
427
457
  break;
428
458
  case "pitchBend":
429
459
  this.setPitchBend(event.channel, event.value + 8192, startTime);
@@ -456,8 +486,9 @@ class MidyGM1 {
456
486
  await Promise.all(this.notePromises);
457
487
  this.notePromises = [];
458
488
  this.exclusiveClassNotes.fill(undefined);
459
- this.audioBufferCache.clear();
489
+ this.voiceCache.clear();
460
490
  for (let i = 0; i < this.channels.length; i++) {
491
+ this.channels[i].scheduledNotes = [];
461
492
  this.resetAllStates(i);
462
493
  }
463
494
  resolve();
@@ -478,8 +509,9 @@ class MidyGM1 {
478
509
  await this.stopNotes(0, true, now);
479
510
  this.notePromises = [];
480
511
  this.exclusiveClassNotes.fill(undefined);
481
- this.audioBufferCache.clear();
512
+ this.voiceCache.clear();
482
513
  for (let i = 0; i < this.channels.length; i++) {
514
+ this.channels[i].scheduledNotes = [];
483
515
  this.resetAllStates(i);
484
516
  }
485
517
  this.isStopping = false;
@@ -511,11 +543,7 @@ class MidyGM1 {
511
543
  secondToTicks(second, secondsPerBeat) {
512
544
  return second * this.ticksPerBeat / secondsPerBeat;
513
545
  }
514
- getAudioBufferId(programNumber, noteNumber, velocity) {
515
- return `${programNumber}:${noteNumber}:${velocity}`;
516
- }
517
546
  extractMidiData(midi) {
518
- this.audioBufferCounter.clear();
519
547
  const instruments = new Set();
520
548
  const timeline = [];
521
549
  const tmpChannels = new Array(this.channels.length);
@@ -535,8 +563,6 @@ class MidyGM1 {
535
563
  switch (event.type) {
536
564
  case "noteOn": {
537
565
  const channel = tmpChannels[event.channel];
538
- const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
539
- this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
540
566
  if (channel.programNumber < 0) {
541
567
  instruments.add(`${channel.bank}:0`);
542
568
  channel.programNumber = 0;
@@ -553,10 +579,6 @@ class MidyGM1 {
553
579
  timeline.push(event);
554
580
  }
555
581
  }
556
- for (const [audioBufferId, count] of this.audioBufferCounter) {
557
- if (count === 1)
558
- this.audioBufferCounter.delete(audioBufferId);
559
- }
560
582
  const priority = {
561
583
  controller: 0,
562
584
  sysEx: 1,
@@ -599,7 +621,6 @@ class MidyGM1 {
599
621
  this.notePromises.push(promise);
600
622
  promises.push(promise);
601
623
  });
602
- channel.scheduledNotes = [];
603
624
  return Promise.all(promises);
604
625
  }
605
626
  stopNotes(velocity, force, scheduleTime) {
@@ -613,6 +634,8 @@ class MidyGM1 {
613
634
  if (this.isPlaying || this.isPaused)
614
635
  return;
615
636
  this.resumeTime = 0;
637
+ if (this.voiceCounter.size === 0)
638
+ this.cacheVoiceIds();
616
639
  await this.playNotes();
617
640
  this.isPlaying = false;
618
641
  }
@@ -655,7 +678,7 @@ class MidyGM1 {
655
678
  }
656
679
  processScheduledNotes(channel, callback) {
657
680
  const scheduledNotes = channel.scheduledNotes;
658
- for (let i = 0; i < scheduledNotes.length; i++) {
681
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
659
682
  const note = scheduledNotes[i];
660
683
  if (!note)
661
684
  continue;
@@ -666,14 +689,14 @@ class MidyGM1 {
666
689
  }
667
690
  processActiveNotes(channel, scheduleTime, callback) {
668
691
  const scheduledNotes = channel.scheduledNotes;
669
- for (let i = 0; i < scheduledNotes.length; i++) {
692
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
670
693
  const note = scheduledNotes[i];
671
694
  if (!note)
672
695
  continue;
673
696
  if (note.ending)
674
697
  continue;
675
698
  if (scheduleTime < note.startTime)
676
- continue;
699
+ break;
677
700
  callback(note);
678
701
  }
679
702
  }
@@ -790,31 +813,31 @@ class MidyGM1 {
790
813
  note.modulationLFO.connect(note.volumeDepth);
791
814
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
792
815
  }
793
- async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
794
- const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
795
- 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);
796
819
  if (cache) {
797
820
  cache.counter += 1;
798
821
  if (cache.maxCount <= cache.counter) {
799
- this.audioBufferCache.delete(audioBufferId);
822
+ this.voiceCache.delete(audioBufferId);
800
823
  }
801
824
  return cache.audioBuffer;
802
825
  }
803
826
  else {
804
- const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
805
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
827
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
828
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
806
829
  const cache = { audioBuffer, maxCount, counter: 1 };
807
- this.audioBufferCache.set(audioBufferId, cache);
830
+ this.voiceCache.set(audioBufferId, cache);
808
831
  return audioBuffer;
809
832
  }
810
833
  }
811
- async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
834
+ async createNote(channel, voice, noteNumber, velocity, startTime) {
812
835
  const now = this.audioContext.currentTime;
813
836
  const state = channel.state;
814
837
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
815
838
  const voiceParams = voice.getAllParams(controllerState);
816
839
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
817
- const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
840
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
818
841
  note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
819
842
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
820
843
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -847,19 +870,18 @@ class MidyGM1 {
847
870
  }
848
871
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
849
872
  }
850
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
873
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
851
874
  const channel = this.channels[channelNumber];
852
875
  const bankNumber = channel.bank;
853
- const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
876
+ const soundFontIndex = this.soundFontTable[channel.programNumber]
877
+ .get(bankNumber);
854
878
  if (soundFontIndex === undefined)
855
879
  return;
856
880
  const soundFont = this.soundFonts[soundFontIndex];
857
881
  const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
858
882
  if (!voice)
859
883
  return;
860
- const isSF3 = soundFont.parsed.info.version.major === 3;
861
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
862
- note.noteOffEvent = noteOffEvent;
884
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
863
885
  note.volumeEnvelopeNode.connect(channel.gainL);
864
886
  note.volumeEnvelopeNode.connect(channel.gainR);
865
887
  if (0.5 <= channel.state.sustainPedal) {
@@ -909,15 +931,29 @@ class MidyGM1 {
909
931
  const channel = this.channels[channelNumber];
910
932
  if (!force && 0.5 <= channel.state.sustainPedal)
911
933
  return;
912
- const note = this.findNoteOffTarget(channel, noteNumber);
913
- if (!note)
934
+ const index = this.findNoteOffIndex(channel, noteNumber);
935
+ if (index < 0)
914
936
  return;
937
+ const note = channel.scheduledNotes[index];
915
938
  note.ending = true;
939
+ this.setNoteIndex(channel, index);
916
940
  this.releaseNote(channel, note, endTime);
917
941
  }
918
- 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) {
919
955
  const scheduledNotes = channel.scheduledNotes;
920
- for (let i = 0; i < scheduledNotes.length; i++) {
956
+ for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
921
957
  const note = scheduledNotes[i];
922
958
  if (!note)
923
959
  continue;
@@ -925,8 +961,9 @@ class MidyGM1 {
925
961
  continue;
926
962
  if (note.noteNumber !== noteNumber)
927
963
  continue;
928
- return note;
964
+ return i;
929
965
  }
966
+ return -1;
930
967
  }
931
968
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
932
969
  scheduleTime ??= this.audioContext.currentTime;
@@ -952,16 +989,16 @@ class MidyGM1 {
952
989
  case 0x90:
953
990
  return this.noteOn(channelNumber, data1, data2, scheduleTime);
954
991
  case 0xB0:
955
- return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
992
+ return this.setControlChange(channelNumber, data1, data2, scheduleTime);
956
993
  case 0xC0:
957
- return this.handleProgramChange(channelNumber, data1, scheduleTime);
994
+ return this.setProgramChange(channelNumber, data1, scheduleTime);
958
995
  case 0xE0:
959
996
  return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
960
997
  default:
961
998
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
962
999
  }
963
1000
  }
964
- handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1001
+ setProgramChange(channelNumber, programNumber, _scheduleTime) {
965
1002
  const channel = this.channels[channelNumber];
966
1003
  channel.programNumber = programNumber;
967
1004
  }
@@ -1054,8 +1091,9 @@ class MidyGM1 {
1054
1091
  this.processScheduledNotes(channel, (note) => {
1055
1092
  const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1056
1093
  const voiceParams = note.voice.getParams(controllerType, controllerState);
1057
- let appliedFilterEnvelope = false;
1058
- let appliedVolumeEnvelope = false;
1094
+ let applyVolumeEnvelope = false;
1095
+ let applyFilterEnvelope = false;
1096
+ let applyPitchEnvelope = false;
1059
1097
  for (const [key, value] of Object.entries(voiceParams)) {
1060
1098
  const prevValue = note.voiceParams[key];
1061
1099
  if (value === prevValue)
@@ -1064,32 +1102,21 @@ class MidyGM1 {
1064
1102
  if (key in this.voiceParamsHandlers) {
1065
1103
  this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1066
1104
  }
1067
- else if (filterEnvelopeKeySet.has(key)) {
1068
- if (appliedFilterEnvelope)
1069
- continue;
1070
- appliedFilterEnvelope = true;
1071
- const noteVoiceParams = note.voiceParams;
1072
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1073
- const key = filterEnvelopeKeys[i];
1074
- if (key in voiceParams)
1075
- noteVoiceParams[key] = voiceParams[key];
1076
- }
1077
- this.setFilterEnvelope(note, scheduleTime);
1078
- this.setPitchEnvelope(note, scheduleTime);
1079
- }
1080
- else if (volumeEnvelopeKeySet.has(key)) {
1081
- if (appliedVolumeEnvelope)
1082
- continue;
1083
- appliedVolumeEnvelope = true;
1084
- const noteVoiceParams = note.voiceParams;
1085
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1086
- const key = volumeEnvelopeKeys[i];
1087
- if (key in voiceParams)
1088
- noteVoiceParams[key] = voiceParams[key];
1089
- }
1090
- this.setVolumeEnvelope(note, scheduleTime);
1105
+ else {
1106
+ if (volumeEnvelopeKeySet.has(key))
1107
+ applyVolumeEnvelope = true;
1108
+ if (filterEnvelopeKeySet.has(key))
1109
+ applyFilterEnvelope = true;
1110
+ if (pitchEnvelopeKeySet.has(key))
1111
+ applyPitchEnvelope = true;
1091
1112
  }
1092
1113
  }
1114
+ if (applyVolumeEnvelope)
1115
+ this.setVolumeEnvelope(note, scheduleTime);
1116
+ if (applyFilterEnvelope)
1117
+ this.setFilterEnvelope(note, scheduleTime);
1118
+ if (applyPitchEnvelope)
1119
+ this.setPitchEnvelope(note, scheduleTime);
1093
1120
  });
1094
1121
  }
1095
1122
  createControlChangeHandlers() {
@@ -1105,9 +1132,10 @@ class MidyGM1 {
1105
1132
  handlers[101] = this.setRPNMSB;
1106
1133
  handlers[120] = this.allSoundOff;
1107
1134
  handlers[121] = this.resetAllControllers;
1135
+ handlers[123] = this.allNotesOff;
1108
1136
  return handlers;
1109
1137
  }
1110
- handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1138
+ setControlChange(channelNumber, controllerType, value, scheduleTime) {
1111
1139
  const handler = this.controlChangeHandlers[controllerType];
1112
1140
  if (handler) {
1113
1141
  handler.call(this, channelNumber, value, scheduleTime);
@@ -1300,7 +1328,7 @@ class MidyGM1 {
1300
1328
  const entries = Object.entries(defaultControllerState);
1301
1329
  for (const [key, { type, defaultValue }] of entries) {
1302
1330
  if (128 <= type) {
1303
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1331
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1304
1332
  }
1305
1333
  else {
1306
1334
  state[key] = defaultValue;
@@ -1325,7 +1353,7 @@ class MidyGM1 {
1325
1353
  const key = keys[i];
1326
1354
  const { type, defaultValue } = defaultControllerState[key];
1327
1355
  if (128 <= type) {
1328
- this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1356
+ this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
1329
1357
  }
1330
1358
  else {
1331
1359
  state[key] = defaultValue;