@marmooo/midy 0.2.4 → 0.2.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.
@@ -19,6 +19,8 @@ export class MidyGMLite {
19
19
  resumeTime: number;
20
20
  soundFonts: any[];
21
21
  soundFontTable: any[];
22
+ audioBufferCounter: Map<any, any>;
23
+ audioBufferCache: Map<any, any>;
22
24
  isPlaying: boolean;
23
25
  isPausing: boolean;
24
26
  isPaused: boolean;
@@ -27,7 +29,7 @@ export class MidyGMLite {
27
29
  timeline: any[];
28
30
  instruments: any[];
29
31
  notePromises: any[];
30
- exclusiveClassMap: Map<any, any>;
32
+ exclusiveClassMap: SparseMap;
31
33
  audioContext: any;
32
34
  masterVolume: any;
33
35
  voiceParamsHandlers: {
@@ -43,11 +45,11 @@ export class MidyGMLite {
43
45
  freqVibLFO: (_channel: any, _note: any, _prevValue: any) => void;
44
46
  };
45
47
  controlChangeHandlers: {
46
- 1: (channelNumber: any, modulation: any) => void;
48
+ 1: (channelNumber: any, modulation: any, scheduleTime: any) => void;
47
49
  6: (channelNumber: any, value: any) => void;
48
- 7: (channelNumber: any, volume: any) => void;
49
- 10: (channelNumber: any, pan: any) => void;
50
- 11: (channelNumber: any, expression: any) => void;
50
+ 7: (channelNumber: any, volume: any, scheduleTime: any) => void;
51
+ 10: (channelNumber: any, pan: any, scheduleTime: any) => void;
52
+ 11: (channelNumber: any, expression: any, scheduleTime: any) => void;
51
53
  38: (channelNumber: any, value: any) => void;
52
54
  64: (channelNumber: any, value: any) => void;
53
55
  100: (channelNumber: any, value: any) => void;
@@ -68,12 +70,13 @@ export class MidyGMLite {
68
70
  };
69
71
  createChannels(audioContext: any): any[];
70
72
  createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
71
- createNoteBufferNode(voiceParams: any, isSF3: any): Promise<any>;
73
+ createNoteBufferNode(audioBuffer: any, voiceParams: any): any;
72
74
  scheduleTimelineEvents(t: any, offset: any, queueIndex: any): Promise<any>;
73
75
  getQueueIndex(second: any): number;
74
76
  playNotes(): Promise<any>;
75
77
  ticksToSecond(ticks: any, secondsPerBeat: any): number;
76
78
  secondToTicks(second: any, secondsPerBeat: any): number;
79
+ getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
77
80
  extractMidiData(midi: any): {
78
81
  instruments: Set<any>;
79
82
  timeline: any[];
@@ -87,7 +90,8 @@ export class MidyGMLite {
87
90
  seekTo(second: any): void;
88
91
  calcTotalTime(): number;
89
92
  currentTime(): number;
90
- getActiveNotes(channel: any, time: any): Map<any, any>;
93
+ processScheduledNotes(channel: any, scheduleTime: any, callback: any): void;
94
+ getActiveNotes(channel: any, time: any): SparseMap;
91
95
  getActiveNote(noteList: any, time: any): any;
92
96
  cbToRatio(cb: any): number;
93
97
  rateToCent(rate: any): number;
@@ -97,10 +101,11 @@ export class MidyGMLite {
97
101
  updateChannelDetune(channel: any): void;
98
102
  updateDetune(channel: any, note: any): void;
99
103
  setVolumeEnvelope(note: any): void;
100
- setPitchEnvelope(note: any): void;
104
+ setPitchEnvelope(note: any, scheduleTime: any): void;
101
105
  clampCutoffFrequency(frequency: any): number;
102
106
  setFilterEnvelope(note: any): void;
103
107
  startModulation(channel: any, note: any, startTime: any): void;
108
+ getAudioBuffer(program: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
104
109
  createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, isSF3: any): Promise<Note>;
105
110
  scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
106
111
  noteOn(channelNumber: any, noteNumber: any, velocity: any): Promise<void>;
@@ -132,11 +137,11 @@ export class MidyGMLite {
132
137
  getControllerState(channel: any, noteNumber: any, velocity: any): Float32Array<any>;
133
138
  applyVoiceParams(channel: any, controllerType: any): void;
134
139
  createControlChangeHandlers(): {
135
- 1: (channelNumber: any, modulation: any) => void;
140
+ 1: (channelNumber: any, modulation: any, scheduleTime: any) => void;
136
141
  6: (channelNumber: any, value: any) => void;
137
- 7: (channelNumber: any, volume: any) => void;
138
- 10: (channelNumber: any, pan: any) => void;
139
- 11: (channelNumber: any, expression: any) => void;
142
+ 7: (channelNumber: any, volume: any, scheduleTime: any) => void;
143
+ 10: (channelNumber: any, pan: any, scheduleTime: any) => void;
144
+ 11: (channelNumber: any, expression: any, scheduleTime: any) => void;
140
145
  38: (channelNumber: any, value: any) => void;
141
146
  64: (channelNumber: any, value: any) => void;
142
147
  100: (channelNumber: any, value: any) => void;
@@ -145,18 +150,18 @@ export class MidyGMLite {
145
150
  121: (channelNumber: any) => void;
146
151
  123: (channelNumber: any) => Promise<void>;
147
152
  };
148
- handleControlChange(channelNumber: any, controllerType: any, value: any): void;
149
- updateModulation(channel: any): void;
150
- setModulationDepth(channelNumber: any, modulation: any): void;
151
- setVolume(channelNumber: any, volume: any): void;
153
+ handleControlChange(channelNumber: any, controllerType: any, value: any, startTime: any): void;
154
+ updateModulation(channel: any, scheduleTime: any): void;
155
+ setModulationDepth(channelNumber: any, modulation: any, scheduleTime: any): void;
156
+ setVolume(channelNumber: any, volume: any, scheduleTime: any): void;
152
157
  panToGain(pan: any): {
153
158
  gainLeft: number;
154
159
  gainRight: number;
155
160
  };
156
- setPan(channelNumber: any, pan: any): void;
157
- setExpression(channelNumber: any, expression: any): void;
161
+ setPan(channelNumber: any, pan: any, scheduleTime: any): void;
162
+ setExpression(channelNumber: any, expression: any, scheduleTime: any): void;
158
163
  dataEntryLSB(channelNumber: any, value: any): void;
159
- updateChannelVolume(channel: any): void;
164
+ updateChannelVolume(channel: any, scheduleTime: any): void;
160
165
  setSustainPedal(channelNumber: any, value: any): void;
161
166
  limitData(channel: any, minMSB: any, maxMSB: any, minLSB: any, maxLSB: any): void;
162
167
  handleRPN(channelNumber: any): void;
@@ -177,10 +182,24 @@ export class MidyGMLite {
177
182
  handleSysEx(data: any): void;
178
183
  scheduleTask(callback: any, startTime: any): Promise<any>;
179
184
  }
185
+ declare class SparseMap {
186
+ constructor(size: any);
187
+ data: any[];
188
+ activeIndices: any[];
189
+ set(key: any, value: any): void;
190
+ get(key: any): any;
191
+ delete(key: any): boolean;
192
+ has(key: any): boolean;
193
+ get size(): number;
194
+ clear(): void;
195
+ forEach(callback: any): void;
196
+ [Symbol.iterator](): Generator<any[], void, unknown>;
197
+ }
180
198
  declare class Note {
181
199
  constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
182
200
  bufferSource: any;
183
201
  filterNode: any;
202
+ filterDepth: any;
184
203
  volumeEnvelopeNode: any;
185
204
  volumeDepth: any;
186
205
  modulationLFO: any;
@@ -1 +1 @@
1
- {"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AAmFA;IAoBE;;;;;;;;;MASE;IAEF,+BAQC;IAtCD,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAClB,iCAA8B;IAc5B,kBAAgC;IAChC,kBAA8C;IAC9C;;;;;;;;;;;MAA2D;IAC3D;;;;;;;;;;;;;MAA+D;IAC/D,gBAAiD;IAKnD,4BAMC;IAED,mCAWC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAeC;IAED,yCAUC;IAED,6DA2BC;IAED,iEAUC;IAED,2EA+CC;IAED,mCAOC;IAED,0BAkDC;IAED,uDAEC;IAED,wDAEC;IAED;;;MAgEC;IAED,+EAmBC;IAED,qDAKC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,uDASC;IAED,6CAQC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,wCAIC;IAED,wCAQC;IAED,4CAKC;IAED,mCAgBC;IAED,kCAqBC;IAED,6CAIC;IAED,mCAuBC;IAED,+DAoBC;IAED,gHA2BC;IAED,kGAgDC;IAED,0EAGC;IAED,qFAwBC;IAED,6HAuBC;IAED,0FAGC;IAED,kEAeC;IAED,gFAiBC;IAED,4DAGC;IAED,qEAGC;IAED,mDASC;IAED,gDASC;IAED,qCAMC;IAED,mCAQC;IAED,gCAOC;IAED,+BAMC;IAED;;;;;;;;;;;MAqBC;IAED,oFAMC;IAED,0DA6CC;IAED;;;;;;;;;;;;;MAeC;IAED,+EAWC;IAED,qCAeC;IAED,8DAIC;IACD,iDAIC;IAED;;;MAMC;IAED,2CAIC;IAED,yDAIC;IAED,mDAGC;IAED,wCAWC;IAED,sDAKC;IAED,kFAeC;IAED,oCAYC;IAED,gDAEC;IAED,gDAEC;IAED,mDAGC;IAED,kDAKC;IAED,wDASC;IAED,+CAEC;IAED,8CAqBC;IAED,+CAEC;IAED,4DAgBC;IAED,oBAMC;IAED,yDAaC;IAED,yCAGC;IAED,mCAQC;IAED,wCAEC;IAED,6BASC;IAED,0DAUC;CACF;AA1tCD;IAQE,0FAMC;IAbD,kBAAa;IACb,gBAAW;IACX,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
1
+ {"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AAiJA;IAsBE;;;;;;;;;MASE;IAEF,+BAQC;IAxCD,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,6BAAuC;IAcrC,kBAAgC;IAChC,kBAA8C;IAC9C;;;;;;;;;;;MAA2D;IAC3D;;;;;;;;;;;;;MAA+D;IAC/D,gBAAiD;IAKnD,4BAMC;IAED,mCAWC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAeC;IAED,yCAUC;IAED,6DA2BC;IAED,8DASC;IAED,2EAqDC;IAED,mCAOC;IAED,0BAoDC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MA4EC;IAED,+EAmBC;IAED,qDAKC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,4EASC;IAED,mDASC;IAED,6CAQC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,wCAIC;IAED,wCAQC;IAED,4CAKC;IAED,mCAgBC;IAED,qDAqBC;IAED,6CAIC;IAED,mCAuBC;IAED,+DAoBC;IAED,yGAgBC;IAED,gHAuCC;IAED,kGAgDC;IAED,0EAGC;IAED,qFAwBC;IAED,6HAuBC;IAED,0FAGC;IAED,kEAeC;IAED,gFAiBC;IAED,4DAGC;IAED,qEAGC;IAED,mDASC;IAED,gDASC;IAED,qCAMC;IAED,mCAQC;IAED,gCAOC;IAED,+BAMC;IAED;;;;;;;;;;;MAqBC;IAED,oFAMC;IAED,0DA6CC;IAED;;;;;;;;;;;;;MAeC;IAED,+FAWC;IAED,wDAWC;IAED,iFAIC;IAED,oEAIC;IAED;;;MAMC;IAED,8DAIC;IAED,4EAIC;IAED,mDAGC;IAED,2DAWC;IAED,sDAKC;IAED,kFAeC;IAED,oCAYC;IAED,gDAEC;IAED,gDAEC;IAED,mDAGC;IAED,kDAKC;IAED,wDASC;IAED,+CAEC;IAED,8CAqBC;IAED,+CAEC;IAED,4DAgBC;IAED,oBAMC;IAED,yDAaC;IAED,yCAGC;IAED,mCAQC;IAED,wCAEC;IAED,6BASC;IAED,0DAUC;CACF;AAt1CD;IACE,uBAGC;IAFC,YAA2B;IAC3B,qBAAuB;IAGzB,gCAKC;IAED,mBAEC;IAED,0BAUC;IAED,uBAEC;IAED,mBAEC;IAED,cAMC;IASD,6BAKC;IAZD,qDAKC;CAQF;AAED;IASE,0FAMC;IAdD,kBAAa;IACb,gBAAW;IACX,iBAAY;IACZ,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
@@ -3,6 +3,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGMLite = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
+ // 2-3 times faster than Map
7
+ class SparseMap {
8
+ constructor(size) {
9
+ this.data = new Array(size);
10
+ this.activeIndices = [];
11
+ }
12
+ set(key, value) {
13
+ if (this.data[key] === undefined) {
14
+ this.activeIndices.push(key);
15
+ }
16
+ this.data[key] = value;
17
+ }
18
+ get(key) {
19
+ return this.data[key];
20
+ }
21
+ delete(key) {
22
+ if (this.data[key] !== undefined) {
23
+ this.data[key] = undefined;
24
+ const index = this.activeIndices.indexOf(key);
25
+ if (index !== -1) {
26
+ this.activeIndices.splice(index, 1);
27
+ }
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ has(key) {
33
+ return this.data[key] !== undefined;
34
+ }
35
+ get size() {
36
+ return this.activeIndices.length;
37
+ }
38
+ clear() {
39
+ for (let i = 0; i < this.activeIndices.length; i++) {
40
+ const key = this.activeIndices[i];
41
+ this.data[key] = undefined;
42
+ }
43
+ this.activeIndices = [];
44
+ }
45
+ *[Symbol.iterator]() {
46
+ for (let i = 0; i < this.activeIndices.length; i++) {
47
+ const key = this.activeIndices[i];
48
+ yield [key, this.data[key]];
49
+ }
50
+ }
51
+ forEach(callback) {
52
+ for (let i = 0; i < this.activeIndices.length; i++) {
53
+ const key = this.activeIndices[i];
54
+ callback(this.data[key], key, this);
55
+ }
56
+ }
57
+ }
6
58
  class Note {
7
59
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
60
  Object.defineProperty(this, "bufferSource", {
@@ -17,6 +69,12 @@ class Note {
17
69
  writable: true,
18
70
  value: void 0
19
71
  });
72
+ Object.defineProperty(this, "filterDepth", {
73
+ enumerable: true,
74
+ configurable: true,
75
+ writable: true,
76
+ value: void 0
77
+ });
20
78
  Object.defineProperty(this, "volumeEnvelopeNode", {
21
79
  enumerable: true,
22
80
  configurable: true,
@@ -169,6 +227,18 @@ class MidyGMLite {
169
227
  writable: true,
170
228
  value: this.initSoundFontTable()
171
229
  });
230
+ Object.defineProperty(this, "audioBufferCounter", {
231
+ enumerable: true,
232
+ configurable: true,
233
+ writable: true,
234
+ value: new Map()
235
+ });
236
+ Object.defineProperty(this, "audioBufferCache", {
237
+ enumerable: true,
238
+ configurable: true,
239
+ writable: true,
240
+ value: new Map()
241
+ });
172
242
  Object.defineProperty(this, "isPlaying", {
173
243
  enumerable: true,
174
244
  configurable: true,
@@ -221,7 +291,7 @@ class MidyGMLite {
221
291
  enumerable: true,
222
292
  configurable: true,
223
293
  writable: true,
224
- value: new Map()
294
+ value: new SparseMap(128)
225
295
  });
226
296
  this.audioContext = audioContext;
227
297
  this.masterVolume = new GainNode(audioContext);
@@ -234,7 +304,7 @@ class MidyGMLite {
234
304
  initSoundFontTable() {
235
305
  const table = new Array(128);
236
306
  for (let i = 0; i < 128; i++) {
237
- table[i] = new Map();
307
+ table[i] = new SparseMap(128);
238
308
  }
239
309
  return table;
240
310
  }
@@ -287,7 +357,7 @@ class MidyGMLite {
287
357
  ...this.constructor.channelSettings,
288
358
  state: new ControllerState(),
289
359
  ...this.setChannelAudioNodes(audioContext),
290
- scheduledNotes: new Map(),
360
+ scheduledNotes: new SparseMap(128),
291
361
  };
292
362
  });
293
363
  return channels;
@@ -321,9 +391,8 @@ class MidyGMLite {
321
391
  return audioBuffer;
322
392
  }
323
393
  }
324
- async createNoteBufferNode(voiceParams, isSF3) {
394
+ createNoteBufferNode(audioBuffer, voiceParams) {
325
395
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
326
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
327
396
  bufferSource.buffer = audioBuffer;
328
397
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
329
398
  if (bufferSource.loop) {
@@ -337,31 +406,32 @@ class MidyGMLite {
337
406
  const event = this.timeline[queueIndex];
338
407
  if (event.startTime > t + this.lookAhead)
339
408
  break;
409
+ const startTime = event.startTime + this.startDelay - offset;
340
410
  switch (event.type) {
341
411
  case "noteOn":
342
412
  if (event.velocity !== 0) {
343
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
413
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
344
414
  break;
345
415
  }
346
416
  /* falls through */
347
417
  case "noteOff": {
348
- const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
418
+ const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, startTime);
349
419
  if (notePromise) {
350
420
  this.notePromises.push(notePromise);
351
421
  }
352
422
  break;
353
423
  }
354
424
  case "controller":
355
- this.handleControlChange(event.channel, event.controllerType, event.value);
425
+ this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
356
426
  break;
357
427
  case "programChange":
358
- this.handleProgramChange(event.channel, event.programNumber);
428
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
359
429
  break;
360
430
  case "pitchBend":
361
- this.setPitchBend(event.channel, event.value + 8192);
431
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
362
432
  break;
363
433
  case "sysEx":
364
- this.handleSysEx(event.data);
434
+ this.handleSysEx(event.data, startTime);
365
435
  }
366
436
  queueIndex++;
367
437
  }
@@ -388,6 +458,7 @@ class MidyGMLite {
388
458
  await Promise.all(this.notePromises);
389
459
  this.notePromises = [];
390
460
  this.exclusiveClassMap.clear();
461
+ this.audioBufferCache.clear();
391
462
  resolve();
392
463
  return;
393
464
  }
@@ -403,8 +474,9 @@ class MidyGMLite {
403
474
  }
404
475
  else if (this.isStopping) {
405
476
  await this.stopNotes(0, true);
406
- this.exclusiveClassMap.clear();
407
477
  this.notePromises = [];
478
+ this.exclusiveClassMap.clear();
479
+ this.audioBufferCache.clear();
408
480
  resolve();
409
481
  this.isStopping = false;
410
482
  this.isPaused = false;
@@ -435,6 +507,9 @@ class MidyGMLite {
435
507
  secondToTicks(second, secondsPerBeat) {
436
508
  return second * this.ticksPerBeat / secondsPerBeat;
437
509
  }
510
+ getAudioBufferId(programNumber, noteNumber, velocity) {
511
+ return `${programNumber}:${noteNumber}:${velocity}`;
512
+ }
438
513
  extractMidiData(midi) {
439
514
  const instruments = new Set();
440
515
  const timeline = [];
@@ -455,6 +530,8 @@ class MidyGMLite {
455
530
  switch (event.type) {
456
531
  case "noteOn": {
457
532
  const channel = tmpChannels[event.channel];
533
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
534
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
458
535
  if (channel.programNumber < 0) {
459
536
  instruments.add(`${channel.bank}:0`);
460
537
  channel.programNumber = 0;
@@ -471,6 +548,10 @@ class MidyGMLite {
471
548
  timeline.push(event);
472
549
  }
473
550
  }
551
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
552
+ if (count === 1)
553
+ this.audioBufferCounter.delete(audioBufferId);
554
+ }
474
555
  const priority = {
475
556
  controller: 0,
476
557
  sysEx: 1,
@@ -560,8 +641,20 @@ class MidyGMLite {
560
641
  const now = this.audioContext.currentTime;
561
642
  return this.resumeTime + now - this.startTime - this.startDelay;
562
643
  }
644
+ processScheduledNotes(channel, scheduleTime, callback) {
645
+ channel.scheduledNotes.forEach((noteList) => {
646
+ for (let i = 0; i < noteList.length; i++) {
647
+ const note = noteList[i];
648
+ if (!note)
649
+ continue;
650
+ if (scheduleTime < note.startTime)
651
+ continue;
652
+ callback(note);
653
+ }
654
+ });
655
+ }
563
656
  getActiveNotes(channel, time) {
564
- const activeNotes = new Map();
657
+ const activeNotes = new SparseMap(128);
565
658
  channel.scheduledNotes.forEach((noteList) => {
566
659
  const activeNote = this.getActiveNote(noteList, time);
567
660
  if (activeNote) {
@@ -631,20 +724,20 @@ class MidyGMLite {
631
724
  .setValueAtTime(attackVolume, volHold)
632
725
  .linearRampToValueAtTime(sustainVolume, volDecay);
633
726
  }
634
- setPitchEnvelope(note) {
635
- const now = this.audioContext.currentTime;
727
+ setPitchEnvelope(note, scheduleTime) {
728
+ scheduleTime ??= this.audioContext.currentTime;
636
729
  const { voiceParams } = note;
637
730
  const baseRate = voiceParams.playbackRate;
638
731
  note.bufferSource.playbackRate
639
- .cancelScheduledValues(now)
640
- .setValueAtTime(baseRate, now);
732
+ .cancelScheduledValues(scheduleTime)
733
+ .setValueAtTime(baseRate, scheduleTime);
641
734
  const modEnvToPitch = voiceParams.modEnvToPitch;
642
735
  if (modEnvToPitch === 0)
643
736
  return;
644
737
  const basePitch = this.rateToCent(baseRate);
645
738
  const peekPitch = basePitch + modEnvToPitch;
646
739
  const peekRate = this.centToRate(peekPitch);
647
- const modDelay = startTime + voiceParams.modDelay;
740
+ const modDelay = note.startTime + voiceParams.modDelay;
648
741
  const modAttack = modDelay + voiceParams.modAttack;
649
742
  const modHold = modAttack + voiceParams.modHold;
650
743
  const modDecay = modHold + voiceParams.modDecay;
@@ -701,11 +794,31 @@ class MidyGMLite {
701
794
  note.modulationLFO.connect(note.volumeDepth);
702
795
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
703
796
  }
797
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
798
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
799
+ const cache = this.audioBufferCache.get(audioBufferId);
800
+ if (cache) {
801
+ cache.counter += 1;
802
+ if (cache.maxCount <= cache.counter) {
803
+ this.audioBufferCache.delete(audioBufferId);
804
+ }
805
+ return cache.audioBuffer;
806
+ }
807
+ else {
808
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
809
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
810
+ const cache = { audioBuffer, maxCount, counter: 1 };
811
+ this.audioBufferCache.set(audioBufferId, cache);
812
+ return audioBuffer;
813
+ }
814
+ }
704
815
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
705
816
  const state = channel.state;
706
- const voiceParams = voice.getAllParams(state.array);
817
+ const controllerState = this.getControllerState(channel, noteNumber, velocity);
818
+ const voiceParams = voice.getAllParams(controllerState);
707
819
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
708
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
820
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
821
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
709
822
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
710
823
  note.filterNode = new BiquadFilterNode(this.audioContext, {
711
824
  type: "lowpass",
@@ -729,10 +842,10 @@ class MidyGMLite {
729
842
  if (soundFontIndex === undefined)
730
843
  return;
731
844
  const soundFont = this.soundFonts[soundFontIndex];
732
- const isSF3 = soundFont.parsed.info.version.major === 3;
733
845
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
734
846
  if (!voice)
735
847
  return;
848
+ const isSF3 = soundFont.parsed.info.version.major === 3;
736
849
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
737
850
  note.volumeEnvelopeNode.connect(channel.gainL);
738
851
  note.volumeEnvelopeNode.connect(channel.gainR);
@@ -1001,10 +1114,10 @@ class MidyGMLite {
1001
1114
  123: this.allNotesOff,
1002
1115
  };
1003
1116
  }
1004
- handleControlChange(channelNumber, controllerType, value) {
1117
+ handleControlChange(channelNumber, controllerType, value, startTime) {
1005
1118
  const handler = this.controlChangeHandlers[controllerType];
1006
1119
  if (handler) {
1007
- handler.call(this, channelNumber, value);
1120
+ handler.call(this, channelNumber, value, startTime);
1008
1121
  const channel = this.channels[channelNumber];
1009
1122
  this.applyVoiceParams(channel, controllerType + 128);
1010
1123
  }
@@ -1012,33 +1125,28 @@ class MidyGMLite {
1012
1125
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
1013
1126
  }
1014
1127
  }
1015
- updateModulation(channel) {
1016
- const now = this.audioContext.currentTime;
1128
+ updateModulation(channel, scheduleTime) {
1129
+ scheduleTime ??= this.audioContext.currentTime;
1017
1130
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1018
- channel.scheduledNotes.forEach((noteList) => {
1019
- for (let i = 0; i < noteList.length; i++) {
1020
- const note = noteList[i];
1021
- if (!note)
1022
- continue;
1023
- if (note.modulationDepth) {
1024
- note.modulationDepth.gain.setValueAtTime(depth, now);
1025
- }
1026
- else {
1027
- this.setPitchEnvelope(note);
1028
- this.startModulation(channel, note, now);
1029
- }
1131
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1132
+ if (note.modulationDepth) {
1133
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1134
+ }
1135
+ else {
1136
+ this.setPitchEnvelope(note, scheduleTime);
1137
+ this.startModulation(channel, note, scheduleTime);
1030
1138
  }
1031
1139
  });
1032
1140
  }
1033
- setModulationDepth(channelNumber, modulation) {
1141
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1034
1142
  const channel = this.channels[channelNumber];
1035
1143
  channel.state.modulationDepth = modulation / 127;
1036
- this.updateModulation(channel);
1144
+ this.updateModulation(channel, scheduleTime);
1037
1145
  }
1038
- setVolume(channelNumber, volume) {
1146
+ setVolume(channelNumber, volume, scheduleTime) {
1039
1147
  const channel = this.channels[channelNumber];
1040
1148
  channel.state.volume = volume / 127;
1041
- this.updateChannelVolume(channel);
1149
+ this.updateChannelVolume(channel, scheduleTime);
1042
1150
  }
1043
1151
  panToGain(pan) {
1044
1152
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1047,31 +1155,31 @@ class MidyGMLite {
1047
1155
  gainRight: Math.sin(theta),
1048
1156
  };
1049
1157
  }
1050
- setPan(channelNumber, pan) {
1158
+ setPan(channelNumber, pan, scheduleTime) {
1051
1159
  const channel = this.channels[channelNumber];
1052
1160
  channel.state.pan = pan / 127;
1053
- this.updateChannelVolume(channel);
1161
+ this.updateChannelVolume(channel, scheduleTime);
1054
1162
  }
1055
- setExpression(channelNumber, expression) {
1163
+ setExpression(channelNumber, expression, scheduleTime) {
1056
1164
  const channel = this.channels[channelNumber];
1057
1165
  channel.state.expression = expression / 127;
1058
- this.updateChannelVolume(channel);
1166
+ this.updateChannelVolume(channel, scheduleTime);
1059
1167
  }
1060
1168
  dataEntryLSB(channelNumber, value) {
1061
1169
  this.channels[channelNumber].dataLSB = value;
1062
1170
  this.handleRPN(channelNumber);
1063
1171
  }
1064
- updateChannelVolume(channel) {
1065
- const now = this.audioContext.currentTime;
1172
+ updateChannelVolume(channel, scheduleTime) {
1173
+ scheduleTime ??= this.audioContext.currentTime;
1066
1174
  const state = channel.state;
1067
1175
  const volume = state.volume * state.expression;
1068
1176
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1069
1177
  channel.gainL.gain
1070
1178
  .cancelScheduledValues(now)
1071
- .setValueAtTime(volume * gainLeft, now);
1179
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1072
1180
  channel.gainR.gain
1073
1181
  .cancelScheduledValues(now)
1074
- .setValueAtTime(volume * gainRight, now);
1182
+ .setValueAtTime(volume * gainRight, scheduleTime);
1075
1183
  }
1076
1184
  setSustainPedal(channelNumber, value) {
1077
1185
  this.channels[channelNumber].state.sustainPedal = value / 127;