@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"}
@@ -1,5 +1,57 @@
1
1
  import { parseMidi } from "midi-file";
2
2
  import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
+ // 2-3 times faster than Map
4
+ class SparseMap {
5
+ constructor(size) {
6
+ this.data = new Array(size);
7
+ this.activeIndices = [];
8
+ }
9
+ set(key, value) {
10
+ if (this.data[key] === undefined) {
11
+ this.activeIndices.push(key);
12
+ }
13
+ this.data[key] = value;
14
+ }
15
+ get(key) {
16
+ return this.data[key];
17
+ }
18
+ delete(key) {
19
+ if (this.data[key] !== undefined) {
20
+ this.data[key] = undefined;
21
+ const index = this.activeIndices.indexOf(key);
22
+ if (index !== -1) {
23
+ this.activeIndices.splice(index, 1);
24
+ }
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+ has(key) {
30
+ return this.data[key] !== undefined;
31
+ }
32
+ get size() {
33
+ return this.activeIndices.length;
34
+ }
35
+ clear() {
36
+ for (let i = 0; i < this.activeIndices.length; i++) {
37
+ const key = this.activeIndices[i];
38
+ this.data[key] = undefined;
39
+ }
40
+ this.activeIndices = [];
41
+ }
42
+ *[Symbol.iterator]() {
43
+ for (let i = 0; i < this.activeIndices.length; i++) {
44
+ const key = this.activeIndices[i];
45
+ yield [key, this.data[key]];
46
+ }
47
+ }
48
+ forEach(callback) {
49
+ for (let i = 0; i < this.activeIndices.length; i++) {
50
+ const key = this.activeIndices[i];
51
+ callback(this.data[key], key, this);
52
+ }
53
+ }
54
+ }
3
55
  class Note {
4
56
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
5
57
  Object.defineProperty(this, "bufferSource", {
@@ -14,6 +66,12 @@ class Note {
14
66
  writable: true,
15
67
  value: void 0
16
68
  });
69
+ Object.defineProperty(this, "filterDepth", {
70
+ enumerable: true,
71
+ configurable: true,
72
+ writable: true,
73
+ value: void 0
74
+ });
17
75
  Object.defineProperty(this, "volumeEnvelopeNode", {
18
76
  enumerable: true,
19
77
  configurable: true,
@@ -166,6 +224,18 @@ export class MidyGMLite {
166
224
  writable: true,
167
225
  value: this.initSoundFontTable()
168
226
  });
227
+ Object.defineProperty(this, "audioBufferCounter", {
228
+ enumerable: true,
229
+ configurable: true,
230
+ writable: true,
231
+ value: new Map()
232
+ });
233
+ Object.defineProperty(this, "audioBufferCache", {
234
+ enumerable: true,
235
+ configurable: true,
236
+ writable: true,
237
+ value: new Map()
238
+ });
169
239
  Object.defineProperty(this, "isPlaying", {
170
240
  enumerable: true,
171
241
  configurable: true,
@@ -218,7 +288,7 @@ export class MidyGMLite {
218
288
  enumerable: true,
219
289
  configurable: true,
220
290
  writable: true,
221
- value: new Map()
291
+ value: new SparseMap(128)
222
292
  });
223
293
  this.audioContext = audioContext;
224
294
  this.masterVolume = new GainNode(audioContext);
@@ -231,7 +301,7 @@ export class MidyGMLite {
231
301
  initSoundFontTable() {
232
302
  const table = new Array(128);
233
303
  for (let i = 0; i < 128; i++) {
234
- table[i] = new Map();
304
+ table[i] = new SparseMap(128);
235
305
  }
236
306
  return table;
237
307
  }
@@ -284,7 +354,7 @@ export class MidyGMLite {
284
354
  ...this.constructor.channelSettings,
285
355
  state: new ControllerState(),
286
356
  ...this.setChannelAudioNodes(audioContext),
287
- scheduledNotes: new Map(),
357
+ scheduledNotes: new SparseMap(128),
288
358
  };
289
359
  });
290
360
  return channels;
@@ -318,9 +388,8 @@ export class MidyGMLite {
318
388
  return audioBuffer;
319
389
  }
320
390
  }
321
- async createNoteBufferNode(voiceParams, isSF3) {
391
+ createNoteBufferNode(audioBuffer, voiceParams) {
322
392
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
323
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
324
393
  bufferSource.buffer = audioBuffer;
325
394
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
326
395
  if (bufferSource.loop) {
@@ -334,31 +403,32 @@ export class MidyGMLite {
334
403
  const event = this.timeline[queueIndex];
335
404
  if (event.startTime > t + this.lookAhead)
336
405
  break;
406
+ const startTime = event.startTime + this.startDelay - offset;
337
407
  switch (event.type) {
338
408
  case "noteOn":
339
409
  if (event.velocity !== 0) {
340
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
410
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
341
411
  break;
342
412
  }
343
413
  /* falls through */
344
414
  case "noteOff": {
345
- const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
415
+ const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, startTime);
346
416
  if (notePromise) {
347
417
  this.notePromises.push(notePromise);
348
418
  }
349
419
  break;
350
420
  }
351
421
  case "controller":
352
- this.handleControlChange(event.channel, event.controllerType, event.value);
422
+ this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
353
423
  break;
354
424
  case "programChange":
355
- this.handleProgramChange(event.channel, event.programNumber);
425
+ this.handleProgramChange(event.channel, event.programNumber, startTime);
356
426
  break;
357
427
  case "pitchBend":
358
- this.setPitchBend(event.channel, event.value + 8192);
428
+ this.setPitchBend(event.channel, event.value + 8192, startTime);
359
429
  break;
360
430
  case "sysEx":
361
- this.handleSysEx(event.data);
431
+ this.handleSysEx(event.data, startTime);
362
432
  }
363
433
  queueIndex++;
364
434
  }
@@ -385,6 +455,7 @@ export class MidyGMLite {
385
455
  await Promise.all(this.notePromises);
386
456
  this.notePromises = [];
387
457
  this.exclusiveClassMap.clear();
458
+ this.audioBufferCache.clear();
388
459
  resolve();
389
460
  return;
390
461
  }
@@ -400,8 +471,9 @@ export class MidyGMLite {
400
471
  }
401
472
  else if (this.isStopping) {
402
473
  await this.stopNotes(0, true);
403
- this.exclusiveClassMap.clear();
404
474
  this.notePromises = [];
475
+ this.exclusiveClassMap.clear();
476
+ this.audioBufferCache.clear();
405
477
  resolve();
406
478
  this.isStopping = false;
407
479
  this.isPaused = false;
@@ -432,6 +504,9 @@ export class MidyGMLite {
432
504
  secondToTicks(second, secondsPerBeat) {
433
505
  return second * this.ticksPerBeat / secondsPerBeat;
434
506
  }
507
+ getAudioBufferId(programNumber, noteNumber, velocity) {
508
+ return `${programNumber}:${noteNumber}:${velocity}`;
509
+ }
435
510
  extractMidiData(midi) {
436
511
  const instruments = new Set();
437
512
  const timeline = [];
@@ -452,6 +527,8 @@ export class MidyGMLite {
452
527
  switch (event.type) {
453
528
  case "noteOn": {
454
529
  const channel = tmpChannels[event.channel];
530
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
531
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
455
532
  if (channel.programNumber < 0) {
456
533
  instruments.add(`${channel.bank}:0`);
457
534
  channel.programNumber = 0;
@@ -468,6 +545,10 @@ export class MidyGMLite {
468
545
  timeline.push(event);
469
546
  }
470
547
  }
548
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
549
+ if (count === 1)
550
+ this.audioBufferCounter.delete(audioBufferId);
551
+ }
471
552
  const priority = {
472
553
  controller: 0,
473
554
  sysEx: 1,
@@ -557,8 +638,20 @@ export class MidyGMLite {
557
638
  const now = this.audioContext.currentTime;
558
639
  return this.resumeTime + now - this.startTime - this.startDelay;
559
640
  }
641
+ processScheduledNotes(channel, scheduleTime, callback) {
642
+ channel.scheduledNotes.forEach((noteList) => {
643
+ for (let i = 0; i < noteList.length; i++) {
644
+ const note = noteList[i];
645
+ if (!note)
646
+ continue;
647
+ if (scheduleTime < note.startTime)
648
+ continue;
649
+ callback(note);
650
+ }
651
+ });
652
+ }
560
653
  getActiveNotes(channel, time) {
561
- const activeNotes = new Map();
654
+ const activeNotes = new SparseMap(128);
562
655
  channel.scheduledNotes.forEach((noteList) => {
563
656
  const activeNote = this.getActiveNote(noteList, time);
564
657
  if (activeNote) {
@@ -628,20 +721,20 @@ export class MidyGMLite {
628
721
  .setValueAtTime(attackVolume, volHold)
629
722
  .linearRampToValueAtTime(sustainVolume, volDecay);
630
723
  }
631
- setPitchEnvelope(note) {
632
- const now = this.audioContext.currentTime;
724
+ setPitchEnvelope(note, scheduleTime) {
725
+ scheduleTime ??= this.audioContext.currentTime;
633
726
  const { voiceParams } = note;
634
727
  const baseRate = voiceParams.playbackRate;
635
728
  note.bufferSource.playbackRate
636
- .cancelScheduledValues(now)
637
- .setValueAtTime(baseRate, now);
729
+ .cancelScheduledValues(scheduleTime)
730
+ .setValueAtTime(baseRate, scheduleTime);
638
731
  const modEnvToPitch = voiceParams.modEnvToPitch;
639
732
  if (modEnvToPitch === 0)
640
733
  return;
641
734
  const basePitch = this.rateToCent(baseRate);
642
735
  const peekPitch = basePitch + modEnvToPitch;
643
736
  const peekRate = this.centToRate(peekPitch);
644
- const modDelay = startTime + voiceParams.modDelay;
737
+ const modDelay = note.startTime + voiceParams.modDelay;
645
738
  const modAttack = modDelay + voiceParams.modAttack;
646
739
  const modHold = modAttack + voiceParams.modHold;
647
740
  const modDecay = modHold + voiceParams.modDecay;
@@ -698,11 +791,31 @@ export class MidyGMLite {
698
791
  note.modulationLFO.connect(note.volumeDepth);
699
792
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
700
793
  }
794
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
795
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
796
+ const cache = this.audioBufferCache.get(audioBufferId);
797
+ if (cache) {
798
+ cache.counter += 1;
799
+ if (cache.maxCount <= cache.counter) {
800
+ this.audioBufferCache.delete(audioBufferId);
801
+ }
802
+ return cache.audioBuffer;
803
+ }
804
+ else {
805
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
806
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
807
+ const cache = { audioBuffer, maxCount, counter: 1 };
808
+ this.audioBufferCache.set(audioBufferId, cache);
809
+ return audioBuffer;
810
+ }
811
+ }
701
812
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
702
813
  const state = channel.state;
703
- const voiceParams = voice.getAllParams(state.array);
814
+ const controllerState = this.getControllerState(channel, noteNumber, velocity);
815
+ const voiceParams = voice.getAllParams(controllerState);
704
816
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
705
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
817
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
818
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
706
819
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
707
820
  note.filterNode = new BiquadFilterNode(this.audioContext, {
708
821
  type: "lowpass",
@@ -726,10 +839,10 @@ export class MidyGMLite {
726
839
  if (soundFontIndex === undefined)
727
840
  return;
728
841
  const soundFont = this.soundFonts[soundFontIndex];
729
- const isSF3 = soundFont.parsed.info.version.major === 3;
730
842
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
731
843
  if (!voice)
732
844
  return;
845
+ const isSF3 = soundFont.parsed.info.version.major === 3;
733
846
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
734
847
  note.volumeEnvelopeNode.connect(channel.gainL);
735
848
  note.volumeEnvelopeNode.connect(channel.gainR);
@@ -998,10 +1111,10 @@ export class MidyGMLite {
998
1111
  123: this.allNotesOff,
999
1112
  };
1000
1113
  }
1001
- handleControlChange(channelNumber, controllerType, value) {
1114
+ handleControlChange(channelNumber, controllerType, value, startTime) {
1002
1115
  const handler = this.controlChangeHandlers[controllerType];
1003
1116
  if (handler) {
1004
- handler.call(this, channelNumber, value);
1117
+ handler.call(this, channelNumber, value, startTime);
1005
1118
  const channel = this.channels[channelNumber];
1006
1119
  this.applyVoiceParams(channel, controllerType + 128);
1007
1120
  }
@@ -1009,33 +1122,28 @@ export class MidyGMLite {
1009
1122
  console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
1010
1123
  }
1011
1124
  }
1012
- updateModulation(channel) {
1013
- const now = this.audioContext.currentTime;
1125
+ updateModulation(channel, scheduleTime) {
1126
+ scheduleTime ??= this.audioContext.currentTime;
1014
1127
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1015
- channel.scheduledNotes.forEach((noteList) => {
1016
- for (let i = 0; i < noteList.length; i++) {
1017
- const note = noteList[i];
1018
- if (!note)
1019
- continue;
1020
- if (note.modulationDepth) {
1021
- note.modulationDepth.gain.setValueAtTime(depth, now);
1022
- }
1023
- else {
1024
- this.setPitchEnvelope(note);
1025
- this.startModulation(channel, note, now);
1026
- }
1128
+ this.processScheduledNotes(channel, scheduleTime, (note) => {
1129
+ if (note.modulationDepth) {
1130
+ note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1131
+ }
1132
+ else {
1133
+ this.setPitchEnvelope(note, scheduleTime);
1134
+ this.startModulation(channel, note, scheduleTime);
1027
1135
  }
1028
1136
  });
1029
1137
  }
1030
- setModulationDepth(channelNumber, modulation) {
1138
+ setModulationDepth(channelNumber, modulation, scheduleTime) {
1031
1139
  const channel = this.channels[channelNumber];
1032
1140
  channel.state.modulationDepth = modulation / 127;
1033
- this.updateModulation(channel);
1141
+ this.updateModulation(channel, scheduleTime);
1034
1142
  }
1035
- setVolume(channelNumber, volume) {
1143
+ setVolume(channelNumber, volume, scheduleTime) {
1036
1144
  const channel = this.channels[channelNumber];
1037
1145
  channel.state.volume = volume / 127;
1038
- this.updateChannelVolume(channel);
1146
+ this.updateChannelVolume(channel, scheduleTime);
1039
1147
  }
1040
1148
  panToGain(pan) {
1041
1149
  const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
@@ -1044,31 +1152,31 @@ export class MidyGMLite {
1044
1152
  gainRight: Math.sin(theta),
1045
1153
  };
1046
1154
  }
1047
- setPan(channelNumber, pan) {
1155
+ setPan(channelNumber, pan, scheduleTime) {
1048
1156
  const channel = this.channels[channelNumber];
1049
1157
  channel.state.pan = pan / 127;
1050
- this.updateChannelVolume(channel);
1158
+ this.updateChannelVolume(channel, scheduleTime);
1051
1159
  }
1052
- setExpression(channelNumber, expression) {
1160
+ setExpression(channelNumber, expression, scheduleTime) {
1053
1161
  const channel = this.channels[channelNumber];
1054
1162
  channel.state.expression = expression / 127;
1055
- this.updateChannelVolume(channel);
1163
+ this.updateChannelVolume(channel, scheduleTime);
1056
1164
  }
1057
1165
  dataEntryLSB(channelNumber, value) {
1058
1166
  this.channels[channelNumber].dataLSB = value;
1059
1167
  this.handleRPN(channelNumber);
1060
1168
  }
1061
- updateChannelVolume(channel) {
1062
- const now = this.audioContext.currentTime;
1169
+ updateChannelVolume(channel, scheduleTime) {
1170
+ scheduleTime ??= this.audioContext.currentTime;
1063
1171
  const state = channel.state;
1064
1172
  const volume = state.volume * state.expression;
1065
1173
  const { gainLeft, gainRight } = this.panToGain(state.pan);
1066
1174
  channel.gainL.gain
1067
1175
  .cancelScheduledValues(now)
1068
- .setValueAtTime(volume * gainLeft, now);
1176
+ .setValueAtTime(volume * gainLeft, scheduleTime);
1069
1177
  channel.gainR.gain
1070
1178
  .cancelScheduledValues(now)
1071
- .setValueAtTime(volume * gainRight, now);
1179
+ .setValueAtTime(volume * gainRight, scheduleTime);
1072
1180
  }
1073
1181
  setSustainPedal(channelNumber, value) {
1074
1182
  this.channels[channelNumber].state.sustainPedal = value / 127;