@marmooo/midy 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -22,6 +22,7 @@ This library provides several files depending on the implementation level.
22
22
 
23
23
  - [@marmooo/midi-player](https://marmooo.github.io/midi-player/) - GUI library
24
24
  - [Humidy](https://marmooo.github.io/humidy/) - GM2 MIDI mixer app
25
+ - [Timidy](https://marmooo.github.io/timidy/) - Timidity++ style MIDI player
25
26
 
26
27
  ## Support Status
27
28
 
@@ -54,7 +55,7 @@ All implementations follow the specification.
54
55
  import { Midy } from "midy.js";
55
56
 
56
57
  const audioContext = new AudioContext();
57
- await audioContext.suspend();
58
+ if (audioContext.state === "running") await audioContext.suspend();
58
59
  const midy = new Midy(audioContext);
59
60
  await midy.loadMIDI("test.mid");
60
61
  await midy.loadSoundFont("test.sf3");
@@ -64,6 +65,7 @@ await midy.start();
64
65
  ### Playback
65
66
 
66
67
  ```js
68
+ midy.loop = true;
67
69
  await midy.start();
68
70
  await midy.stop();
69
71
  await midy.pause();
@@ -71,6 +73,17 @@ await midy.resume();
71
73
  midy.seekTo(second);
72
74
  ```
73
75
 
76
+ ### Events
77
+
78
+ ```js
79
+ midy.addEventListener("looped", func);
80
+ midy.addEventListener("started", func);
81
+ midy.addEventListener("stopped", func);
82
+ midy.addEventListener("paused", func);
83
+ midy.addEventListener("resumed", func);
84
+ midy.addEventListener("seeked", func);
85
+ ```
86
+
74
87
  ### MIDI Message
75
88
 
76
89
  There are functions that handle MIDI messages as they are, as well as simplified
package/esm/midy-GM1.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export class MidyGM1 {
1
+ export class MidyGM1 extends EventTarget {
2
2
  static channelSettings: {
3
3
  scheduleIndex: number;
4
4
  detune: number;
@@ -31,6 +31,7 @@ export class MidyGM1 {
31
31
  isPaused: boolean;
32
32
  isStopping: boolean;
33
33
  isSeeking: boolean;
34
+ loop: boolean;
34
35
  playPromise: any;
35
36
  timeline: any[];
36
37
  notePromises: any[];
@@ -69,7 +70,7 @@ export class MidyGM1 {
69
70
  createChannels(audioContext: any): any[];
70
71
  createAudioBuffer(voiceParams: any): Promise<any>;
71
72
  createBufferSource(voiceParams: any, audioBuffer: any): any;
72
- scheduleTimelineEvents(scheduleTime: any, queueIndex: any): Promise<any>;
73
+ scheduleTimelineEvents(scheduleTime: any, queueIndex: any): any;
73
74
  getQueueIndex(second: any): number;
74
75
  resetAllStates(): void;
75
76
  updateStates(queueIndex: any, nextQueueIndex: any): void;
@@ -91,8 +92,8 @@ export class MidyGM1 {
91
92
  seekTo(second: any): void;
92
93
  calcTotalTime(): number;
93
94
  currentTime(): number;
94
- processScheduledNotes(channel: any, callback: any): void;
95
- processActiveNotes(channel: any, scheduleTime: any, callback: any): void;
95
+ processScheduledNotes(channel: any, callback: any): Promise<void>;
96
+ processActiveNotes(channel: any, scheduleTime: any, callback: any): Promise<void>;
96
97
  cbToRatio(cb: any): number;
97
98
  rateToCent(rate: any): number;
98
99
  centToRate(cent: any): number;
@@ -112,13 +113,13 @@ export class MidyGM1 {
112
113
  noteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
113
114
  disconnectNote(note: any): void;
114
115
  releaseNote(channel: any, note: any, endTime: any): Promise<any>;
115
- noteOff(channelNumber: any, noteNumber: any, velocity: any, endTime: any, force: any): void;
116
+ noteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): any;
116
117
  setNoteIndex(channel: any, index: any): void;
117
118
  findNoteOffIndex(channel: any, noteNumber: any): any;
118
- releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): void[];
119
+ releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): any[];
119
120
  createMessageHandlers(): any[];
120
121
  handleMessage(data: any, scheduleTime: any): void;
121
- handleChannelMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
122
+ handleChannelMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): any;
122
123
  setProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
123
124
  handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
124
125
  setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AA+FA;IA2BE;;;;;;;;;;;MAWE;IAEF,+BAgBC;IAvDD,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,cAAa;IACb,iBAAY;IACZ,gBAAc;IACd,oBAAkB;IAClB,sBAAwB;IACxB,2BAAqC;IAiBnC,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,kDASC;IAED,4DASC;IAED,gEAoDC;IAED,mCAOC;IAED,uBASC;IAED,yDAgCC;IAED,2BAyEC;IAED,uDAEC;IAED,wDAEC;IAED,qCAKC;IAED;;;MAwDC;IAED,kGAeC;IAED,mGAeC;IAED,wEAMC;IAED,uBAMC;IAED,sBAIC;IAED,uBAMC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAIC;IAED,kEAWC;IAED,kFAYC;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,uEAwCC;IAED,0EAiBC;IAED,oEASC;IAED,0FAoBC;IAED,gCASC;IAED,iEAqBC;IAED,4FAmBC;IAED,6CAUC;IAED,qDAUC;IAED,qFAeC;IAED,+BAmBC;IAED,kDAOC;IAED,sFA2BC;IAED,mFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAYC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MAiCC;IAED,oFAMC;IAED,6EA2BC;IAED,qCAeC;IAED,+FAWC;IAED,wDAUC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,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"}
package/esm/midy-GM1.js CHANGED
@@ -26,12 +26,6 @@ class Note {
26
26
  writable: true,
27
27
  value: false
28
28
  });
29
- Object.defineProperty(this, "pending", {
30
- enumerable: true,
31
- configurable: true,
32
- writable: true,
33
- value: true
34
- });
35
29
  Object.defineProperty(this, "bufferSource", {
36
30
  enumerable: true,
37
31
  configurable: true,
@@ -77,6 +71,9 @@ class Note {
77
71
  this.noteNumber = noteNumber;
78
72
  this.velocity = velocity;
79
73
  this.startTime = startTime;
74
+ this.ready = new Promise((resolve) => {
75
+ this.resolveReady = resolve;
76
+ });
80
77
  }
81
78
  }
82
79
  // normalized to 0-1 for use with the SF2 modulator model
@@ -86,11 +83,11 @@ const defaultControllerState = {
86
83
  pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
87
84
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
88
85
  link: { type: 127, defaultValue: 0 },
89
- modulationDepth: { type: 128 + 1, defaultValue: 0 },
86
+ modulationDepthMSB: { type: 128 + 1, defaultValue: 0 },
90
87
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
91
- volume: { type: 128 + 7, defaultValue: 100 / 127 },
92
- pan: { type: 128 + 10, defaultValue: 64 / 127 },
93
- expression: { type: 128 + 11, defaultValue: 1 },
88
+ volumeMSB: { type: 128 + 7, defaultValue: 100 / 127 },
89
+ panMSB: { type: 128 + 10, defaultValue: 64 / 127 },
90
+ expressionMSB: { type: 128 + 11, defaultValue: 1 },
94
91
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
95
92
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
96
93
  // rpnLSB: { type: 128 + 100, defaultValue: 127 },
@@ -150,8 +147,9 @@ const pitchEnvelopeKeys = [
150
147
  "playbackRate",
151
148
  ];
152
149
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
153
- export class MidyGM1 {
150
+ export class MidyGM1 extends EventTarget {
154
151
  constructor(audioContext) {
152
+ super();
155
153
  Object.defineProperty(this, "mode", {
156
154
  enumerable: true,
157
155
  configurable: true,
@@ -266,6 +264,12 @@ export class MidyGM1 {
266
264
  writable: true,
267
265
  value: false
268
266
  });
267
+ Object.defineProperty(this, "loop", {
268
+ enumerable: true,
269
+ configurable: true,
270
+ writable: true,
271
+ value: false
272
+ });
269
273
  Object.defineProperty(this, "playPromise", {
270
274
  enumerable: true,
271
275
  configurable: true,
@@ -410,7 +414,7 @@ export class MidyGM1 {
410
414
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
411
415
  }
412
416
  createChannelAudioNodes(audioContext) {
413
- const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
417
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.panMSB.defaultValue);
414
418
  const gainL = new GainNode(audioContext, { gain: gainLeft });
415
419
  const gainR = new GainNode(audioContext, { gain: gainRight });
416
420
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -438,10 +442,9 @@ export class MidyGM1 {
438
442
  return channels;
439
443
  }
440
444
  async createAudioBuffer(voiceParams) {
441
- const sample = voiceParams.sample;
442
- const sampleStart = voiceParams.start;
443
- const sampleEnd = sample.data.length + voiceParams.end;
444
- const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
445
+ const { sample, start, end } = voiceParams;
446
+ const sampleEnd = sample.data.length + end;
447
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, start, sampleEnd);
445
448
  return audioBuffer;
446
449
  }
447
450
  createBufferSource(voiceParams, audioBuffer) {
@@ -454,7 +457,7 @@ export class MidyGM1 {
454
457
  }
455
458
  return bufferSource;
456
459
  }
457
- async scheduleTimelineEvents(scheduleTime, queueIndex) {
460
+ scheduleTimelineEvents(scheduleTime, queueIndex) {
458
461
  const timeOffset = this.resumeTime - this.startTime;
459
462
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
460
463
  const schedulingOffset = this.startDelay - timeOffset;
@@ -466,12 +469,10 @@ export class MidyGM1 {
466
469
  const startTime = event.startTime + schedulingOffset;
467
470
  switch (event.type) {
468
471
  case "noteOn":
469
- await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
472
+ this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
470
473
  break;
471
474
  case "noteOff": {
472
- const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
473
- if (notePromise)
474
- this.notePromises.push(notePromise);
475
+ this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
475
476
  break;
476
477
  }
477
478
  case "controller":
@@ -509,22 +510,23 @@ export class MidyGM1 {
509
510
  }
510
511
  }
511
512
  updateStates(queueIndex, nextQueueIndex) {
513
+ const now = this.audioContext.currentTime;
512
514
  if (nextQueueIndex < queueIndex)
513
515
  queueIndex = 0;
514
516
  for (let i = queueIndex; i < nextQueueIndex; i++) {
515
517
  const event = this.timeline[i];
516
518
  switch (event.type) {
517
519
  case "controller":
518
- this.setControlChange(event.channel, event.controllerType, event.value, 0);
520
+ this.setControlChange(event.channel, event.controllerType, event.value, now - this.resumeTime + event.startTime);
519
521
  break;
520
522
  case "programChange":
521
- this.setProgramChange(event.channel, event.programNumber, 0);
523
+ this.setProgramChange(event.channel, event.programNumber, now - this.resumeTime + event.startTime);
522
524
  break;
523
525
  case "pitchBend":
524
- this.setPitchBend(event.channel, event.value + 8192, 0);
526
+ this.setPitchBend(event.channel, event.value + 8192, now - this.resumeTime + event.startTime);
525
527
  break;
526
528
  case "sysEx":
527
- this.handleSysEx(event.data, 0);
529
+ this.handleSysEx(event.data, now - this.resumeTime + event.startTime);
528
530
  }
529
531
  }
530
532
  }
@@ -532,44 +534,80 @@ export class MidyGM1 {
532
534
  if (this.audioContext.state === "suspended") {
533
535
  await this.audioContext.resume();
534
536
  }
537
+ const paused = this.isPaused;
535
538
  this.isPlaying = true;
536
539
  this.isPaused = false;
537
540
  this.startTime = this.audioContext.currentTime;
541
+ if (paused) {
542
+ this.dispatchEvent(new Event("resumed"));
543
+ }
544
+ else {
545
+ this.dispatchEvent(new Event("started"));
546
+ }
538
547
  let queueIndex = this.getQueueIndex(this.resumeTime);
539
- let finished = false;
548
+ let exitReason;
540
549
  this.notePromises = [];
541
- while (queueIndex < this.timeline.length) {
550
+ while (true) {
542
551
  const now = this.audioContext.currentTime;
552
+ if (this.timeline.length <= queueIndex) {
553
+ await this.stopNotes(0, true, now);
554
+ if (this.loop) {
555
+ this.notePromises = [];
556
+ this.resetAllStates();
557
+ this.startTime = this.audioContext.currentTime;
558
+ this.resumeTime = 0;
559
+ queueIndex = 0;
560
+ this.dispatchEvent(new Event("looped"));
561
+ continue;
562
+ }
563
+ else {
564
+ await this.audioContext.suspend();
565
+ exitReason = "ended";
566
+ break;
567
+ }
568
+ }
543
569
  if (this.isPausing) {
544
570
  await this.stopNotes(0, true, now);
545
571
  await this.audioContext.suspend();
546
572
  this.notePromises = [];
573
+ this.isPausing = false;
574
+ exitReason = "paused";
547
575
  break;
548
576
  }
549
577
  else if (this.isStopping) {
550
578
  await this.stopNotes(0, true, now);
551
579
  await this.audioContext.suspend();
552
- finished = true;
580
+ this.isStopping = false;
581
+ exitReason = "stopped";
553
582
  break;
554
583
  }
555
584
  else if (this.isSeeking) {
556
- await this.stopNotes(0, true, now);
585
+ this.stopNotes(0, true, now);
557
586
  this.startTime = this.audioContext.currentTime;
558
587
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
559
588
  this.updateStates(queueIndex, nextQueueIndex);
560
589
  queueIndex = nextQueueIndex;
561
590
  this.isSeeking = false;
591
+ this.dispatchEvent(new Event("seeked"));
562
592
  continue;
563
593
  }
564
- queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
594
+ queueIndex = this.scheduleTimelineEvents(now, queueIndex);
565
595
  const waitTime = now + this.noteCheckInterval;
566
596
  await this.scheduleTask(() => { }, waitTime);
567
597
  }
568
- if (finished) {
598
+ if (exitReason !== "paused") {
569
599
  this.notePromises = [];
570
600
  this.resetAllStates();
571
601
  }
572
602
  this.isPlaying = false;
603
+ if (exitReason === "paused") {
604
+ this.isPaused = true;
605
+ this.dispatchEvent(new Event("paused"));
606
+ }
607
+ else {
608
+ this.isPaused = false;
609
+ this.dispatchEvent(new Event(exitReason));
610
+ }
573
611
  }
574
612
  ticksToSecond(ticks, secondsPerBeat) {
575
613
  return ticks * secondsPerBeat / this.ticksPerBeat;
@@ -676,24 +714,20 @@ export class MidyGM1 {
676
714
  return;
677
715
  this.isStopping = true;
678
716
  await this.playPromise;
679
- this.isStopping = false;
680
717
  }
681
718
  async pause() {
682
719
  if (!this.isPlaying || this.isPaused)
683
720
  return;
684
721
  const now = this.audioContext.currentTime;
685
- this.resumeTime = now - this.startTime - this.startDelay;
722
+ this.resumeTime = now + this.resumeTime - this.startTime;
686
723
  this.isPausing = true;
687
724
  await this.playPromise;
688
- this.isPausing = false;
689
- this.isPaused = true;
690
725
  }
691
726
  async resume() {
692
727
  if (!this.isPaused)
693
728
  return;
694
729
  this.playPromise = this.playNotes();
695
730
  await this.playPromise;
696
- this.isPaused = false;
697
731
  }
698
732
  seekTo(second) {
699
733
  this.resumeTime = second;
@@ -716,19 +750,23 @@ export class MidyGM1 {
716
750
  const now = this.audioContext.currentTime;
717
751
  return now + this.resumeTime - this.startTime;
718
752
  }
719
- processScheduledNotes(channel, callback) {
753
+ async processScheduledNotes(channel, callback) {
720
754
  const scheduledNotes = channel.scheduledNotes;
755
+ const tasks = [];
721
756
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
722
757
  const note = scheduledNotes[i];
723
758
  if (!note)
724
759
  continue;
725
760
  if (note.ending)
726
761
  continue;
727
- callback(note);
762
+ const task = note.ready.then(() => callback(note));
763
+ tasks.push(task);
728
764
  }
765
+ await Promise.all(tasks);
729
766
  }
730
- processActiveNotes(channel, scheduleTime, callback) {
767
+ async processActiveNotes(channel, scheduleTime, callback) {
731
768
  const scheduledNotes = channel.scheduledNotes;
769
+ const tasks = [];
732
770
  for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
733
771
  const note = scheduledNotes[i];
734
772
  if (!note)
@@ -737,8 +775,10 @@ export class MidyGM1 {
737
775
  continue;
738
776
  if (scheduleTime < note.startTime)
739
777
  break;
740
- callback(note);
778
+ const task = note.ready.then(() => callback(note));
779
+ tasks.push(task);
741
780
  }
781
+ await Promise.all(tasks);
742
782
  }
743
783
  cbToRatio(cb) {
744
784
  return Math.pow(10, cb / 200);
@@ -904,7 +944,13 @@ export class MidyGM1 {
904
944
  }
905
945
  note.bufferSource.connect(note.filterNode);
906
946
  note.filterNode.connect(note.volumeEnvelopeNode);
907
- note.bufferSource.start(startTime);
947
+ if (voiceParams.sample.type === "compressed") {
948
+ const offset = voiceParams.start / audioBuffer.sampleRate;
949
+ note.bufferSource.start(startTime, offset);
950
+ }
951
+ else {
952
+ note.bufferSource.start(startTime);
953
+ }
908
954
  return note;
909
955
  }
910
956
  handleExclusiveClass(note, channelNumber, startTime) {
@@ -954,11 +1000,7 @@ export class MidyGM1 {
954
1000
  return;
955
1001
  await this.setNoteAudioNode(channel, note, realtime);
956
1002
  this.setNoteRouting(channelNumber, note, startTime);
957
- note.pending = false;
958
- const off = note.offEvent;
959
- if (off) {
960
- this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
961
- }
1003
+ note.resolveReady();
962
1004
  }
963
1005
  disconnectNote(note) {
964
1006
  note.bufferSource.disconnect();
@@ -992,7 +1034,7 @@ export class MidyGM1 {
992
1034
  }, stopTime);
993
1035
  });
994
1036
  }
995
- noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1037
+ noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
996
1038
  const channel = this.channels[channelNumber];
997
1039
  if (!force && 0.5 <= channel.state.sustainPedal)
998
1040
  return;
@@ -1000,13 +1042,13 @@ export class MidyGM1 {
1000
1042
  if (index < 0)
1001
1043
  return;
1002
1044
  const note = channel.scheduledNotes[index];
1003
- if (note.pending) {
1004
- note.offEvent = { velocity, startTime: endTime };
1005
- return;
1006
- }
1007
1045
  note.ending = true;
1008
1046
  this.setNoteIndex(channel, index);
1009
- this.releaseNote(channel, note, endTime);
1047
+ const promise = note.ready.then(() => {
1048
+ return this.releaseNote(channel, note, endTime);
1049
+ });
1050
+ this.notePromises.push(promise);
1051
+ return promise;
1010
1052
  }
1011
1053
  setNoteIndex(channel, index) {
1012
1054
  let allEnds = true;
@@ -1092,7 +1134,8 @@ export class MidyGM1 {
1092
1134
  }
1093
1135
  setPitchBend(channelNumber, value, scheduleTime) {
1094
1136
  const channel = this.channels[channelNumber];
1095
- scheduleTime ??= this.audioContext.currentTime;
1137
+ if (!(0 <= scheduleTime))
1138
+ scheduleTime = this.audioContext.currentTime;
1096
1139
  const state = channel.state;
1097
1140
  const prev = state.pitchWheel * 2 - 1;
1098
1141
  const next = (value - 8192) / 8192;
@@ -1104,11 +1147,12 @@ export class MidyGM1 {
1104
1147
  setModLfoToPitch(channel, note, scheduleTime) {
1105
1148
  if (note.modulationDepth) {
1106
1149
  const modLfoToPitch = note.voiceParams.modLfoToPitch;
1107
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1108
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1150
+ const baseDepth = Math.abs(modLfoToPitch) +
1151
+ channel.state.modulationDepthMSB;
1152
+ const depth = baseDepth * Math.sign(modLfoToPitch);
1109
1153
  note.modulationDepth.gain
1110
1154
  .cancelScheduledValues(scheduleTime)
1111
- .setValueAtTime(modulationDepth, scheduleTime);
1155
+ .setValueAtTime(depth, scheduleTime);
1112
1156
  }
1113
1157
  else {
1114
1158
  this.startModulation(channel, note, scheduleTime);
@@ -1145,18 +1189,18 @@ export class MidyGM1 {
1145
1189
  createVoiceParamsHandlers() {
1146
1190
  return {
1147
1191
  modLfoToPitch: (channel, note, scheduleTime) => {
1148
- if (0 < channel.state.modulationDepth) {
1192
+ if (0 < channel.state.modulationDepthMSB) {
1149
1193
  this.setModLfoToPitch(channel, note, scheduleTime);
1150
1194
  }
1151
1195
  },
1152
1196
  vibLfoToPitch: (_channel, _note, _scheduleTime) => { },
1153
1197
  modLfoToFilterFc: (channel, note, scheduleTime) => {
1154
- if (0 < channel.state.modulationDepth) {
1198
+ if (0 < channel.state.modulationDepthMSB) {
1155
1199
  this.setModLfoToFilterFc(note, scheduleTime);
1156
1200
  }
1157
1201
  },
1158
1202
  modLfoToVolume: (channel, note, scheduleTime) => {
1159
- if (0 < channel.state.modulationDepth) {
1203
+ if (0 < channel.state.modulationDepthMSB) {
1160
1204
  this.setModLfoToVolume(note, scheduleTime);
1161
1205
  }
1162
1206
  },
@@ -1168,7 +1212,7 @@ export class MidyGM1 {
1168
1212
  }
1169
1213
  },
1170
1214
  freqModLFO: (_channel, note, scheduleTime) => {
1171
- if (0 < channel.state.modulationDepth) {
1215
+ if (0 < channel.state.modulationDepthMSB) {
1172
1216
  this.setFreqModLFO(note, scheduleTime);
1173
1217
  }
1174
1218
  },
@@ -1243,7 +1287,8 @@ export class MidyGM1 {
1243
1287
  }
1244
1288
  }
1245
1289
  updateModulation(channel, scheduleTime) {
1246
- const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1290
+ const depth = channel.state.modulationDepthMSB *
1291
+ channel.modulationDepthRange;
1247
1292
  this.processScheduledNotes(channel, (note) => {
1248
1293
  if (note.modulationDepth) {
1249
1294
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
@@ -1255,14 +1300,16 @@ export class MidyGM1 {
1255
1300
  }
1256
1301
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1257
1302
  const channel = this.channels[channelNumber];
1258
- scheduleTime ??= this.audioContext.currentTime;
1259
- channel.state.modulationDepth = modulation / 127;
1303
+ if (!(0 <= scheduleTime))
1304
+ scheduleTime = this.audioContext.currentTime;
1305
+ channel.state.modulationDepthMSB = modulation / 127;
1260
1306
  this.updateModulation(channel, scheduleTime);
1261
1307
  }
1262
1308
  setVolume(channelNumber, volume, scheduleTime) {
1263
- scheduleTime ??= this.audioContext.currentTime;
1309
+ if (!(0 <= scheduleTime))
1310
+ scheduleTime = this.audioContext.currentTime;
1264
1311
  const channel = this.channels[channelNumber];
1265
- channel.state.volume = volume / 127;
1312
+ channel.state.volumeMSB = volume / 127;
1266
1313
  this.updateChannelVolume(channel, scheduleTime);
1267
1314
  }
1268
1315
  panToGain(pan) {
@@ -1273,15 +1320,17 @@ export class MidyGM1 {
1273
1320
  };
1274
1321
  }
1275
1322
  setPan(channelNumber, pan, scheduleTime) {
1276
- scheduleTime ??= this.audioContext.currentTime;
1323
+ if (!(0 <= scheduleTime))
1324
+ scheduleTime = this.audioContext.currentTime;
1277
1325
  const channel = this.channels[channelNumber];
1278
- channel.state.pan = pan / 127;
1326
+ channel.state.panMSB = pan / 127;
1279
1327
  this.updateChannelVolume(channel, scheduleTime);
1280
1328
  }
1281
1329
  setExpression(channelNumber, expression, scheduleTime) {
1282
- scheduleTime ??= this.audioContext.currentTime;
1330
+ if (!(0 <= scheduleTime))
1331
+ scheduleTime = this.audioContext.currentTime;
1283
1332
  const channel = this.channels[channelNumber];
1284
- channel.state.expression = expression / 127;
1333
+ channel.state.expressionMSB = expression / 127;
1285
1334
  this.updateChannelVolume(channel, scheduleTime);
1286
1335
  }
1287
1336
  dataEntryLSB(channelNumber, value, scheduleTime) {
@@ -1290,8 +1339,8 @@ export class MidyGM1 {
1290
1339
  }
1291
1340
  updateChannelVolume(channel, scheduleTime) {
1292
1341
  const state = channel.state;
1293
- const volume = state.volume * state.expression;
1294
- const { gainLeft, gainRight } = this.panToGain(state.pan);
1342
+ const volume = state.volumeMSB * state.expressionMSB;
1343
+ const { gainLeft, gainRight } = this.panToGain(state.panMSB);
1295
1344
  channel.gainL.gain
1296
1345
  .cancelScheduledValues(scheduleTime)
1297
1346
  .setValueAtTime(volume * gainLeft, scheduleTime);
@@ -1301,7 +1350,8 @@ export class MidyGM1 {
1301
1350
  }
1302
1351
  setSustainPedal(channelNumber, value, scheduleTime) {
1303
1352
  const channel = this.channels[channelNumber];
1304
- scheduleTime ??= this.audioContext.currentTime;
1353
+ if (!(0 <= scheduleTime))
1354
+ scheduleTime = this.audioContext.currentTime;
1305
1355
  channel.state.sustainPedal = value / 127;
1306
1356
  if (64 <= value) {
1307
1357
  this.processScheduledNotes(channel, (note) => {
@@ -1373,7 +1423,8 @@ export class MidyGM1 {
1373
1423
  }
1374
1424
  setPitchBendRange(channelNumber, value, scheduleTime) {
1375
1425
  const channel = this.channels[channelNumber];
1376
- scheduleTime ??= this.audioContext.currentTime;
1426
+ if (!(0 <= scheduleTime))
1427
+ scheduleTime = this.audioContext.currentTime;
1377
1428
  const state = channel.state;
1378
1429
  const prev = state.pitchWheelSensitivity;
1379
1430
  const next = value / 12800;
@@ -1391,7 +1442,8 @@ export class MidyGM1 {
1391
1442
  }
1392
1443
  setFineTuning(channelNumber, value, scheduleTime) {
1393
1444
  const channel = this.channels[channelNumber];
1394
- scheduleTime ??= this.audioContext.currentTime;
1445
+ if (!(0 <= scheduleTime))
1446
+ scheduleTime = this.audioContext.currentTime;
1395
1447
  const prev = channel.fineTuning;
1396
1448
  const next = value;
1397
1449
  channel.fineTuning = next;
@@ -1406,7 +1458,8 @@ export class MidyGM1 {
1406
1458
  }
1407
1459
  setCoarseTuning(channelNumber, value, scheduleTime) {
1408
1460
  const channel = this.channels[channelNumber];
1409
- scheduleTime ??= this.audioContext.currentTime;
1461
+ if (!(0 <= scheduleTime))
1462
+ scheduleTime = this.audioContext.currentTime;
1410
1463
  const prev = channel.coarseTuning;
1411
1464
  const next = value;
1412
1465
  channel.coarseTuning = next;
@@ -1414,7 +1467,8 @@ export class MidyGM1 {
1414
1467
  this.updateChannelDetune(channel, scheduleTime);
1415
1468
  }
1416
1469
  allSoundOff(channelNumber, _value, scheduleTime) {
1417
- scheduleTime ??= this.audioContext.currentTime;
1470
+ if (!(0 <= scheduleTime))
1471
+ scheduleTime = this.audioContext.currentTime;
1418
1472
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
1419
1473
  }
1420
1474
  resetChannelStates(channelNumber) {
@@ -1439,8 +1493,8 @@ export class MidyGM1 {
1439
1493
  resetAllControllers(channelNumber, _value, scheduleTime) {
1440
1494
  const keys = [
1441
1495
  "pitchWheel",
1442
- "expression",
1443
- "modulationDepth",
1496
+ "expressionMSB",
1497
+ "modulationDepthMSB",
1444
1498
  "sustainPedal",
1445
1499
  ];
1446
1500
  const channel = this.channels[channelNumber];
@@ -1466,7 +1520,8 @@ export class MidyGM1 {
1466
1520
  }
1467
1521
  }
1468
1522
  allNotesOff(channelNumber, _value, scheduleTime) {
1469
- scheduleTime ??= this.audioContext.currentTime;
1523
+ if (!(0 <= scheduleTime))
1524
+ scheduleTime = this.audioContext.currentTime;
1470
1525
  return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
1471
1526
  }
1472
1527
  handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
@@ -1487,7 +1542,8 @@ export class MidyGM1 {
1487
1542
  }
1488
1543
  }
1489
1544
  GM1SystemOn(scheduleTime) {
1490
- scheduleTime ??= this.audioContext.currentTime;
1545
+ if (!(0 <= scheduleTime))
1546
+ scheduleTime = this.audioContext.currentTime;
1491
1547
  this.mode = "GM1";
1492
1548
  for (let i = 0; i < this.channels.length; i++) {
1493
1549
  this.allSoundOff(i, 0, scheduleTime);
@@ -1515,7 +1571,8 @@ export class MidyGM1 {
1515
1571
  this.setMasterVolume(volume, scheduleTime);
1516
1572
  }
1517
1573
  setMasterVolume(value, scheduleTime) {
1518
- scheduleTime ??= this.audioContext.currentTime;
1574
+ if (!(0 <= scheduleTime))
1575
+ scheduleTime = this.audioContext.currentTime;
1519
1576
  this.masterVolume.gain
1520
1577
  .cancelScheduledValues(scheduleTime)
1521
1578
  .setValueAtTime(value * value, scheduleTime);