@marmooo/midy 0.2.4 → 0.2.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/esm/midy-GM2.js CHANGED
@@ -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", {
@@ -280,6 +332,18 @@ export class MidyGM2 {
280
332
  writable: true,
281
333
  value: this.initSoundFontTable()
282
334
  });
335
+ Object.defineProperty(this, "audioBufferCounter", {
336
+ enumerable: true,
337
+ configurable: true,
338
+ writable: true,
339
+ value: new Map()
340
+ });
341
+ Object.defineProperty(this, "audioBufferCache", {
342
+ enumerable: true,
343
+ configurable: true,
344
+ writable: true,
345
+ value: new Map()
346
+ });
283
347
  Object.defineProperty(this, "isPlaying", {
284
348
  enumerable: true,
285
349
  configurable: true,
@@ -332,7 +396,7 @@ export class MidyGM2 {
332
396
  enumerable: true,
333
397
  configurable: true,
334
398
  writable: true,
335
- value: new Map()
399
+ value: new SparseMap(128)
336
400
  });
337
401
  Object.defineProperty(this, "defaultOptions", {
338
402
  enumerable: true,
@@ -372,7 +436,7 @@ export class MidyGM2 {
372
436
  initSoundFontTable() {
373
437
  const table = new Array(128);
374
438
  for (let i = 0; i < 128; i++) {
375
- table[i] = new Map();
439
+ table[i] = new SparseMap(128);
376
440
  }
377
441
  return table;
378
442
  }
@@ -426,8 +490,8 @@ export class MidyGM2 {
426
490
  state: new ControllerState(),
427
491
  controlTable: this.initControlTable(),
428
492
  ...this.setChannelAudioNodes(audioContext),
429
- scheduledNotes: new Map(),
430
- sostenutoNotes: new Map(),
493
+ scheduledNotes: new SparseMap(128),
494
+ sostenutoNotes: new SparseMap(128),
431
495
  };
432
496
  });
433
497
  return channels;
@@ -461,9 +525,8 @@ export class MidyGM2 {
461
525
  return audioBuffer;
462
526
  }
463
527
  }
464
- async createNoteBufferNode(voiceParams, isSF3) {
528
+ createNoteBufferNode(audioBuffer, voiceParams) {
465
529
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
466
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
467
530
  bufferSource.buffer = audioBuffer;
468
531
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
469
532
  if (bufferSource.loop) {
@@ -552,6 +615,7 @@ export class MidyGM2 {
552
615
  await Promise.all(this.notePromises);
553
616
  this.notePromises = [];
554
617
  this.exclusiveClassMap.clear();
618
+ this.audioBufferCache.clear();
555
619
  resolve();
556
620
  return;
557
621
  }
@@ -567,8 +631,9 @@ export class MidyGM2 {
567
631
  }
568
632
  else if (this.isStopping) {
569
633
  await this.stopNotes(0, true);
570
- this.exclusiveClassMap.clear();
571
634
  this.notePromises = [];
635
+ this.exclusiveClassMap.clear();
636
+ this.audioBufferCache.clear();
572
637
  resolve();
573
638
  this.isStopping = false;
574
639
  this.isPaused = false;
@@ -599,6 +664,9 @@ export class MidyGM2 {
599
664
  secondToTicks(second, secondsPerBeat) {
600
665
  return second * this.ticksPerBeat / secondsPerBeat;
601
666
  }
667
+ getAudioBufferId(programNumber, noteNumber, velocity) {
668
+ return `${programNumber}:${noteNumber}:${velocity}`;
669
+ }
602
670
  extractMidiData(midi) {
603
671
  const instruments = new Set();
604
672
  const timeline = [];
@@ -620,6 +688,8 @@ export class MidyGM2 {
620
688
  switch (event.type) {
621
689
  case "noteOn": {
622
690
  const channel = tmpChannels[event.channel];
691
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
692
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
623
693
  if (channel.programNumber < 0) {
624
694
  channel.programNumber = event.programNumber;
625
695
  switch (channel.bankMSB) {
@@ -669,6 +739,10 @@ export class MidyGM2 {
669
739
  timeline.push(event);
670
740
  }
671
741
  }
742
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
743
+ if (count === 1)
744
+ this.audioBufferCounter.delete(audioBufferId);
745
+ }
672
746
  const priority = {
673
747
  controller: 0,
674
748
  sysEx: 1,
@@ -762,7 +836,7 @@ export class MidyGM2 {
762
836
  return this.resumeTime + now - this.startTime - this.startDelay;
763
837
  }
764
838
  getActiveNotes(channel, time) {
765
- const activeNotes = new Map();
839
+ const activeNotes = new SparseMap(128);
766
840
  channel.scheduledNotes.forEach((noteList) => {
767
841
  const activeNote = this.getActiveNote(noteList, time);
768
842
  if (activeNote) {
@@ -1097,12 +1171,31 @@ export class MidyGM2 {
1097
1171
  note.vibratoLFO.connect(note.vibratoDepth);
1098
1172
  note.vibratoDepth.connect(note.bufferSource.detune);
1099
1173
  }
1174
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1175
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1176
+ const cache = this.audioBufferCache.get(audioBufferId);
1177
+ if (cache) {
1178
+ cache.counter += 1;
1179
+ if (cache.maxCount <= cache.counter) {
1180
+ this.audioBufferCache.delete(audioBufferId);
1181
+ }
1182
+ return cache.audioBuffer;
1183
+ }
1184
+ else {
1185
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1186
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1187
+ const cache = { audioBuffer, maxCount, counter: 1 };
1188
+ this.audioBufferCache.set(audioBufferId, cache);
1189
+ return audioBuffer;
1190
+ }
1191
+ }
1100
1192
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1101
1193
  const state = channel.state;
1102
1194
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1103
1195
  const voiceParams = voice.getAllParams(controllerState);
1104
1196
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1105
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1197
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1198
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1106
1199
  note.volumeNode = new GainNode(this.audioContext);
1107
1200
  note.gainL = new GainNode(this.audioContext);
1108
1201
  note.gainR = new GainNode(this.audioContext);
@@ -1162,10 +1255,10 @@ export class MidyGM2 {
1162
1255
  if (soundFontIndex === undefined)
1163
1256
  return;
1164
1257
  const soundFont = this.soundFonts[soundFontIndex];
1165
- const isSF3 = soundFont.parsed.info.version.major === 3;
1166
1258
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1167
1259
  if (!voice)
1168
1260
  return;
1261
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1169
1262
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1170
1263
  note.gainL.connect(channel.gainL);
1171
1264
  note.gainR.connect(channel.gainR);
@@ -1334,6 +1427,7 @@ export class MidyGM2 {
1334
1427
  channel.program = program;
1335
1428
  }
1336
1429
  handleChannelPressure(channelNumber, value) {
1430
+ const now = this.audioContext.currentTime;
1337
1431
  const channel = this.channels[channelNumber];
1338
1432
  const prev = channel.state.channelPressure;
1339
1433
  const next = value / 127;
@@ -1343,13 +1437,8 @@ export class MidyGM2 {
1343
1437
  channel.detune += pressureDepth * (next - prev);
1344
1438
  }
1345
1439
  const table = channel.channelPressureTable;
1346
- channel.scheduledNotes.forEach((noteList) => {
1347
- for (let i = 0; i < noteList.length; i++) {
1348
- const note = noteList[i];
1349
- if (!note)
1350
- continue;
1351
- this.applyDestinationSettings(channel, note, table);
1352
- }
1440
+ this.getActiveNotes(channel, now).forEach((note) => {
1441
+ this.applyDestinationSettings(channel, note, table);
1353
1442
  });
1354
1443
  // this.applyVoiceParams(channel, 13);
1355
1444
  }
@@ -1750,8 +1839,7 @@ export class MidyGM2 {
1750
1839
  channel.state.sostenutoPedal = value / 127;
1751
1840
  if (64 <= value) {
1752
1841
  const now = this.audioContext.currentTime;
1753
- const activeNotes = this.getActiveNotes(channel, now);
1754
- channel.sostenutoNotes = new Map(activeNotes);
1842
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1755
1843
  }
1756
1844
  else {
1757
1845
  this.releaseSostenutoPedal(channelNumber, value);
@@ -2004,7 +2092,7 @@ export class MidyGM2 {
2004
2092
  switch (data[3]) {
2005
2093
  case 8:
2006
2094
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2007
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2095
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2008
2096
  default:
2009
2097
  console.warn(`Unsupported Exclusive Message: ${data}`);
2010
2098
  }
@@ -2326,8 +2414,8 @@ export class MidyGM2 {
2326
2414
  }
2327
2415
  return bitmap;
2328
2416
  }
2329
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2330
- if (data.length < 18) {
2417
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2418
+ if (data.length < 19) {
2331
2419
  console.error("Data length is too short");
2332
2420
  return;
2333
2421
  }
@@ -2335,10 +2423,13 @@ export class MidyGM2 {
2335
2423
  for (let i = 0; i < channelBitmap.length; i++) {
2336
2424
  if (!channelBitmap[i])
2337
2425
  continue;
2426
+ const channel = this.channels[i];
2338
2427
  for (let j = 0; j < 12; j++) {
2339
- const value = data[j + 7] - 64; // cent
2340
- this.channels[i].scaleOctaveTuningTable[j] = value;
2428
+ const centValue = data[j + 7] - 64;
2429
+ channel.scaleOctaveTuningTable[j] = centValue;
2341
2430
  }
2431
+ if (realtime)
2432
+ this.updateChannelDetune(channel);
2342
2433
  }
2343
2434
  }
2344
2435
  applyDestinationSettings(channel, note, table) {
@@ -2470,7 +2561,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2470
2561
  value: {
2471
2562
  currentBufferSource: null,
2472
2563
  detune: 0,
2473
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2564
+ scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
2474
2565
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2475
2566
  keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
2476
2567
  program: 0,
@@ -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: {
@@ -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,7 @@ 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
+ getActiveNotes(channel: any, time: any): SparseMap;
91
94
  getActiveNote(noteList: any, time: any): any;
92
95
  cbToRatio(cb: any): number;
93
96
  rateToCent(rate: any): number;
@@ -101,6 +104,7 @@ export class MidyGMLite {
101
104
  clampCutoffFrequency(frequency: any): number;
102
105
  setFilterEnvelope(note: any): void;
103
106
  startModulation(channel: any, note: any, startTime: any): void;
107
+ getAudioBuffer(program: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
104
108
  createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, isSF3: any): Promise<Note>;
105
109
  scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
106
110
  noteOn(channelNumber: any, noteNumber: any, velocity: any): Promise<void>;
@@ -177,6 +181,19 @@ export class MidyGMLite {
177
181
  handleSysEx(data: any): void;
178
182
  scheduleTask(callback: any, startTime: any): Promise<any>;
179
183
  }
184
+ declare class SparseMap {
185
+ constructor(size: any);
186
+ data: any[];
187
+ activeIndices: any[];
188
+ set(key: any, value: any): void;
189
+ get(key: any): any;
190
+ delete(key: any): boolean;
191
+ has(key: any): boolean;
192
+ get size(): number;
193
+ clear(): void;
194
+ forEach(callback: any): void;
195
+ [Symbol.iterator](): Generator<any[], void, unknown>;
196
+ }
180
197
  declare class Note {
181
198
  constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
182
199
  bufferSource: 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":"AAgJA;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,2EA+CC;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,mDASC;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,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,+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;AAv0CD;IACE,uBAGC;IAFC,YAA2B;IAC3B,qBAAuB;IAGzB,gCAKC;IAED,mBAEC;IAED,0BAUC;IAED,uBAEC;IAED,mBAEC;IAED,cAMC;IASD,6BAKC;IAZD,qDAKC;CAQF;AAED;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,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", {
@@ -166,6 +218,18 @@ export class MidyGMLite {
166
218
  writable: true,
167
219
  value: this.initSoundFontTable()
168
220
  });
221
+ Object.defineProperty(this, "audioBufferCounter", {
222
+ enumerable: true,
223
+ configurable: true,
224
+ writable: true,
225
+ value: new Map()
226
+ });
227
+ Object.defineProperty(this, "audioBufferCache", {
228
+ enumerable: true,
229
+ configurable: true,
230
+ writable: true,
231
+ value: new Map()
232
+ });
169
233
  Object.defineProperty(this, "isPlaying", {
170
234
  enumerable: true,
171
235
  configurable: true,
@@ -218,7 +282,7 @@ export class MidyGMLite {
218
282
  enumerable: true,
219
283
  configurable: true,
220
284
  writable: true,
221
- value: new Map()
285
+ value: new SparseMap(128)
222
286
  });
223
287
  this.audioContext = audioContext;
224
288
  this.masterVolume = new GainNode(audioContext);
@@ -231,7 +295,7 @@ export class MidyGMLite {
231
295
  initSoundFontTable() {
232
296
  const table = new Array(128);
233
297
  for (let i = 0; i < 128; i++) {
234
- table[i] = new Map();
298
+ table[i] = new SparseMap(128);
235
299
  }
236
300
  return table;
237
301
  }
@@ -284,7 +348,7 @@ export class MidyGMLite {
284
348
  ...this.constructor.channelSettings,
285
349
  state: new ControllerState(),
286
350
  ...this.setChannelAudioNodes(audioContext),
287
- scheduledNotes: new Map(),
351
+ scheduledNotes: new SparseMap(128),
288
352
  };
289
353
  });
290
354
  return channels;
@@ -318,9 +382,8 @@ export class MidyGMLite {
318
382
  return audioBuffer;
319
383
  }
320
384
  }
321
- async createNoteBufferNode(voiceParams, isSF3) {
385
+ createNoteBufferNode(audioBuffer, voiceParams) {
322
386
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
323
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
324
387
  bufferSource.buffer = audioBuffer;
325
388
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
326
389
  if (bufferSource.loop) {
@@ -385,6 +448,7 @@ export class MidyGMLite {
385
448
  await Promise.all(this.notePromises);
386
449
  this.notePromises = [];
387
450
  this.exclusiveClassMap.clear();
451
+ this.audioBufferCache.clear();
388
452
  resolve();
389
453
  return;
390
454
  }
@@ -400,8 +464,9 @@ export class MidyGMLite {
400
464
  }
401
465
  else if (this.isStopping) {
402
466
  await this.stopNotes(0, true);
403
- this.exclusiveClassMap.clear();
404
467
  this.notePromises = [];
468
+ this.exclusiveClassMap.clear();
469
+ this.audioBufferCache.clear();
405
470
  resolve();
406
471
  this.isStopping = false;
407
472
  this.isPaused = false;
@@ -432,6 +497,9 @@ export class MidyGMLite {
432
497
  secondToTicks(second, secondsPerBeat) {
433
498
  return second * this.ticksPerBeat / secondsPerBeat;
434
499
  }
500
+ getAudioBufferId(programNumber, noteNumber, velocity) {
501
+ return `${programNumber}:${noteNumber}:${velocity}`;
502
+ }
435
503
  extractMidiData(midi) {
436
504
  const instruments = new Set();
437
505
  const timeline = [];
@@ -452,6 +520,8 @@ export class MidyGMLite {
452
520
  switch (event.type) {
453
521
  case "noteOn": {
454
522
  const channel = tmpChannels[event.channel];
523
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
524
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
455
525
  if (channel.programNumber < 0) {
456
526
  instruments.add(`${channel.bank}:0`);
457
527
  channel.programNumber = 0;
@@ -468,6 +538,10 @@ export class MidyGMLite {
468
538
  timeline.push(event);
469
539
  }
470
540
  }
541
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
542
+ if (count === 1)
543
+ this.audioBufferCounter.delete(audioBufferId);
544
+ }
471
545
  const priority = {
472
546
  controller: 0,
473
547
  sysEx: 1,
@@ -558,7 +632,7 @@ export class MidyGMLite {
558
632
  return this.resumeTime + now - this.startTime - this.startDelay;
559
633
  }
560
634
  getActiveNotes(channel, time) {
561
- const activeNotes = new Map();
635
+ const activeNotes = new SparseMap(128);
562
636
  channel.scheduledNotes.forEach((noteList) => {
563
637
  const activeNote = this.getActiveNote(noteList, time);
564
638
  if (activeNote) {
@@ -698,11 +772,31 @@ export class MidyGMLite {
698
772
  note.modulationLFO.connect(note.volumeDepth);
699
773
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
700
774
  }
775
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
776
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
777
+ const cache = this.audioBufferCache.get(audioBufferId);
778
+ if (cache) {
779
+ cache.counter += 1;
780
+ if (cache.maxCount <= cache.counter) {
781
+ this.audioBufferCache.delete(audioBufferId);
782
+ }
783
+ return cache.audioBuffer;
784
+ }
785
+ else {
786
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
787
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
788
+ const cache = { audioBuffer, maxCount, counter: 1 };
789
+ this.audioBufferCache.set(audioBufferId, cache);
790
+ return audioBuffer;
791
+ }
792
+ }
701
793
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
702
794
  const state = channel.state;
703
- const voiceParams = voice.getAllParams(state.array);
795
+ const controllerState = this.getControllerState(channel, noteNumber, velocity);
796
+ const voiceParams = voice.getAllParams(controllerState);
704
797
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
705
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
798
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
799
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
706
800
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
707
801
  note.filterNode = new BiquadFilterNode(this.audioContext, {
708
802
  type: "lowpass",
@@ -726,10 +820,10 @@ export class MidyGMLite {
726
820
  if (soundFontIndex === undefined)
727
821
  return;
728
822
  const soundFont = this.soundFonts[soundFontIndex];
729
- const isSF3 = soundFont.parsed.info.version.major === 3;
730
823
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
731
824
  if (!voice)
732
825
  return;
826
+ const isSF3 = soundFont.parsed.info.version.major === 3;
733
827
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
734
828
  note.volumeEnvelopeNode.connect(channel.gainL);
735
829
  note.volumeEnvelopeNode.connect(channel.gainR);
package/esm/midy.d.ts CHANGED
@@ -2,7 +2,7 @@ export class Midy {
2
2
  static channelSettings: {
3
3
  currentBufferSource: null;
4
4
  detune: number;
5
- scaleOctaveTuningTable: any[];
5
+ scaleOctaveTuningTable: Float32Array<ArrayBuffer>;
6
6
  channelPressureTable: Uint8Array<ArrayBuffer>;
7
7
  polyphonicKeyPressureTable: Uint8Array<ArrayBuffer>;
8
8
  keyBasedInstrumentControlTable: Int8Array<ArrayBuffer>;
@@ -48,6 +48,8 @@ export class Midy {
48
48
  resumeTime: number;
49
49
  soundFonts: any[];
50
50
  soundFontTable: any[];
51
+ audioBufferCounter: Map<any, any>;
52
+ audioBufferCache: Map<any, any>;
51
53
  isPlaying: boolean;
52
54
  isPausing: boolean;
53
55
  isPaused: boolean;
@@ -56,7 +58,7 @@ export class Midy {
56
58
  timeline: any[];
57
59
  instruments: any[];
58
60
  notePromises: any[];
59
- exclusiveClassMap: Map<any, any>;
61
+ exclusiveClassMap: SparseMap;
60
62
  defaultOptions: {
61
63
  reverbAlgorithm: (audioContext: any) => {
62
64
  input: any;
@@ -144,13 +146,14 @@ export class Midy {
144
146
  };
145
147
  createChannels(audioContext: any): any[];
146
148
  createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
147
- createNoteBufferNode(voiceParams: any, isSF3: any): Promise<any>;
149
+ createNoteBufferNode(audioBuffer: any, voiceParams: any): any;
148
150
  findPortamentoTarget(queueIndex: any): any;
149
151
  scheduleTimelineEvents(t: any, offset: any, queueIndex: any): Promise<any>;
150
152
  getQueueIndex(second: any): number;
151
153
  playNotes(): Promise<any>;
152
154
  ticksToSecond(ticks: any, secondsPerBeat: any): number;
153
155
  secondToTicks(second: any, secondsPerBeat: any): number;
156
+ getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
154
157
  extractMidiData(midi: any): {
155
158
  instruments: Set<any>;
156
159
  timeline: any[];
@@ -164,7 +167,7 @@ export class Midy {
164
167
  seekTo(second: any): void;
165
168
  calcTotalTime(): number;
166
169
  currentTime(): number;
167
- getActiveNotes(channel: any, time: any): Map<any, any>;
170
+ getActiveNotes(channel: any, time: any): SparseMap;
168
171
  getActiveNote(noteList: any, time: any): any;
169
172
  createConvolutionReverbImpulse(audioContext: any, decay: any, preDecay: any): any;
170
173
  createConvolutionReverb(audioContext: any, impulse: any): {
@@ -205,6 +208,7 @@ export class Midy {
205
208
  setFilterEnvelope(channel: any, note: any, pressure: any): void;
206
209
  startModulation(channel: any, note: any, startTime: any): void;
207
210
  startVibrato(channel: any, note: any, startTime: any): void;
211
+ getAudioBuffer(program: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
208
212
  createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, portamento: any, isSF3: any): Promise<Note>;
209
213
  calcBank(channel: any, channelNumber: any): any;
210
214
  scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any, portamento: any): Promise<void>;
@@ -362,7 +366,8 @@ export class Midy {
362
366
  setChorusSendToReverb(value: any): void;
363
367
  getChorusSendToReverb(value: any): number;
364
368
  getChannelBitmap(data: any): any[];
365
- handleScaleOctaveTuning1ByteFormatSysEx(data: any): void;
369
+ handleScaleOctaveTuning1ByteFormatSysEx(data: any, realtime: any): void;
370
+ handleScaleOctaveTuning2ByteFormatSysEx(data: any, realtime: any): void;
366
371
  applyDestinationSettings(channel: any, note: any, table: any): void;
367
372
  handleChannelPressureSysEx(data: any, tableName: any): void;
368
373
  initControlTable(): Uint8Array<ArrayBuffer>;
@@ -374,6 +379,19 @@ export class Midy {
374
379
  handleSysEx(data: any): any;
375
380
  scheduleTask(callback: any, startTime: any): Promise<any>;
376
381
  }
382
+ declare class SparseMap {
383
+ constructor(size: any);
384
+ data: any[];
385
+ activeIndices: any[];
386
+ set(key: any, value: any): void;
387
+ get(key: any): any;
388
+ delete(key: any): boolean;
389
+ has(key: any): boolean;
390
+ get size(): number;
391
+ clear(): void;
392
+ forEach(callback: any): void;
393
+ [Symbol.iterator](): Generator<any[], void, unknown>;
394
+ }
377
395
  declare class Note {
378
396
  constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
379
397
  bufferSource: any;