@marmooo/midy 0.3.7 → 0.4.0

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.7",
3
+ "version": "0.4.0",
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.3",
25
+ "@marmooo/soundfont-parser": "^0.1.4",
26
26
  "midi-file": "^1.2.4"
27
27
  },
28
28
  "devDependencies": {
@@ -1,8 +1,8 @@
1
1
  export class MidyGM1 {
2
2
  static channelSettings: {
3
+ scheduleIndex: number;
3
4
  detune: number;
4
5
  programNumber: number;
5
- bank: number;
6
6
  dataMSB: number;
7
7
  dataLSB: number;
8
8
  rpnMSB: number;
@@ -22,9 +22,10 @@ export class MidyGM1 {
22
22
  startTime: number;
23
23
  resumeTime: number;
24
24
  soundFonts: any[];
25
- soundFontTable: any[];
25
+ soundFontTable: never[][];
26
26
  voiceCounter: Map<any, any>;
27
27
  voiceCache: Map<any, any>;
28
+ realtimeVoiceCache: Map<any, any>;
28
29
  isPlaying: boolean;
29
30
  isPausing: boolean;
30
31
  isPaused: boolean;
@@ -39,6 +40,7 @@ export class MidyGM1 {
39
40
  masterVolume: any;
40
41
  scheduler: any;
41
42
  schedulerBuffer: any;
43
+ messageHandlers: any[];
42
44
  voiceParamsHandlers: {
43
45
  modLfoToPitch: (channel: any, note: any, scheduleTime: any) => void;
44
46
  vibLfoToPitch: (_channel: any, _note: any, _scheduleTime: any) => void;
@@ -53,7 +55,6 @@ export class MidyGM1 {
53
55
  };
54
56
  controlChangeHandlers: any[];
55
57
  channels: any[];
56
- initSoundFontTable(): any[];
57
58
  addSoundFont(soundFont: any): void;
58
59
  toUint8Array(input: any): Promise<Uint8Array<ArrayBuffer>>;
59
60
  loadSoundFont(input: any): Promise<void>;
@@ -68,13 +69,14 @@ export class MidyGM1 {
68
69
  createChannels(audioContext: any): any[];
69
70
  createAudioBuffer(voiceParams: any): Promise<any>;
70
71
  createBufferSource(voiceParams: any, audioBuffer: any): any;
71
- scheduleTimelineEvents(t: any, resumeTime: any, queueIndex: any): Promise<any>;
72
+ scheduleTimelineEvents(scheduleTime: any, queueIndex: any): Promise<any>;
72
73
  getQueueIndex(second: any): number;
73
74
  resetAllStates(): void;
74
75
  updateStates(queueIndex: any, nextQueueIndex: any): void;
75
76
  playNotes(): Promise<void>;
76
77
  ticksToSecond(ticks: any, secondsPerBeat: any): number;
77
78
  secondToTicks(second: any, secondsPerBeat: any): number;
79
+ getSoundFontId(channel: any): string;
78
80
  extractMidiData(midi: any): {
79
81
  instruments: Set<any>;
80
82
  timeline: any[];
@@ -103,19 +105,20 @@ export class MidyGM1 {
103
105
  clampCutoffFrequency(frequency: any): number;
104
106
  setFilterEnvelope(note: any, scheduleTime: any): void;
105
107
  startModulation(channel: any, note: any, scheduleTime: any): void;
106
- getAudioBuffer(channel: any, noteNumber: any, velocity: any, voiceParams: any): Promise<any>;
107
- createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any): Promise<Note>;
108
+ getAudioBuffer(channel: any, noteNumber: any, velocity: any, voiceParams: any, realtime: any): Promise<any>;
109
+ setNoteAudioNode(channel: any, note: any, realtime: any): Promise<any>;
108
110
  handleExclusiveClass(note: any, channelNumber: any, startTime: any): void;
109
- scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
110
- noteOn(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<void>;
111
+ setNoteRouting(channelNumber: any, note: any, startTime: any): void;
112
+ noteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
111
113
  disconnectNote(note: any): void;
112
114
  releaseNote(channel: any, note: any, endTime: any): Promise<any>;
113
- scheduleNoteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): void;
115
+ noteOff(channelNumber: any, noteNumber: any, velocity: any, endTime: any, force: any): void;
114
116
  setNoteIndex(channel: any, index: any): void;
115
117
  findNoteOffIndex(channel: any, noteNumber: any): any;
116
- noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): void;
117
118
  releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): void[];
118
- handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
119
+ createMessageHandlers(): any[];
120
+ handleMessage(data: any, scheduleTime: any): void;
121
+ handleChannelMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
119
122
  setProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
120
123
  handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
121
124
  setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
@@ -172,26 +175,8 @@ export class MidyGM1 {
172
175
  GM1SystemOn(scheduleTime: any): void;
173
176
  handleUniversalRealTimeExclusiveMessage(data: any, scheduleTime: any): void;
174
177
  handleMasterVolumeSysEx(data: any, scheduleTime: any): void;
175
- setMasterVolume(volume: any, scheduleTime: any): void;
178
+ setMasterVolume(value: any, scheduleTime: any): void;
176
179
  handleSysEx(data: any, scheduleTime: any): void;
177
180
  scheduleTask(callback: any, scheduleTime: any): Promise<any>;
178
181
  }
179
- declare class Note {
180
- constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
181
- index: number;
182
- ending: boolean;
183
- bufferSource: any;
184
- filterNode: any;
185
- filterDepth: any;
186
- volumeEnvelopeNode: any;
187
- volumeDepth: any;
188
- modulationLFO: any;
189
- modulationDepth: any;
190
- noteNumber: any;
191
- velocity: any;
192
- startTime: any;
193
- voice: any;
194
- voiceParams: any;
195
- }
196
- export {};
197
182
  //# sourceMappingURL=midy-GM1.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AA4FA;IAyBE;;;;;;;;;;;MAWE;IAEF,+BAcC;IAnDD,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,iBAAY;IACZ,gBAAc;IACd,oBAAkB;IAClB,sBAAwB;IACxB,2BAAqC;IAgBnC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCASC;IAED,2DAYC;IAED,yCAmBC;IAED,oCASC;IAED,sBAoCC;IAED,8DAcC;IAED;;;;MAeC;IAED,yCAaC;IAED,kDAUC;IAED,4DASC;IAED,+EAkDC;IAED,mCAOC;IAED,uBAQC;IAED,yDA2BC;IAED,2BA+CC;IAED,uDAEC;IAED,wDAEC;IAED;;;MAgEC;IAED,kGAeC;IAED,mGAeC;IAED,wEAMC;IAED,uBAMC;IAED,sBAKC;IAED,uBAQC;IAED,wBAKC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,yDAQC;IAED,yEASC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,qCAMC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,6FAyBC;IAED,oGAuCC;IAED,0EAiBC;IAED,kGAmCC;IAED,6FASC;IAED,gCASC;IAED,iEAoBC;IAED,qGAeC;IAED,6CAUC;IAED,qDAUC;IAED,qFASC;IAED,sFAeC;IAED,oGA2BC;IAED,mFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAWC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MAiCC;IAED,oFAMC;IAED,6EA2BC;IAED,qCAeC;IAED,+FAWC;IAED,wDASC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,iEAMC;IAED,uEAQC;IAED,mEAKC;IAED,yEAQC;IAED,gFAGC;IAED,6CAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AAhiDD;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":"AA6FA;IA0BE;;;;;;;;;;;MAWE;IAEF,+BAeC;IArDD,aAAa;IACb,oBAAiB;IACjB,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,0BAAuD;IACvD,4BAAyB;IACzB,0BAAuB;IACvB,kCAA+B;IAC/B,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,iBAAY;IACZ,gBAAc;IACd,oBAAkB;IAClB,sBAAwB;IACxB,2BAAqC;IAgBnC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF,uBAAmD;IACnD;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,mCASC;IAED,2DAYC;IAED,yCAmBC;IAED,oCASC;IAED,sBAoCC;IAED,8DAWC;IAED;;;;MAeC;IAED,yCAaC;IAED,kDAUC;IAED,4DASC;IAED,yEAqDC;IAED,mCAOC;IAED,uBASC;IAED,yDA2BC;IAED,2BAwCC;IAED,uDAEC;IAED,wDAEC;IAED,qCAKC;IAED;;;MAwDC;IAED,kGAeC;IAED,mGAeC;IAED,wEAMC;IAED,uBAMC;IAED,sBAKC;IAED,uBAQC;IAED,wBAKC;IAED,0BAKC;IAED,wBAOC;IAED,sBAIC;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,4GAkCC;IAED,uEAmCC;IAED,0EAiBC;IAED,oEASC;IAED,0FAwBC;IAED,gCASC;IAED,iEAqBC;IAED,4FAmBC;IAED,6CAUC;IAED,qDAUC;IAED,sFAeC;IAED,+BAmBC;IAED,kDAOC;IAED,uGA2BC;IAED,mFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAWC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MAiCC;IAED,oFAMC;IAED,6EA2BC;IAED,qCAeC;IAED,+FAWC;IAED,wDASC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,iEAMC;IAED,uEAQC;IAED,mEAKC;IAED,yEAQC;IAED,gFAGC;IAED,6CAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCASC;IAED,4EAaC;IAED,4DAGC;IAED,qDAKC;IAED,gDAYC;IAGD,6DAgBC;CACF"}
@@ -4,7 +4,19 @@ exports.MidyGM1 = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
6
  class Note {
7
- constructor(noteNumber, velocity, startTime, voice, voiceParams) {
7
+ constructor(noteNumber, velocity, startTime) {
8
+ Object.defineProperty(this, "voice", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: void 0
13
+ });
14
+ Object.defineProperty(this, "voiceParams", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: void 0
19
+ });
8
20
  Object.defineProperty(this, "index", {
9
21
  enumerable: true,
10
22
  configurable: true,
@@ -17,6 +29,12 @@ class Note {
17
29
  writable: true,
18
30
  value: false
19
31
  });
32
+ Object.defineProperty(this, "pending", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: true
37
+ });
20
38
  Object.defineProperty(this, "bufferSource", {
21
39
  enumerable: true,
22
40
  configurable: true,
@@ -62,8 +80,6 @@ class Note {
62
80
  this.noteNumber = noteNumber;
63
81
  this.velocity = velocity;
64
82
  this.startTime = startTime;
65
- this.voice = voice;
66
- this.voiceParams = voiceParams;
67
83
  }
68
84
  }
69
85
  // normalized to 0-1 for use with the SF2 modulator model
@@ -203,7 +219,7 @@ class MidyGM1 {
203
219
  enumerable: true,
204
220
  configurable: true,
205
221
  writable: true,
206
- value: this.initSoundFontTable()
222
+ value: Array.from({ length: 128 }, () => [])
207
223
  });
208
224
  Object.defineProperty(this, "voiceCounter", {
209
225
  enumerable: true,
@@ -217,6 +233,12 @@ class MidyGM1 {
217
233
  writable: true,
218
234
  value: new Map()
219
235
  });
236
+ Object.defineProperty(this, "realtimeVoiceCache", {
237
+ enumerable: true,
238
+ configurable: true,
239
+ writable: true,
240
+ value: new Map()
241
+ });
220
242
  Object.defineProperty(this, "isPlaying", {
221
243
  enumerable: true,
222
244
  configurable: true,
@@ -284,6 +306,7 @@ class MidyGM1 {
284
306
  length: 1,
285
307
  sampleRate: audioContext.sampleRate,
286
308
  });
309
+ this.messageHandlers = this.createMessageHandlers();
287
310
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
288
311
  this.controlChangeHandlers = this.createControlChangeHandlers();
289
312
  this.channels = this.createChannels(audioContext);
@@ -291,21 +314,14 @@ class MidyGM1 {
291
314
  this.scheduler.connect(audioContext.destination);
292
315
  this.GM1SystemOn();
293
316
  }
294
- initSoundFontTable() {
295
- const table = new Array(128);
296
- for (let i = 0; i < 128; i++) {
297
- table[i] = new Map();
298
- }
299
- return table;
300
- }
301
317
  addSoundFont(soundFont) {
302
318
  const index = this.soundFonts.length;
303
319
  this.soundFonts.push(soundFont);
304
320
  const presetHeaders = soundFont.parsed.presetHeaders;
321
+ const soundFontTable = this.soundFontTable;
305
322
  for (let i = 0; i < presetHeaders.length; i++) {
306
- const presetHeader = presetHeaders[i];
307
- const banks = this.soundFontTable[presetHeader.preset];
308
- banks.set(presetHeader.bank, index);
323
+ const { preset, bank } = presetHeaders[i];
324
+ soundFontTable[preset][bank] = index;
309
325
  }
310
326
  }
311
327
  async toUint8Array(input) {
@@ -383,13 +399,16 @@ class MidyGM1 {
383
399
  this.GM1SystemOn();
384
400
  }
385
401
  getVoiceId(channel, noteNumber, velocity) {
386
- const bankNumber = this.calcBank(channel);
387
- const soundFontIndex = this.soundFontTable[channel.programNumber]
388
- .get(bankNumber);
402
+ const programNumber = channel.programNumber;
403
+ const bankTable = this.soundFontTable[programNumber];
404
+ if (!bankTable)
405
+ return;
406
+ const bank = channel.isDrum ? 128 : 0;
407
+ const soundFontIndex = bankTable[bank];
389
408
  if (soundFontIndex === undefined)
390
409
  return;
391
410
  const soundFont = this.soundFonts[soundFontIndex];
392
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
411
+ const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
393
412
  const { instrument, sampleID } = voice.generators;
394
413
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
395
414
  }
@@ -438,19 +457,22 @@ class MidyGM1 {
438
457
  }
439
458
  return bufferSource;
440
459
  }
441
- async scheduleTimelineEvents(t, resumeTime, queueIndex) {
442
- while (queueIndex < this.timeline.length) {
443
- const event = this.timeline[queueIndex];
444
- if (event.startTime > t + this.lookAhead)
460
+ async scheduleTimelineEvents(scheduleTime, queueIndex) {
461
+ const timeOffset = this.resumeTime - this.startTime;
462
+ const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
463
+ const schedulingOffset = this.startDelay - timeOffset;
464
+ const timeline = this.timeline;
465
+ while (queueIndex < timeline.length) {
466
+ const event = timeline[queueIndex];
467
+ if (lookAheadCheckTime < event.startTime)
445
468
  break;
446
- const delay = this.startDelay - resumeTime;
447
- const startTime = event.startTime + delay;
469
+ const startTime = event.startTime + schedulingOffset;
448
470
  switch (event.type) {
449
471
  case "noteOn":
450
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
472
+ await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
451
473
  break;
452
474
  case "noteOff": {
453
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
475
+ const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
454
476
  if (notePromise)
455
477
  this.notePromises.push(notePromise);
456
478
  break;
@@ -483,6 +505,7 @@ class MidyGM1 {
483
505
  this.exclusiveClassNotes.fill(undefined);
484
506
  this.drumExclusiveClassNotes.fill(undefined);
485
507
  this.voiceCache.clear();
508
+ this.realtimeVoiceCache.clear();
486
509
  for (let i = 0; i < this.channels.length; i++) {
487
510
  this.channels[i].scheduledNotes = [];
488
511
  this.resetChannelStates(i);
@@ -516,13 +539,10 @@ class MidyGM1 {
516
539
  this.isPaused = false;
517
540
  this.startTime = this.audioContext.currentTime;
518
541
  let queueIndex = this.getQueueIndex(this.resumeTime);
519
- let resumeTime = this.resumeTime - this.startTime;
520
542
  let finished = false;
521
543
  this.notePromises = [];
522
544
  while (queueIndex < this.timeline.length) {
523
545
  const now = this.audioContext.currentTime;
524
- const t = now + resumeTime;
525
- queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
526
546
  if (this.isPausing) {
527
547
  await this.stopNotes(0, true, now);
528
548
  await this.audioContext.suspend();
@@ -541,10 +561,10 @@ class MidyGM1 {
541
561
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
542
562
  this.updateStates(queueIndex, nextQueueIndex);
543
563
  queueIndex = nextQueueIndex;
544
- resumeTime = this.resumeTime - this.startTime;
545
564
  this.isSeeking = false;
546
565
  continue;
547
566
  }
567
+ queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
548
568
  const waitTime = now + this.noteCheckInterval;
549
569
  await this.scheduleTask(() => { }, waitTime);
550
570
  }
@@ -560,16 +580,16 @@ class MidyGM1 {
560
580
  secondToTicks(second, secondsPerBeat) {
561
581
  return second * this.ticksPerBeat / secondsPerBeat;
562
582
  }
583
+ getSoundFontId(channel) {
584
+ const programNumber = channel.programNumber;
585
+ const bank = channel.isDrum ? "128" : "000";
586
+ const program = programNumber.toString().padStart(3, "0");
587
+ return `${bank}:${program}`;
588
+ }
563
589
  extractMidiData(midi) {
564
590
  const instruments = new Set();
565
591
  const timeline = [];
566
- const tmpChannels = new Array(this.channels.length);
567
- for (let i = 0; i < tmpChannels.length; i++) {
568
- tmpChannels[i] = {
569
- programNumber: -1,
570
- bank: this.channels[i].bank,
571
- };
572
- }
592
+ const channels = this.channels;
573
593
  for (let i = 0; i < midi.tracks.length; i++) {
574
594
  const track = midi.tracks[i];
575
595
  let currentTicks = 0;
@@ -579,17 +599,15 @@ class MidyGM1 {
579
599
  event.ticks = currentTicks;
580
600
  switch (event.type) {
581
601
  case "noteOn": {
582
- const channel = tmpChannels[event.channel];
583
- if (channel.programNumber < 0) {
584
- instruments.add(`${channel.bank}:0`);
585
- channel.programNumber = 0;
586
- }
602
+ const channel = channels[event.channel];
603
+ instruments.add(this.getSoundFontId(channel));
587
604
  break;
588
605
  }
589
606
  case "programChange": {
590
- const channel = tmpChannels[event.channel];
591
- channel.programNumber = event.programNumber;
592
- instruments.add(`${channel.bankNumber}:${channel.programNumber}`);
607
+ const channel = channels[event.channel];
608
+ this.setProgramChange(event.channel, event.programNumber);
609
+ instruments.add(this.getSoundFontId(channel));
610
+ break;
593
611
  }
594
612
  }
595
613
  delete event.deltaTime;
@@ -624,7 +642,7 @@ class MidyGM1 {
624
642
  const channel = this.channels[channelNumber];
625
643
  const promises = [];
626
644
  this.processActiveNotes(channel, scheduleTime, (note) => {
627
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
645
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
628
646
  this.notePromises.push(promise);
629
647
  promises.push(promise);
630
648
  });
@@ -634,7 +652,7 @@ class MidyGM1 {
634
652
  const channel = this.channels[channelNumber];
635
653
  const promises = [];
636
654
  this.processScheduledNotes(channel, (note) => {
637
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
655
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
638
656
  this.notePromises.push(promise);
639
657
  promises.push(promise);
640
658
  });
@@ -667,7 +685,7 @@ class MidyGM1 {
667
685
  if (!this.isPlaying || this.isPaused)
668
686
  return;
669
687
  const now = this.audioContext.currentTime;
670
- this.resumeTime += now - this.startTime - this.startDelay;
688
+ this.resumeTime = now - this.startTime - this.startDelay;
671
689
  this.isPausing = true;
672
690
  await this.playPromise;
673
691
  this.isPausing = false;
@@ -693,11 +711,13 @@ class MidyGM1 {
693
711
  if (totalTime < event.startTime)
694
712
  totalTime = event.startTime;
695
713
  }
696
- return totalTime;
714
+ return totalTime + this.startDelay;
697
715
  }
698
716
  currentTime() {
717
+ if (!this.isPlaying)
718
+ return this.resumeTime;
699
719
  const now = this.audioContext.currentTime;
700
- return this.resumeTime + now - this.startTime - this.startDelay;
720
+ return now + this.resumeTime - this.startTime;
701
721
  }
702
722
  processScheduledNotes(channel, callback) {
703
723
  const scheduledNotes = channel.scheduledNotes;
@@ -836,31 +856,42 @@ class MidyGM1 {
836
856
  note.modulationLFO.connect(note.volumeDepth);
837
857
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
838
858
  }
839
- async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
859
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
840
860
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
841
- const cache = this.voiceCache.get(audioBufferId);
842
- if (cache) {
843
- cache.counter += 1;
844
- if (cache.maxCount <= cache.counter) {
845
- this.voiceCache.delete(audioBufferId);
846
- }
847
- return cache.audioBuffer;
848
- }
849
- else {
850
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
861
+ if (realtime) {
862
+ const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
863
+ if (cachedAudioBuffer)
864
+ return cachedAudioBuffer;
851
865
  const audioBuffer = await this.createAudioBuffer(voiceParams);
852
- const cache = { audioBuffer, maxCount, counter: 1 };
853
- this.voiceCache.set(audioBufferId, cache);
866
+ this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
854
867
  return audioBuffer;
855
868
  }
869
+ else {
870
+ const cache = this.voiceCache.get(audioBufferId);
871
+ if (cache) {
872
+ cache.counter += 1;
873
+ if (cache.maxCount <= cache.counter) {
874
+ this.voiceCache.delete(audioBufferId);
875
+ }
876
+ return cache.audioBuffer;
877
+ }
878
+ else {
879
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
880
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
881
+ const cache = { audioBuffer, maxCount, counter: 1 };
882
+ this.voiceCache.set(audioBufferId, cache);
883
+ return audioBuffer;
884
+ }
885
+ }
856
886
  }
857
- async createNote(channel, voice, noteNumber, velocity, startTime) {
887
+ async setNoteAudioNode(channel, note, realtime) {
858
888
  const now = this.audioContext.currentTime;
889
+ const { noteNumber, velocity, startTime } = note;
859
890
  const state = channel.state;
860
891
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
861
- const voiceParams = voice.getAllParams(controllerState);
862
- const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
863
- const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
892
+ const voiceParams = note.voice.getAllParams(controllerState);
893
+ note.voiceParams = voiceParams;
894
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
864
895
  note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
865
896
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
866
897
  note.filterNode = new BiquadFilterNode(this.audioContext, {
@@ -887,37 +918,50 @@ class MidyGM1 {
887
918
  if (prev) {
888
919
  const [prevNote, prevChannelNumber] = prev;
889
920
  if (prevNote && !prevNote.ending) {
890
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
921
+ this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
891
922
  startTime, true);
892
923
  }
893
924
  }
894
925
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
895
926
  }
896
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
927
+ setNoteRouting(channelNumber, note, startTime) {
897
928
  const channel = this.channels[channelNumber];
898
- const bankNumber = channel.bank;
899
- const soundFontIndex = this.soundFontTable[channel.programNumber]
900
- .get(bankNumber);
901
- if (soundFontIndex === undefined)
902
- return;
903
- const soundFont = this.soundFonts[soundFontIndex];
904
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
905
- if (!voice)
906
- return;
907
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
908
- note.volumeEnvelopeNode.connect(channel.gainL);
909
- note.volumeEnvelopeNode.connect(channel.gainR);
929
+ const volumeEnvelopeNode = note.volumeEnvelopeNode;
930
+ volumeEnvelopeNode.connect(channel.gainL);
931
+ volumeEnvelopeNode.connect(channel.gainR);
910
932
  if (0.5 <= channel.state.sustainPedal) {
911
933
  channel.sustainNotes.push(note);
912
934
  }
913
935
  this.handleExclusiveClass(note, channelNumber, startTime);
936
+ }
937
+ async noteOn(channelNumber, noteNumber, velocity, startTime) {
938
+ const channel = this.channels[channelNumber];
939
+ const realtime = startTime === undefined;
940
+ if (realtime)
941
+ startTime = this.audioContext.currentTime;
942
+ const note = new Note(noteNumber, velocity, startTime);
914
943
  const scheduledNotes = channel.scheduledNotes;
915
944
  note.index = scheduledNotes.length;
916
945
  scheduledNotes.push(note);
917
- }
918
- noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
919
- scheduleTime ??= this.audioContext.currentTime;
920
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
946
+ const programNumber = channel.programNumber;
947
+ const bankTable = this.soundFontTable[programNumber];
948
+ if (!bankTable)
949
+ return;
950
+ const bank = channel.isDrum ? 128 : 0;
951
+ const soundFontIndex = bankTable[bank];
952
+ if (soundFontIndex === undefined)
953
+ return;
954
+ const soundFont = this.soundFonts[soundFontIndex];
955
+ note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
956
+ if (!note.voice)
957
+ return;
958
+ await this.setNoteAudioNode(channel, note, realtime);
959
+ this.setNoteRouting(channelNumber, note, startTime);
960
+ note.pending = false;
961
+ const off = note.offEvent;
962
+ if (off) {
963
+ this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
964
+ }
921
965
  }
922
966
  disconnectNote(note) {
923
967
  note.bufferSource.disconnect();
@@ -930,6 +974,7 @@ class MidyGM1 {
930
974
  }
931
975
  }
932
976
  releaseNote(channel, note, endTime) {
977
+ endTime ??= this.audioContext.currentTime;
933
978
  const volRelease = endTime + note.voiceParams.volRelease;
934
979
  const modRelease = endTime + note.voiceParams.modRelease;
935
980
  const stopTime = Math.min(volRelease, modRelease);
@@ -950,7 +995,7 @@ class MidyGM1 {
950
995
  }, stopTime);
951
996
  });
952
997
  }
953
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
998
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
954
999
  const channel = this.channels[channelNumber];
955
1000
  if (!force && 0.5 <= channel.state.sustainPedal)
956
1001
  return;
@@ -958,6 +1003,10 @@ class MidyGM1 {
958
1003
  if (index < 0)
959
1004
  return;
960
1005
  const note = channel.scheduledNotes[index];
1006
+ if (note.pending) {
1007
+ note.offEvent = { velocity, startTime: endTime };
1008
+ return;
1009
+ }
961
1010
  note.ending = true;
962
1011
  this.setNoteIndex(channel, index);
963
1012
  this.releaseNote(channel, note, endTime);
@@ -988,22 +1037,37 @@ class MidyGM1 {
988
1037
  }
989
1038
  return -1;
990
1039
  }
991
- noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
992
- scheduleTime ??= this.audioContext.currentTime;
993
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
994
- }
995
1040
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
996
1041
  const velocity = halfVelocity * 2;
997
1042
  const channel = this.channels[channelNumber];
998
1043
  const promises = [];
999
1044
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1000
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1045
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1001
1046
  promises.push(promise);
1002
1047
  }
1003
1048
  channel.sustainNotes = [];
1004
1049
  return promises;
1005
1050
  }
1006
- handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1051
+ createMessageHandlers() {
1052
+ const handlers = new Array(256);
1053
+ // Channel Message
1054
+ handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
1055
+ handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
1056
+ handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
1057
+ handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
1058
+ handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
1059
+ return handlers;
1060
+ }
1061
+ handleMessage(data, scheduleTime) {
1062
+ const status = data[0];
1063
+ if (status === 0xF0) {
1064
+ return this.handleSysEx(data.subarray(1), scheduleTime);
1065
+ }
1066
+ const handler = this.messageHandlers[status];
1067
+ if (handler)
1068
+ handler(data, scheduleTime);
1069
+ }
1070
+ handleChannelMessage(statusByte, data1, data2, scheduleTime) {
1007
1071
  const channelNumber = statusByte & 0x0F;
1008
1072
  const messageType = statusByte & 0xF0;
1009
1073
  switch (messageType) {
@@ -1431,10 +1495,8 @@ class MidyGM1 {
1431
1495
  for (let i = 0; i < this.channels.length; i++) {
1432
1496
  this.allSoundOff(i, 0, scheduleTime);
1433
1497
  const channel = this.channels[i];
1434
- channel.bank = 0;
1435
1498
  channel.isDrum = false;
1436
1499
  }
1437
- this.channels[9].bank = 128;
1438
1500
  this.channels[9].isDrum = true;
1439
1501
  }
1440
1502
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
@@ -1455,16 +1517,11 @@ class MidyGM1 {
1455
1517
  const volume = (data[5] * 128 + data[4]) / 16383;
1456
1518
  this.setMasterVolume(volume, scheduleTime);
1457
1519
  }
1458
- setMasterVolume(volume, scheduleTime) {
1520
+ setMasterVolume(value, scheduleTime) {
1459
1521
  scheduleTime ??= this.audioContext.currentTime;
1460
- if (volume < 0 && 1 < volume) {
1461
- console.error("Master Volume is out of range");
1462
- }
1463
- else {
1464
- this.masterVolume.gain
1465
- .cancelScheduledValues(scheduleTime)
1466
- .setValueAtTime(volume * volume, scheduleTime);
1467
- }
1522
+ this.masterVolume.gain
1523
+ .cancelScheduledValues(scheduleTime)
1524
+ .setValueAtTime(value * value, scheduleTime);
1468
1525
  }
1469
1526
  handleSysEx(data, scheduleTime) {
1470
1527
  switch (data[0]) {
@@ -1502,9 +1559,9 @@ Object.defineProperty(MidyGM1, "channelSettings", {
1502
1559
  configurable: true,
1503
1560
  writable: true,
1504
1561
  value: {
1562
+ scheduleIndex: 0,
1505
1563
  detune: 0,
1506
1564
  programNumber: 0,
1507
- bank: 0,
1508
1565
  dataMSB: 0,
1509
1566
  dataLSB: 0,
1510
1567
  rpnMSB: 127,