@marmooo/midy 0.2.7 → 0.2.9

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
@@ -77,8 +77,8 @@ There are functions that handle SysEx data as is, as well as simplified
77
77
  functions.
78
78
 
79
79
  ```js
80
- midy.handleSysEx(data); // [F0 F6 04 01 xx xx F7] data
81
- midy.handleMasterVolumeSysEx(data); // [F0 F6 04 01 xx xx F7] data
80
+ midy.handleSysEx(data, scheduleTime); // [F0 F6 04 01 xx xx F7] data
81
+ midy.handleMasterVolumeSysEx(data, scheduleTime); // [F0 F6 04 01 xx xx F7] data
82
82
  midy.setMasterVolume(volume); // [0-1] volume
83
83
  ```
84
84
 
package/esm/midy-GM1.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export class MidyGM1 {
2
2
  static channelSettings: {
3
3
  currentBufferSource: null;
4
+ isDrum: boolean;
4
5
  detune: number;
5
6
  program: number;
6
7
  bank: number;
@@ -8,11 +9,12 @@ export class MidyGM1 {
8
9
  dataLSB: number;
9
10
  rpnMSB: number;
10
11
  rpnLSB: number;
12
+ modulationDepthRange: number;
11
13
  fineTuning: number;
12
14
  coarseTuning: number;
13
- modulationDepthRange: number;
14
15
  };
15
16
  constructor(audioContext: any);
17
+ mode: string;
16
18
  ticksPerBeat: number;
17
19
  totalTime: number;
18
20
  noteCheckInterval: number;
@@ -35,6 +37,8 @@ export class MidyGM1 {
35
37
  exclusiveClassMap: SparseMap;
36
38
  audioContext: any;
37
39
  masterVolume: any;
40
+ scheduler: any;
41
+ schedulerBuffer: any;
38
42
  voiceParamsHandlers: {
39
43
  modLfoToPitch: (channel: any, note: any, _prevValue: any, scheduleTime: any) => void;
40
44
  vibLfoToPitch: (_channel: any, _note: any, _prevValue: any, _scheduleTime: any) => void;
@@ -73,7 +77,7 @@ export class MidyGM1 {
73
77
  };
74
78
  createChannels(audioContext: any): any[];
75
79
  createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
76
- createNoteBufferNode(audioBuffer: any, voiceParams: any): any;
80
+ createBufferSource(audioBuffer: any, voiceParams: any): any;
77
81
  scheduleTimelineEvents(t: any, offset: any, queueIndex: any): Promise<any>;
78
82
  getQueueIndex(second: any): number;
79
83
  playNotes(): Promise<any>;
@@ -93,7 +97,7 @@ export class MidyGM1 {
93
97
  seekTo(second: any): void;
94
98
  calcTotalTime(): number;
95
99
  currentTime(): number;
96
- processScheduledNotes(channel: any, scheduleTime: any, callback: any): void;
100
+ processScheduledNotes(channel: any, callback: any): void;
97
101
  getActiveNotes(channel: any, scheduleTime: any): SparseMap;
98
102
  getActiveNote(noteList: any, scheduleTime: any): any;
99
103
  cbToRatio(cb: any): number;
@@ -115,7 +119,7 @@ export class MidyGM1 {
115
119
  stopNote(endTime: any, stopTime: any, scheduledNotes: any, index: any): Promise<any>;
116
120
  scheduleNoteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): Promise<any> | undefined;
117
121
  noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<any> | undefined;
118
- releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): any[];
122
+ releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): (Promise<any> | undefined)[];
119
123
  handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<any>;
120
124
  handleProgramChange(channelNumber: any, program: any, _scheduleTime: any): void;
121
125
  handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
@@ -174,15 +178,15 @@ export class MidyGM1 {
174
178
  dataEntryMSB(channelNumber: any, value: any, scheduleTime: any): void;
175
179
  handlePitchBendRangeRPN(channelNumber: any, scheduleTime: any): void;
176
180
  setPitchBendRange(channelNumber: any, value: any, scheduleTime: any): void;
177
- handleFineTuningRPN(channelNumber: any): void;
178
- setFineTuning(channelNumber: any, value: any): void;
179
- handleCoarseTuningRPN(channelNumber: any): void;
180
- setCoarseTuning(channelNumber: any, value: any): void;
181
+ handleFineTuningRPN(channelNumber: any, scheduleTime: any): void;
182
+ setFineTuning(channelNumber: any, value: any, scheduleTime: any): void;
183
+ handleCoarseTuningRPN(channelNumber: any, scheduleTime: any): void;
184
+ setCoarseTuning(channelNumber: any, value: any, scheduleTime: any): void;
181
185
  allSoundOff(channelNumber: any, _value: any, scheduleTime: any): Promise<any[]>;
182
186
  resetAllControllers(channelNumber: any): void;
183
187
  allNotesOff(channelNumber: any, _value: any, scheduleTime: any): Promise<any[]>;
184
- handleUniversalNonRealTimeExclusiveMessage(data: any, _scheduleTime: any): void;
185
- GM1SystemOn(): void;
188
+ handleUniversalNonRealTimeExclusiveMessage(data: any, scheduleTime: any): void;
189
+ GM1SystemOn(scheduleTime: any): void;
186
190
  handleUniversalRealTimeExclusiveMessage(data: any, scheduleTime: any): void;
187
191
  handleMasterVolumeSysEx(data: any, scheduleTime: any): void;
188
192
  setMasterVolume(volume: any, scheduleTime: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AAiJA;IAsBE;;;;;;;;;;;;MAYE;IAEF,+BAQC;IA3CD,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;IAiBrC,kBAAgC;IAChC,kBAA8C;IAC9C;;;;;;;;;;;MAA2D;IAC3D;;;;;;;;;;;;;MAA+D;IAC/D,gBAAiD;IAKnD,4BAMC;IAED,mCAWC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAeC;IAED,yCAUC;IAED,6DA2BC;IAED,8DASC;IAED,2EAqDC;IAED,mCAOC;IAED,0BAoDC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MA4EC;IAED,mGAoBC;IAED,wEAMC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,4EASC;IAED,2DASC;IAED,qDAQC;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,yGAgBC;IAED,gHAwCC;IAED,kGAgDC;IAED,6FAQC;IAED,qFAwBC;IAED,yHAuBC;IAED,yGASC;IAED,qFAUC;IAED,mGA2BC;IAED,gFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAQC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAkDC;IAED;;;;;;;;;;;;;MAeC;IAED,kGAWC;IAED,wDAWC;IAED,iFAIC;IAED,oEAIC;IAED;;;MAMC;IAED,8DAIC;IAED,4EAIC;IAED,sEAGC;IAED,2DAUC;IAED,yEAMC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,8CAKC;IAED,oDAOC;IAED,gDAKC;IAED,sDAOC;IAED,gFAGC;IAED,8CAqBC;IAED,gFAGC;IAED,gFAgBC;IAED,oBAMC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAED,6DAUC;CACF;AA95CD;IACE,uBAGC;IAFC,YAA2B;IAC3B,qBAAuB;IAGzB,gCAKC;IAED,mBAEC;IAED,0BAUC;IAED,uBAEC;IAED,mBAEC;IAED,cAMC;IASD,6BAKC;IAZD,qDAKC;CAQF;AAED;IASE,0FAMC;IAdD,kBAAa;IACb,gBAAW;IACX,iBAAY;IACZ,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
1
+ {"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AAiJA;IAuBE;;;;;;;;;;;;;MAaE;IAEF,+BAcC;IAnDD,aAAa;IACb,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;IAkBrC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D;;;;;;;;;;;;;MAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCAWC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAeC;IAED,yCAWC;IAED,6DA2BC;IAED,4DASC;IAED,2EAsDC;IAED,mCAOC;IAED,0BAoDC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MA4EC;IAED,mGAgBC;IAED,wEAMC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,yDAQC;IAED,2DASC;IAED,qDAQC;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,yGAgBC;IAED,gHAwCC;IAED,kGAkDC;IAED,6FAQC;IAED,qFAwBC;IAED,yHAuBC;IAED,yGASC;IAED,4GAeC;IAED,mGA2BC;IAED,gFAGC;IAED,wFAGC;IAED,sEAWC;IAED,mEAQC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EA2CC;IAED;;;;;;;;;;;;;MAeC;IAED,kGAWC;IAED,wDAUC;IAED,iFAMC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAYC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAWC;IAED,iEAKC;IAED,uEASC;IAED,mEAKC;IAED,yEASC;IAED,gFAGC;IAED,8CAqBC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAED,6DAgBC;CACF;AA97CD;IACE,uBAGC;IAFC,YAA2B;IAC3B,qBAAuB;IAGzB,gCAKC;IAED,mBAEC;IAED,0BAUC;IAED,uBAEC;IAED,mBAEC;IAED,cAMC;IASD,6BAKC;IAZD,qDAKC;CAQF;AAED;IASE,0FAMC;IAdD,kBAAa;IACb,gBAAW;IACX,iBAAY;IACZ,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
package/esm/midy-GM1.js CHANGED
@@ -170,6 +170,12 @@ const volumeEnvelopeKeys = [
170
170
  const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
171
171
  export class MidyGM1 {
172
172
  constructor(audioContext) {
173
+ Object.defineProperty(this, "mode", {
174
+ enumerable: true,
175
+ configurable: true,
176
+ writable: true,
177
+ value: "GM1"
178
+ });
173
179
  Object.defineProperty(this, "ticksPerBeat", {
174
180
  enumerable: true,
175
181
  configurable: true,
@@ -292,10 +298,16 @@ export class MidyGM1 {
292
298
  });
293
299
  this.audioContext = audioContext;
294
300
  this.masterVolume = new GainNode(audioContext);
301
+ this.scheduler = new GainNode(audioContext, { gain: 0 });
302
+ this.schedulerBuffer = new AudioBuffer({
303
+ length: 1,
304
+ sampleRate: audioContext.sampleRate,
305
+ });
295
306
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
296
307
  this.controlChangeHandlers = this.createControlChangeHandlers();
297
308
  this.channels = this.createChannels(audioContext);
298
309
  this.masterVolume.connect(audioContext.destination);
310
+ this.scheduler.connect(audioContext.destination);
299
311
  this.GM1SystemOn();
300
312
  }
301
313
  initSoundFontTable() {
@@ -355,6 +367,7 @@ export class MidyGM1 {
355
367
  state: new ControllerState(),
356
368
  ...this.setChannelAudioNodes(audioContext),
357
369
  scheduledNotes: new SparseMap(128),
370
+ sustainNotes: [],
358
371
  };
359
372
  });
360
373
  return channels;
@@ -388,7 +401,7 @@ export class MidyGM1 {
388
401
  return audioBuffer;
389
402
  }
390
403
  }
391
- createNoteBufferNode(audioBuffer, voiceParams) {
404
+ createBufferSource(audioBuffer, voiceParams) {
392
405
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
393
406
  bufferSource.buffer = audioBuffer;
394
407
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
@@ -412,7 +425,7 @@ export class MidyGM1 {
412
425
  }
413
426
  /* falls through */
414
427
  case "noteOff": {
415
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime);
428
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
416
429
  if (notePromise) {
417
430
  this.notePromises.push(notePromise);
418
431
  }
@@ -576,15 +589,10 @@ export class MidyGM1 {
576
589
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
577
590
  const channel = this.channels[channelNumber];
578
591
  const promises = [];
579
- channel.scheduledNotes.forEach((noteList) => {
580
- for (let i = 0; i < noteList.length; i++) {
581
- const note = noteList[i];
582
- if (!note)
583
- continue;
584
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
585
- this.notePromises.push(promise);
586
- promises.push(promise);
587
- }
592
+ this.processScheduledNotes(channel, (note) => {
593
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
594
+ this.notePromises.push(promise);
595
+ promises.push(promise);
588
596
  });
589
597
  channel.scheduledNotes.clear();
590
598
  return Promise.all(promises);
@@ -640,14 +648,12 @@ export class MidyGM1 {
640
648
  const now = this.audioContext.currentTime;
641
649
  return this.resumeTime + now - this.startTime - this.startDelay;
642
650
  }
643
- processScheduledNotes(channel, scheduleTime, callback) {
651
+ processScheduledNotes(channel, callback) {
644
652
  channel.scheduledNotes.forEach((noteList) => {
645
653
  for (let i = 0; i < noteList.length; i++) {
646
654
  const note = noteList[i];
647
655
  if (!note)
648
656
  continue;
649
- if (scheduleTime < note.startTime)
650
- continue;
651
657
  callback(note);
652
658
  }
653
659
  });
@@ -693,7 +699,7 @@ export class MidyGM1 {
693
699
  return tuning + pitch;
694
700
  }
695
701
  updateChannelDetune(channel, scheduleTime) {
696
- this.processScheduledNotes(channel, scheduleTime, (note) => {
702
+ this.processScheduledNotes(channel, (note) => {
697
703
  this.updateDetune(channel, note, scheduleTime);
698
704
  });
699
705
  }
@@ -811,7 +817,7 @@ export class MidyGM1 {
811
817
  const voiceParams = voice.getAllParams(controllerState);
812
818
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
813
819
  const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
814
- note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
820
+ note.bufferSource = this.createBufferSource(audioBuffer, voiceParams);
815
821
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
816
822
  note.filterNode = new BiquadFilterNode(this.audioContext, {
817
823
  type: "lowpass",
@@ -842,15 +848,17 @@ export class MidyGM1 {
842
848
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
843
849
  note.volumeEnvelopeNode.connect(channel.gainL);
844
850
  note.volumeEnvelopeNode.connect(channel.gainR);
851
+ if (0.5 <= channel.state.sustainPedal) {
852
+ channel.sustainNotes.push(note);
853
+ }
845
854
  const exclusiveClass = note.voiceParams.exclusiveClass;
846
855
  if (exclusiveClass !== 0) {
847
856
  if (this.exclusiveClassMap.has(exclusiveClass)) {
848
857
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
849
858
  const [prevNote, prevChannelNumber] = prevEntry;
850
- if (!prevNote.ending) {
859
+ if (prevNote && !prevNote.ending) {
851
860
  this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
852
- startTime, undefined, // portamentoNoteNumber
853
- true);
861
+ startTime, true);
854
862
  }
855
863
  }
856
864
  this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
@@ -894,7 +902,7 @@ export class MidyGM1 {
894
902
  }
895
903
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
896
904
  const channel = this.channels[channelNumber];
897
- if (!force && 0.5 < channel.state.sustainPedal)
905
+ if (!force && 0.5 <= channel.state.sustainPedal)
898
906
  return;
899
907
  if (!channel.scheduledNotes.has(noteNumber))
900
908
  return;
@@ -922,11 +930,11 @@ export class MidyGM1 {
922
930
  const velocity = halfVelocity * 2;
923
931
  const channel = this.channels[channelNumber];
924
932
  const promises = [];
925
- this.processScheduledNotes(channel, scheduleTime, (note) => {
926
- const { noteNumber } = note;
927
- const promise = this.noteOff(channelNumber, noteNumber, velocity);
933
+ for (let i = 0; i < channel.sustainNotes.length; i++) {
934
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
928
935
  promises.push(promise);
929
- });
936
+ }
937
+ channel.sustainNotes = [];
930
938
  return promises;
931
939
  }
932
940
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -956,8 +964,10 @@ export class MidyGM1 {
956
964
  this.setPitchBend(channelNumber, pitchBend, scheduleTime);
957
965
  }
958
966
  setPitchBend(channelNumber, value, scheduleTime) {
959
- scheduleTime ??= this.audioContext.currentTime;
960
967
  const channel = this.channels[channelNumber];
968
+ if (channel.isDrum)
969
+ return;
970
+ scheduleTime ??= this.audioContext.currentTime;
961
971
  const state = channel.state;
962
972
  const prev = state.pitchWheel * 2 - 1;
963
973
  const next = (value - 8192) / 8192;
@@ -1037,48 +1047,43 @@ export class MidyGM1 {
1037
1047
  return state;
1038
1048
  }
1039
1049
  applyVoiceParams(channel, controllerType, scheduleTime) {
1040
- channel.scheduledNotes.forEach((noteList) => {
1041
- for (let i = 0; i < noteList.length; i++) {
1042
- const note = noteList[i];
1043
- if (!note)
1050
+ this.processScheduledNotes(channel, (note) => {
1051
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1052
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
1053
+ let appliedFilterEnvelope = false;
1054
+ let appliedVolumeEnvelope = false;
1055
+ for (const [key, value] of Object.entries(voiceParams)) {
1056
+ const prevValue = note.voiceParams[key];
1057
+ if (value === prevValue)
1044
1058
  continue;
1045
- const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1046
- const voiceParams = note.voice.getParams(controllerType, controllerState);
1047
- let appliedFilterEnvelope = false;
1048
- let appliedVolumeEnvelope = false;
1049
- for (const [key, value] of Object.entries(voiceParams)) {
1050
- const prevValue = note.voiceParams[key];
1051
- if (value === prevValue)
1059
+ note.voiceParams[key] = value;
1060
+ if (key in this.voiceParamsHandlers) {
1061
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1062
+ }
1063
+ else if (filterEnvelopeKeySet.has(key)) {
1064
+ if (appliedFilterEnvelope)
1052
1065
  continue;
1053
- note.voiceParams[key] = value;
1054
- if (key in this.voiceParamsHandlers) {
1055
- this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1056
- }
1057
- else if (filterEnvelopeKeySet.has(key)) {
1058
- if (appliedFilterEnvelope)
1059
- continue;
1060
- appliedFilterEnvelope = true;
1061
- const noteVoiceParams = note.voiceParams;
1062
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1063
- const key = filterEnvelopeKeys[i];
1064
- if (key in voiceParams)
1065
- noteVoiceParams[key] = voiceParams[key];
1066
- }
1067
- this.setFilterEnvelope(note, scheduleTime);
1068
- this.setPitchEnvelope(note, scheduleTime);
1066
+ appliedFilterEnvelope = true;
1067
+ const noteVoiceParams = note.voiceParams;
1068
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1069
+ const key = filterEnvelopeKeys[i];
1070
+ if (key in voiceParams)
1071
+ noteVoiceParams[key] = voiceParams[key];
1069
1072
  }
1070
- else if (volumeEnvelopeKeySet.has(key)) {
1071
- if (appliedVolumeEnvelope)
1072
- continue;
1073
- appliedVolumeEnvelope = true;
1074
- const noteVoiceParams = note.voiceParams;
1075
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1076
- const key = volumeEnvelopeKeys[i];
1077
- if (key in voiceParams)
1078
- noteVoiceParams[key] = voiceParams[key];
1079
- }
1080
- this.setVolumeEnvelope(channel, note, scheduleTime);
1073
+ this.setFilterEnvelope(note, scheduleTime);
1074
+ this.setPitchEnvelope(note, scheduleTime);
1075
+ }
1076
+ else if (volumeEnvelopeKeySet.has(key)) {
1077
+ if (appliedVolumeEnvelope)
1078
+ continue;
1079
+ appliedVolumeEnvelope = true;
1080
+ const noteVoiceParams = note.voiceParams;
1081
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1082
+ const key = volumeEnvelopeKeys[i];
1083
+ if (key in voiceParams)
1084
+ noteVoiceParams[key] = voiceParams[key];
1081
1085
  }
1086
+ this.setVolumeEnvelope(note, scheduleTime);
1082
1087
  }
1083
1088
  }
1084
1089
  });
@@ -1111,9 +1116,8 @@ export class MidyGM1 {
1111
1116
  }
1112
1117
  }
1113
1118
  updateModulation(channel, scheduleTime) {
1114
- scheduleTime ??= this.audioContext.currentTime;
1115
1119
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1116
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1120
+ this.processScheduledNotes(channel, (note) => {
1117
1121
  if (note.modulationDepth) {
1118
1122
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1119
1123
  }
@@ -1125,10 +1129,14 @@ export class MidyGM1 {
1125
1129
  }
1126
1130
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1127
1131
  const channel = this.channels[channelNumber];
1132
+ if (channel.isDrum)
1133
+ return;
1134
+ scheduleTime ??= this.audioContext.currentTime;
1128
1135
  channel.state.modulationDepth = modulation / 127;
1129
1136
  this.updateModulation(channel, scheduleTime);
1130
1137
  }
1131
1138
  setVolume(channelNumber, volume, scheduleTime) {
1139
+ scheduleTime ??= this.audioContext.currentTime;
1132
1140
  const channel = this.channels[channelNumber];
1133
1141
  channel.state.volume = volume / 127;
1134
1142
  this.updateChannelVolume(channel, scheduleTime);
@@ -1141,11 +1149,13 @@ export class MidyGM1 {
1141
1149
  };
1142
1150
  }
1143
1151
  setPan(channelNumber, pan, scheduleTime) {
1152
+ scheduleTime ??= this.audioContext.currentTime;
1144
1153
  const channel = this.channels[channelNumber];
1145
1154
  channel.state.pan = pan / 127;
1146
1155
  this.updateChannelVolume(channel, scheduleTime);
1147
1156
  }
1148
1157
  setExpression(channelNumber, expression, scheduleTime) {
1158
+ scheduleTime ??= this.audioContext.currentTime;
1149
1159
  const channel = this.channels[channelNumber];
1150
1160
  channel.state.expression = expression / 127;
1151
1161
  this.updateChannelVolume(channel, scheduleTime);
@@ -1166,9 +1176,17 @@ export class MidyGM1 {
1166
1176
  .setValueAtTime(volume * gainRight, scheduleTime);
1167
1177
  }
1168
1178
  setSustainPedal(channelNumber, value, scheduleTime) {
1179
+ const channel = this.channels[channelNumber];
1180
+ if (channel.isDrum)
1181
+ return;
1169
1182
  scheduleTime ??= this.audioContext.currentTime;
1170
- this.channels[channelNumber].state.sustainPedal = value / 127;
1171
- if (value < 64) {
1183
+ channel.state.sustainPedal = value / 127;
1184
+ if (64 <= value) {
1185
+ this.processScheduledNotes(channel, (note) => {
1186
+ channel.sustainNotes.push(note);
1187
+ });
1188
+ }
1189
+ else {
1172
1190
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
1173
1191
  }
1174
1192
  }
@@ -1206,10 +1224,10 @@ export class MidyGM1 {
1206
1224
  this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1207
1225
  break;
1208
1226
  case 1:
1209
- this.handleFineTuningRPN(channelNumber);
1227
+ this.handleFineTuningRPN(channelNumber, scheduleTime);
1210
1228
  break;
1211
1229
  case 2:
1212
- this.handleCoarseTuningRPN(channelNumber);
1230
+ this.handleCoarseTuningRPN(channelNumber, scheduleTime);
1213
1231
  break;
1214
1232
  default:
1215
1233
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
@@ -1232,8 +1250,10 @@ export class MidyGM1 {
1232
1250
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1233
1251
  }
1234
1252
  setPitchBendRange(channelNumber, value, scheduleTime) {
1235
- scheduleTime ??= this.audioContext.currentTime;
1236
1253
  const channel = this.channels[channelNumber];
1254
+ if (channel.isDrum)
1255
+ return;
1256
+ scheduleTime ??= this.audioContext.currentTime;
1237
1257
  const state = channel.state;
1238
1258
  const prev = state.pitchWheelSensitivity;
1239
1259
  const next = value / 128;
@@ -1242,33 +1262,39 @@ export class MidyGM1 {
1242
1262
  this.updateChannelDetune(channel, scheduleTime);
1243
1263
  this.applyVoiceParams(channel, 16, scheduleTime);
1244
1264
  }
1245
- handleFineTuningRPN(channelNumber) {
1265
+ handleFineTuningRPN(channelNumber, scheduleTime) {
1246
1266
  const channel = this.channels[channelNumber];
1247
1267
  this.limitData(channel, 0, 127, 0, 127);
1248
1268
  const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
1249
- this.setFineTuning(channelNumber, fineTuning);
1269
+ this.setFineTuning(channelNumber, fineTuning, scheduleTime);
1250
1270
  }
1251
- setFineTuning(channelNumber, value) {
1271
+ setFineTuning(channelNumber, value, scheduleTime) {
1252
1272
  const channel = this.channels[channelNumber];
1273
+ if (channel.isDrum)
1274
+ return;
1275
+ scheduleTime ??= this.audioContext.currentTime;
1253
1276
  const prev = channel.fineTuning;
1254
1277
  const next = (value - 8192) / 8.192; // cent
1255
1278
  channel.fineTuning = next;
1256
1279
  channel.detune += next - prev;
1257
- this.updateChannelDetune(channel);
1280
+ this.updateChannelDetune(channel, scheduleTime);
1258
1281
  }
1259
- handleCoarseTuningRPN(channelNumber) {
1282
+ handleCoarseTuningRPN(channelNumber, scheduleTime) {
1260
1283
  const channel = this.channels[channelNumber];
1261
1284
  this.limitDataMSB(channel, 0, 127);
1262
1285
  const coarseTuning = channel.dataMSB;
1263
- this.setCoarseTuning(channelNumber, coarseTuning);
1286
+ this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
1264
1287
  }
1265
- setCoarseTuning(channelNumber, value) {
1288
+ setCoarseTuning(channelNumber, value, scheduleTime) {
1266
1289
  const channel = this.channels[channelNumber];
1290
+ if (channel.isDrum)
1291
+ return;
1292
+ scheduleTime ??= this.audioContext.currentTime;
1267
1293
  const prev = channel.coarseTuning;
1268
1294
  const next = (value - 64) * 100; // cent
1269
1295
  channel.coarseTuning = next;
1270
1296
  channel.detune += next - prev;
1271
- this.updateChannelDetune(channel);
1297
+ this.updateChannelDetune(channel, scheduleTime);
1272
1298
  }
1273
1299
  allSoundOff(channelNumber, _value, scheduleTime) {
1274
1300
  scheduleTime ??= this.audioContext.currentTime;
@@ -1300,12 +1326,12 @@ export class MidyGM1 {
1300
1326
  scheduleTime ??= this.audioContext.currentTime;
1301
1327
  return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
1302
1328
  }
1303
- handleUniversalNonRealTimeExclusiveMessage(data, _scheduleTime) {
1329
+ handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
1304
1330
  switch (data[2]) {
1305
1331
  case 9:
1306
1332
  switch (data[3]) {
1307
1333
  case 1:
1308
- this.GM1SystemOn();
1334
+ this.GM1SystemOn(scheduleTime);
1309
1335
  break;
1310
1336
  case 2: // GM System Off
1311
1337
  break;
@@ -1317,12 +1343,17 @@ export class MidyGM1 {
1317
1343
  console.warn(`Unsupported Exclusive Message: ${data}`);
1318
1344
  }
1319
1345
  }
1320
- GM1SystemOn() {
1346
+ GM1SystemOn(scheduleTime) {
1347
+ scheduleTime ??= this.audioContext.currentTime;
1348
+ this.mode = "GM1";
1321
1349
  for (let i = 0; i < this.channels.length; i++) {
1350
+ this.allSoundOff(i, 0, scheduleTime);
1322
1351
  const channel = this.channels[i];
1323
1352
  channel.bank = 0;
1353
+ channel.isDrum = false;
1324
1354
  }
1325
1355
  this.channels[9].bank = 128;
1356
+ this.channels[9].isDrum = true;
1326
1357
  }
1327
1358
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
1328
1359
  switch (data[2]) {
@@ -1365,13 +1396,20 @@ export class MidyGM1 {
1365
1396
  }
1366
1397
  scheduleTask(callback, scheduleTime) {
1367
1398
  return new Promise((resolve) => {
1368
- const bufferSource = new AudioBufferSourceNode(this.audioContext);
1399
+ const bufferSource = new AudioBufferSourceNode(this.audioContext, {
1400
+ buffer: this.schedulerBuffer,
1401
+ });
1402
+ bufferSource.connect(this.scheduler);
1369
1403
  bufferSource.onended = () => {
1370
- callback();
1371
- resolve();
1404
+ try {
1405
+ callback();
1406
+ }
1407
+ finally {
1408
+ bufferSource.disconnect();
1409
+ resolve();
1410
+ }
1372
1411
  };
1373
1412
  bufferSource.start(scheduleTime);
1374
- bufferSource.stop(scheduleTime);
1375
1413
  });
1376
1414
  }
1377
1415
  }
@@ -1381,6 +1419,7 @@ Object.defineProperty(MidyGM1, "channelSettings", {
1381
1419
  writable: true,
1382
1420
  value: {
1383
1421
  currentBufferSource: null,
1422
+ isDrum: false,
1384
1423
  detune: 0,
1385
1424
  program: 0,
1386
1425
  bank: 0,
@@ -1388,8 +1427,8 @@ Object.defineProperty(MidyGM1, "channelSettings", {
1388
1427
  dataLSB: 0,
1389
1428
  rpnMSB: 127,
1390
1429
  rpnLSB: 127,
1430
+ modulationDepthRange: 50, // cent
1391
1431
  fineTuning: 0, // cb
1392
1432
  coarseTuning: 0, // cb
1393
- modulationDepthRange: 50, // cent
1394
1433
  }
1395
1434
  });