@marmooo/midy 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.4 → soundfont-parser@0.0.6}/+esm.d.ts +16 -20
  2. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts.map +1 -0
  3. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js +180 -0
  4. package/esm/midy-GM1.d.ts +36 -8
  5. package/esm/midy-GM1.d.ts.map +1 -1
  6. package/esm/midy-GM1.js +87 -89
  7. package/esm/midy-GM2.d.ts +65 -10
  8. package/esm/midy-GM2.d.ts.map +1 -1
  9. package/esm/midy-GM2.js +233 -154
  10. package/esm/midy-GMLite.d.ts +36 -8
  11. package/esm/midy-GMLite.d.ts.map +1 -1
  12. package/esm/midy-GMLite.js +87 -89
  13. package/esm/midy.d.ts +85 -10
  14. package/esm/midy.d.ts.map +1 -1
  15. package/esm/midy.js +254 -174
  16. package/package.json +1 -1
  17. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.4 → soundfont-parser@0.0.6}/+esm.d.ts +16 -20
  18. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.d.ts.map +1 -0
  19. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js +190 -0
  20. package/script/midy-GM1.d.ts +36 -8
  21. package/script/midy-GM1.d.ts.map +1 -1
  22. package/script/midy-GM1.js +87 -89
  23. package/script/midy-GM2.d.ts +65 -10
  24. package/script/midy-GM2.d.ts.map +1 -1
  25. package/script/midy-GM2.js +233 -154
  26. package/script/midy-GMLite.d.ts +36 -8
  27. package/script/midy-GMLite.d.ts.map +1 -1
  28. package/script/midy-GMLite.js +87 -89
  29. package/script/midy.d.ts +85 -10
  30. package/script/midy.d.ts.map +1 -1
  31. package/script/midy.js +254 -174
  32. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.4/+esm.d.ts.map +0 -1
  33. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.4/+esm.js +0 -162
  34. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.4/+esm.d.ts.map +0 -1
  35. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.4/+esm.js +0 -169
package/esm/midy.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseMidi } from "./deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js";
2
- import { parse, SoundFont, } from "./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.4/+esm.js";
2
+ import { parse, SoundFont, } from "./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.6/+esm.js";
3
3
  class Note {
4
4
  constructor(noteNumber, velocity, startTime, instrumentKey) {
5
5
  Object.defineProperty(this, "bufferSource", {
@@ -230,6 +230,7 @@ export class Midy {
230
230
  this.audioContext = audioContext;
231
231
  this.options = { ...this.defaultOptions, ...options };
232
232
  this.masterGain = new GainNode(audioContext);
233
+ this.controlChangeHandlers = this.createControlChangeHandlers();
233
234
  this.channels = this.createChannels(audioContext);
234
235
  this.reverbEffect = this.options.reverbAlgorithm(audioContext);
235
236
  this.chorusEffect = this.createChorusEffect(audioContext);
@@ -248,12 +249,14 @@ export class Midy {
248
249
  addSoundFont(soundFont) {
249
250
  const index = this.soundFonts.length;
250
251
  this.soundFonts.push(soundFont);
251
- soundFont.parsed.presetHeaders.forEach((presetHeader) => {
252
+ const presetHeaders = soundFont.parsed.presetHeaders;
253
+ for (let i = 0; i < presetHeaders.length; i++) {
254
+ const presetHeader = presetHeaders[i];
252
255
  if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
253
256
  const banks = this.soundFontTable[presetHeader.preset];
254
257
  banks.set(presetHeader.bank, index);
255
258
  }
256
- });
259
+ }
257
260
  }
258
261
  async loadSoundFont(soundFontUrl) {
259
262
  const response = await fetch(soundFontUrl);
@@ -305,27 +308,25 @@ export class Midy {
305
308
  return channels;
306
309
  }
307
310
  async createNoteBuffer(instrumentKey, isSF3) {
311
+ const sampleStart = instrumentKey.start;
308
312
  const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
309
313
  if (isSF3) {
310
- const sample = new Uint8Array(instrumentKey.sample.length);
311
- sample.set(instrumentKey.sample);
314
+ const sample = instrumentKey.sample.slice(sampleStart, sampleEnd);
312
315
  const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
313
- for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
314
- const channelData = audioBuffer.getChannelData(channel);
315
- channelData.set(channelData.subarray(0, sampleEnd));
316
- }
317
316
  return audioBuffer;
318
317
  }
319
318
  else {
320
- const sample = instrumentKey.sample.subarray(0, sampleEnd);
321
- const floatSample = this.convertToFloat32Array(sample);
319
+ const sample = instrumentKey.sample.subarray(sampleStart, sampleEnd);
322
320
  const audioBuffer = new AudioBuffer({
323
321
  numberOfChannels: 1,
324
322
  length: sample.length,
325
323
  sampleRate: instrumentKey.sampleRate,
326
324
  });
327
325
  const channelData = audioBuffer.getChannelData(0);
328
- channelData.set(floatSample);
326
+ const int16Array = new Int16Array(sample.buffer);
327
+ for (let i = 0; i < int16Array.length; i++) {
328
+ channelData[i] = int16Array[i] / 32768;
329
+ }
329
330
  return audioBuffer;
330
331
  }
331
332
  }
@@ -341,13 +342,23 @@ export class Midy {
341
342
  }
342
343
  return bufferSource;
343
344
  }
344
- convertToFloat32Array(uint8Array) {
345
- const int16Array = new Int16Array(uint8Array.buffer);
346
- const float32Array = new Float32Array(int16Array.length);
347
- for (let i = 0; i < int16Array.length; i++) {
348
- float32Array[i] = int16Array[i] / 32768;
345
+ findPortamentoTarget(queueIndex) {
346
+ const endEvent = this.timeline[queueIndex];
347
+ if (!this.channels[endEvent.channel].portamento)
348
+ return;
349
+ const endTime = endEvent.startTime;
350
+ let target;
351
+ while (++queueIndex < this.timeline.length) {
352
+ const event = this.timeline[queueIndex];
353
+ if (endTime !== event.startTime)
354
+ break;
355
+ if (event.type !== "noteOn")
356
+ continue;
357
+ if (!target || event.noteNumber < target.noteNumber) {
358
+ target = event;
359
+ }
349
360
  }
350
- return float32Array;
361
+ return target;
351
362
  }
352
363
  async scheduleTimelineEvents(t, offset, queueIndex) {
353
364
  while (queueIndex < this.timeline.length) {
@@ -357,12 +368,15 @@ export class Midy {
357
368
  switch (event.type) {
358
369
  case "noteOn":
359
370
  if (event.velocity !== 0) {
360
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
371
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, event.portamento);
361
372
  break;
362
373
  }
363
374
  /* falls through */
364
375
  case "noteOff": {
365
- const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset);
376
+ const portamentoTarget = this.findPortamentoTarget(queueIndex);
377
+ if (portamentoTarget)
378
+ portamentoTarget.portamento = true;
379
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, event.startTime + this.startDelay - offset, portamentoTarget?.noteNumber, false);
366
380
  if (notePromise) {
367
381
  this.notePromises.push(notePromise);
368
382
  }
@@ -466,9 +480,11 @@ export class Midy {
466
480
  bankLSB: this.channels[i].bankLSB,
467
481
  };
468
482
  }
469
- midi.tracks.forEach((track) => {
483
+ for (let i = 0; i < midi.tracks.length; i++) {
484
+ const track = midi.tracks[i];
470
485
  let currentTicks = 0;
471
- track.forEach((event) => {
486
+ for (let j = 0; j < track.length; j++) {
487
+ const event = track[j];
472
488
  currentTicks += event.deltaTime;
473
489
  event.ticks = currentTicks;
474
490
  switch (event.type) {
@@ -521,16 +537,18 @@ export class Midy {
521
537
  }
522
538
  delete event.deltaTime;
523
539
  timeline.push(event);
524
- });
525
- });
540
+ }
541
+ }
526
542
  const priority = {
527
543
  controller: 0,
528
544
  sysEx: 1,
545
+ noteOff: 2, // for portamento
546
+ noteOn: 3,
529
547
  };
530
548
  timeline.sort((a, b) => {
531
549
  if (a.ticks !== b.ticks)
532
550
  return a.ticks - b.ticks;
533
- return (priority[a.type] || 2) - (priority[b.type] || 2);
551
+ return (priority[a.type] || 4) - (priority[b.type] || 4);
534
552
  });
535
553
  let prevTempoTime = 0;
536
554
  let prevTempoTicks = 0;
@@ -547,7 +565,7 @@ export class Midy {
547
565
  }
548
566
  return { instruments, timeline };
549
567
  }
550
- async stopChannelNotes(channelNumber, velocity, stopPedal) {
568
+ async stopChannelNotes(channelNumber, velocity, force) {
551
569
  const now = this.audioContext.currentTime;
552
570
  const channel = this.channels[channelNumber];
553
571
  channel.scheduledNotes.forEach((noteList) => {
@@ -555,16 +573,17 @@ export class Midy {
555
573
  const note = noteList[i];
556
574
  if (!note)
557
575
  continue;
558
- const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, stopPedal);
576
+ const promise = this.scheduleNoteRelease(channelNumber, note.noteNumber, velocity, now, undefined, // portamentoNoteNumber
577
+ force);
559
578
  this.notePromises.push(promise);
560
579
  }
561
580
  });
562
581
  channel.scheduledNotes.clear();
563
582
  await Promise.all(this.notePromises);
564
583
  }
565
- stopNotes(velocity, stopPedal) {
584
+ stopNotes(velocity, force) {
566
585
  for (let i = 0; i < this.channels.length; i++) {
567
- this.stopChannelNotes(i, velocity, stopPedal);
586
+ this.stopChannelNotes(i, velocity, force);
568
587
  }
569
588
  return Promise.all(this.notePromises);
570
589
  }
@@ -778,6 +797,17 @@ export class Midy {
778
797
  return instrumentKey.playbackRate(noteNumber) *
779
798
  Math.pow(2, semitoneOffset / 12);
780
799
  }
800
+ setPortamentoStartVolumeEnvelope(channel, note) {
801
+ const { instrumentKey, startTime } = note;
802
+ const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
803
+ const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
804
+ const volDelay = startTime + instrumentKey.volDelay;
805
+ const portamentoTime = volDelay + channel.portamentoTime;
806
+ note.volumeNode.gain
807
+ .cancelScheduledValues(startTime)
808
+ .setValueAtTime(0, volDelay)
809
+ .linearRampToValueAtTime(sustainVolume, portamentoTime);
810
+ }
781
811
  setVolumeEnvelope(channel, note) {
782
812
  const { instrumentKey, startTime } = note;
783
813
  const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation);
@@ -817,6 +847,25 @@ export class Midy {
817
847
  const maxFrequency = 20000; // max Hz of initialFilterFc
818
848
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
819
849
  }
850
+ setPortamentoStartFilterEnvelope(channel, note) {
851
+ const { instrumentKey, noteNumber, startTime } = note;
852
+ const softPedalFactor = 1 -
853
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
854
+ const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
855
+ softPedalFactor * channel.brightness;
856
+ const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor * channel.brightness;
857
+ const sustainFreq = baseFreq +
858
+ (peekFreq - baseFreq) * (1 - instrumentKey.modSustain);
859
+ const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
860
+ const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
861
+ const portamentoTime = startTime + channel.portamentoTime;
862
+ const modDelay = startTime + instrumentKey.modDelay;
863
+ note.filterNode.frequency
864
+ .cancelScheduledValues(startTime)
865
+ .setValueAtTime(adjustedBaseFreq, startTime)
866
+ .setValueAtTime(adjustedBaseFreq, modDelay)
867
+ .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
868
+ }
820
869
  setFilterEnvelope(channel, note) {
821
870
  const { instrumentKey, noteNumber, startTime } = note;
822
871
  const softPedalFactor = 1 -
@@ -884,7 +933,7 @@ export class Midy {
884
933
  note.vibratoLFO.connect(note.vibratoDepth);
885
934
  note.vibratoDepth.connect(note.bufferSource.detune);
886
935
  }
887
- async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
936
+ async createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3) {
888
937
  const semitoneOffset = this.calcSemitoneOffset(channel);
889
938
  const note = new Note(noteNumber, velocity, startTime, instrumentKey);
890
939
  note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
@@ -893,8 +942,14 @@ export class Midy {
893
942
  type: "lowpass",
894
943
  Q: instrumentKey.initialFilterQ / 10 * channel.filterResonance, // dB
895
944
  });
896
- this.setVolumeEnvelope(channel, note);
897
- this.setFilterEnvelope(channel, note);
945
+ if (portamento) {
946
+ this.setPortamentoStartVolumeEnvelope(channel, note);
947
+ this.setPortamentoStartFilterEnvelope(channel, note);
948
+ }
949
+ else {
950
+ this.setVolumeEnvelope(channel, note);
951
+ this.setFilterEnvelope(channel, note);
952
+ }
898
953
  if (0 < channel.vibratoDepth) {
899
954
  this.startVibrato(channel, note, startTime);
900
955
  }
@@ -911,7 +966,7 @@ export class Midy {
911
966
  }
912
967
  note.bufferSource.connect(note.filterNode);
913
968
  note.filterNode.connect(note.volumeNode);
914
- note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
969
+ note.bufferSource.start(startTime);
915
970
  return note;
916
971
  }
917
972
  calcBank(channel, channelNumber) {
@@ -923,7 +978,7 @@ export class Midy {
923
978
  }
924
979
  return channel.bank;
925
980
  }
926
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
981
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
927
982
  const channel = this.channels[channelNumber];
928
983
  const bankNumber = this.calcBank(channel, channelNumber);
929
984
  const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
@@ -931,10 +986,10 @@ export class Midy {
931
986
  return;
932
987
  const soundFont = this.soundFonts[soundFontIndex];
933
988
  const isSF3 = soundFont.parsed.info.version.major === 3;
934
- const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
989
+ const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber, velocity);
935
990
  if (!instrumentKey)
936
991
  return;
937
- const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
992
+ const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, portamento, isSF3);
938
993
  note.volumeNode.connect(channel.gainL);
939
994
  note.volumeNode.connect(channel.gainR);
940
995
  if (channel.sostenutoPedal) {
@@ -948,16 +1003,47 @@ export class Midy {
948
1003
  scheduledNotes.set(noteNumber, [note]);
949
1004
  }
950
1005
  }
951
- noteOn(channelNumber, noteNumber, velocity) {
1006
+ noteOn(channelNumber, noteNumber, velocity, portamento) {
952
1007
  const now = this.audioContext.currentTime;
953
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
1008
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now, portamento);
954
1009
  }
955
- scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, stopPedal = false) {
1010
+ stopNote(stopTime, endTime, scheduledNotes, index) {
1011
+ const note = scheduledNotes[index];
1012
+ note.volumeNode.gain
1013
+ .cancelScheduledValues(stopTime)
1014
+ .linearRampToValueAtTime(0, endTime);
1015
+ note.ending = true;
1016
+ this.scheduleTask(() => {
1017
+ note.bufferSource.loop = false;
1018
+ }, endTime);
1019
+ return new Promise((resolve) => {
1020
+ note.bufferSource.onended = () => {
1021
+ scheduledNotes[index] = null;
1022
+ note.bufferSource.disconnect();
1023
+ note.volumeNode.disconnect();
1024
+ note.filterNode.disconnect();
1025
+ if (note.modulationDepth) {
1026
+ note.volumeDepth.disconnect();
1027
+ note.modulationDepth.disconnect();
1028
+ note.modulationLFO.stop();
1029
+ }
1030
+ if (note.vibratoDepth) {
1031
+ note.vibratoDepth.disconnect();
1032
+ note.vibratoLFO.stop();
1033
+ }
1034
+ resolve();
1035
+ };
1036
+ note.bufferSource.stop(endTime);
1037
+ });
1038
+ }
1039
+ scheduleNoteRelease(channelNumber, noteNumber, _velocity, stopTime, portamentoNoteNumber, force) {
956
1040
  const channel = this.channels[channelNumber];
957
- if (stopPedal && channel.sustainPedal)
958
- return;
959
- if (stopPedal && channel.sostenutoNotes.has(noteNumber))
960
- return;
1041
+ if (!force) {
1042
+ if (channel.sustainPedal)
1043
+ return;
1044
+ if (channel.sostenutoNotes.has(noteNumber))
1045
+ return;
1046
+ }
961
1047
  if (!channel.scheduledNotes.has(noteNumber))
962
1048
  return;
963
1049
  const scheduledNotes = channel.scheduledNotes.get(noteNumber);
@@ -967,44 +1053,29 @@ export class Midy {
967
1053
  continue;
968
1054
  if (note.ending)
969
1055
  continue;
970
- const volEndTime = stopTime +
971
- note.instrumentKey.volRelease * channel.releaseTime;
972
- note.volumeNode.gain
973
- .cancelScheduledValues(stopTime)
974
- .linearRampToValueAtTime(0, volEndTime);
975
- const modRelease = stopTime + note.instrumentKey.modRelease;
976
- note.filterNode.frequency
977
- .cancelScheduledValues(stopTime)
978
- .linearRampToValueAtTime(0, modRelease);
979
- note.ending = true;
980
- this.scheduleTask(() => {
981
- note.bufferSource.loop = false;
982
- }, stopTime);
983
- return new Promise((resolve) => {
984
- note.bufferSource.onended = () => {
985
- scheduledNotes[i] = null;
986
- note.bufferSource.disconnect();
987
- note.volumeNode.disconnect();
988
- note.filterNode.disconnect();
989
- if (note.volumeDepth)
990
- note.volumeDepth.disconnect();
991
- if (note.modulationDepth)
992
- note.modulationDepth.disconnect();
993
- if (note.modulationLFO)
994
- note.modulationLFO.stop();
995
- if (note.vibratoDepth)
996
- note.vibratoDepth.disconnect();
997
- if (note.vibratoLFO)
998
- note.vibratoLFO.stop();
999
- resolve();
1000
- };
1001
- note.bufferSource.stop(volEndTime);
1002
- });
1056
+ if (portamentoNoteNumber === undefined) {
1057
+ const volEndTime = stopTime +
1058
+ note.instrumentKey.volRelease * channel.releaseTime;
1059
+ const modRelease = stopTime + note.instrumentKey.modRelease;
1060
+ note.filterNode.frequency
1061
+ .cancelScheduledValues(stopTime)
1062
+ .linearRampToValueAtTime(0, modRelease);
1063
+ return this.stopNote(stopTime, volEndTime, scheduledNotes, i);
1064
+ }
1065
+ else {
1066
+ const portamentoTime = stopTime + channel.portamentoTime;
1067
+ const detuneChange = (portamentoNoteNumber - noteNumber) * 100;
1068
+ const detune = note.bufferSource.detune.value + detuneChange;
1069
+ note.bufferSource.detune
1070
+ .cancelScheduledValues(stopTime)
1071
+ .linearRampToValueAtTime(detune, portamentoTime);
1072
+ return this.stopNote(stopTime, portamentoTime, scheduledNotes, i);
1073
+ }
1003
1074
  }
1004
1075
  }
1005
- releaseNote(channelNumber, noteNumber, velocity) {
1076
+ releaseNote(channelNumber, noteNumber, velocity, portamentoNoteNumber) {
1006
1077
  const now = this.audioContext.currentTime;
1007
- return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
1078
+ return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, portamentoNoteNumber, false);
1008
1079
  }
1009
1080
  releaseSustainPedal(channelNumber, halfVelocity) {
1010
1081
  const velocity = halfVelocity * 2;
@@ -1105,78 +1176,51 @@ export class Midy {
1105
1176
  channel.pitchBendRange * 100;
1106
1177
  this.updateDetune(channel, detuneChange);
1107
1178
  }
1179
+ createControlChangeHandlers() {
1180
+ return {
1181
+ 0: this.setBankMSB,
1182
+ 1: this.setModulationDepth,
1183
+ 5: this.setPortamentoTime,
1184
+ 6: this.dataEntryMSB,
1185
+ 7: this.setVolume,
1186
+ 10: this.setPan,
1187
+ 11: this.setExpression,
1188
+ 32: this.setBankLSB,
1189
+ 38: this.dataEntryLSB,
1190
+ 64: this.setSustainPedal,
1191
+ 65: this.setPortamento,
1192
+ 66: this.setSostenutoPedal,
1193
+ 67: this.setSoftPedal,
1194
+ 71: this.setFilterResonance,
1195
+ 72: this.setReleaseTime,
1196
+ 73: this.setAttackTime,
1197
+ 74: this.setBrightness,
1198
+ 75: this.setDecayTime,
1199
+ 76: this.setVibratoRate,
1200
+ 77: this.setVibratoDepth,
1201
+ 78: this.setVibratoDelay,
1202
+ 91: this.setReverbSendLevel,
1203
+ 93: this.setChorusSendLevel,
1204
+ 96: this.dataIncrement,
1205
+ 97: this.dataDecrement,
1206
+ 100: this.setRPNLSB,
1207
+ 101: this.setRPNMSB,
1208
+ 120: this.allSoundOff,
1209
+ 121: this.resetAllControllers,
1210
+ 123: this.allNotesOff,
1211
+ 124: this.omniOff,
1212
+ 125: this.omniOn,
1213
+ 126: this.monoOn,
1214
+ 127: this.polyOn,
1215
+ };
1216
+ }
1108
1217
  handleControlChange(channelNumber, controller, value) {
1109
- switch (controller) {
1110
- case 0:
1111
- return this.setBankMSB(channelNumber, value);
1112
- case 1:
1113
- return this.setModulationDepth(channelNumber, value);
1114
- case 5:
1115
- return this.setPortamentoTime(channelNumber, value);
1116
- case 6:
1117
- return this.dataEntryMSB(channelNumber, value);
1118
- case 7:
1119
- return this.setVolume(channelNumber, value);
1120
- case 10:
1121
- return this.setPan(channelNumber, value);
1122
- case 11:
1123
- return this.setExpression(channelNumber, value);
1124
- case 32:
1125
- return this.setBankLSB(channelNumber, value);
1126
- case 38:
1127
- return this.dataEntryLSB(channelNumber, value);
1128
- case 64:
1129
- return this.setSustainPedal(channelNumber, value);
1130
- case 65:
1131
- return this.setPortamento(channelNumber, value);
1132
- case 66:
1133
- return this.setSostenutoPedal(channelNumber, value);
1134
- case 67:
1135
- return this.setSoftPedal(channelNumber, value);
1136
- case 71:
1137
- return this.setFilterResonance(channelNumber, value);
1138
- case 72:
1139
- return this.setReleaseTime(channelNumber, value);
1140
- case 73:
1141
- return this.setAttackTime(channelNumber, value);
1142
- case 74:
1143
- return this.setBrightness(channelNumber, value);
1144
- case 75:
1145
- return this.setDecayTime(channelNumber, value);
1146
- case 76:
1147
- return this.setVibratoRate(channelNumber, value);
1148
- case 77:
1149
- return this.setVibratoDepth(channelNumber, value);
1150
- case 78:
1151
- return this.setVibratoDelay(channelNumber, value);
1152
- case 91:
1153
- return this.setReverbSendLevel(channelNumber, value);
1154
- case 93:
1155
- return this.setChorusSendLevel(channelNumber, value);
1156
- case 96: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1157
- return this.dataIncrement(channelNumber);
1158
- case 97: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1159
- return this.dataDecrement(channelNumber);
1160
- case 100:
1161
- return this.setRPNLSB(channelNumber, value);
1162
- case 101:
1163
- return this.setRPNMSB(channelNumber, value);
1164
- case 120:
1165
- return this.allSoundOff(channelNumber);
1166
- case 121:
1167
- return this.resetAllControllers(channelNumber);
1168
- case 123:
1169
- return this.allNotesOff(channelNumber);
1170
- case 124:
1171
- return this.omniOff();
1172
- case 125:
1173
- return this.omniOn();
1174
- case 126:
1175
- return this.monoOn();
1176
- case 127:
1177
- return this.polyOn();
1178
- default:
1179
- console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
1218
+ const handler = this.controlChangeHandlers[controller];
1219
+ if (handler) {
1220
+ handler.call(this, channelNumber, value);
1221
+ }
1222
+ else {
1223
+ console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
1180
1224
  }
1181
1225
  }
1182
1226
  setBankMSB(channelNumber, msb) {
@@ -1206,12 +1250,14 @@ export class Midy {
1206
1250
  this.updateModulation(channel);
1207
1251
  }
1208
1252
  setPortamentoTime(channelNumber, portamentoTime) {
1209
- this.channels[channelNumber].portamentoTime = portamentoTime / 127;
1253
+ const channel = this.channels[channelNumber];
1254
+ const factor = 5 * Math.log(10) / 127;
1255
+ channel.portamentoTime = Math.exp(factor * portamentoTime);
1210
1256
  }
1211
1257
  setVolume(channelNumber, volume) {
1212
1258
  const channel = this.channels[channelNumber];
1213
1259
  channel.volume = volume / 127;
1214
- this.updateChannelGain(channel);
1260
+ this.updateChannelVolume(channel);
1215
1261
  }
1216
1262
  panToGain(pan) {
1217
1263
  const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
@@ -1223,12 +1269,12 @@ export class Midy {
1223
1269
  setPan(channelNumber, pan) {
1224
1270
  const channel = this.channels[channelNumber];
1225
1271
  channel.pan = pan;
1226
- this.updateChannelGain(channel);
1272
+ this.updateChannelVolume(channel);
1227
1273
  }
1228
1274
  setExpression(channelNumber, expression) {
1229
1275
  const channel = this.channels[channelNumber];
1230
1276
  channel.expression = expression / 127;
1231
- this.updateChannelGain(channel);
1277
+ this.updateChannelVolume(channel);
1232
1278
  }
1233
1279
  setBankLSB(channelNumber, lsb) {
1234
1280
  this.channels[channelNumber].bankLSB = lsb;
@@ -1237,7 +1283,7 @@ export class Midy {
1237
1283
  this.channels[channelNumber].dataLSB = value;
1238
1284
  this.handleRPN(channelNumber, 0);
1239
1285
  }
1240
- updateChannelGain(channel) {
1286
+ updateChannelVolume(channel) {
1241
1287
  const now = this.audioContext.currentTime;
1242
1288
  const volume = channel.volume * channel.expression;
1243
1289
  const { gainLeft, gainRight } = this.panToGain(channel.pan);
@@ -1255,34 +1301,55 @@ export class Midy {
1255
1301
  this.releaseSustainPedal(channelNumber, value);
1256
1302
  }
1257
1303
  }
1258
- // TODO
1259
1304
  setPortamento(channelNumber, value) {
1260
1305
  this.channels[channelNumber].portamento = value >= 64;
1261
1306
  }
1262
1307
  setReverbSendLevel(channelNumber, reverbSendLevel) {
1263
1308
  const channel = this.channels[channelNumber];
1264
1309
  const reverbEffect = this.reverbEffect;
1265
- if (0 < reverbSendLevel) {
1266
- const now = this.audioContext.currentTime;
1267
- channel.reverbSendLevel = reverbSendLevel / 127;
1268
- reverbEffect.output.gain.cancelScheduledValues(now);
1269
- reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1310
+ if (0 < channel.reverbSendLevel) {
1311
+ if (0 < reverbSendLevel) {
1312
+ const now = this.audioContext.currentTime;
1313
+ channel.reverbSendLevel = reverbSendLevel / 127;
1314
+ reverbEffect.output.gain.cancelScheduledValues(now);
1315
+ reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1316
+ }
1317
+ else {
1318
+ channel.merger.disconnect(reverbEffect.input);
1319
+ }
1270
1320
  }
1271
- else if (channel.reverbSendLevel !== 0) {
1272
- channel.merger.disconnect(reverbEffect.input);
1321
+ else {
1322
+ if (0 < reverbSendLevel) {
1323
+ channel.merger.connect(reverbEffect.input);
1324
+ const now = this.audioContext.currentTime;
1325
+ channel.reverbSendLevel = reverbSendLevel / 127;
1326
+ reverbEffect.output.gain.cancelScheduledValues(now);
1327
+ reverbEffect.output.gain.setValueAtTime(channel.reverbSendLevel, now);
1328
+ }
1273
1329
  }
1274
1330
  }
1275
1331
  setChorusSendLevel(channelNumber, chorusSendLevel) {
1276
1332
  const channel = this.channels[channelNumber];
1277
1333
  const chorusEffect = this.chorusEffect;
1278
- if (0 < chorusSendLevel) {
1279
- const now = this.audioContext.currentTime;
1280
- channel.chorusSendLevel = chorusSendLevel / 127;
1281
- chorusEffect.output.gain.cancelScheduledValues(now);
1282
- chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1334
+ if (0 < channel.chorusSendLevel) {
1335
+ if (0 < chorusSendLevel) {
1336
+ const now = this.audioContext.currentTime;
1337
+ channel.chorusSendLevel = chorusSendLevel / 127;
1338
+ chorusEffect.output.gain.cancelScheduledValues(now);
1339
+ chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1340
+ }
1341
+ else {
1342
+ channel.merger.disconnect(chorusEffect.input);
1343
+ }
1283
1344
  }
1284
- else if (channel.chorusSendLevel !== 0) {
1285
- channel.merger.disconnect(chorusEffect.input);
1345
+ else {
1346
+ if (0 < chorusSendLevel) {
1347
+ channel.merger.connect(chorusEffect.input);
1348
+ const now = this.audioContext.currentTime;
1349
+ channel.chorusSendLevel = chorusSendLevel / 127;
1350
+ chorusEffect.output.gain.cancelScheduledValues(now);
1351
+ chorusEffect.output.gain.setValueAtTime(channel.chorusSendLevel, now);
1352
+ }
1286
1353
  }
1287
1354
  }
1288
1355
  setSostenutoPedal(channelNumber, value) {
@@ -1363,6 +1430,15 @@ export class Midy {
1363
1430
  setVibratoRate(channelNumber, vibratoRate) {
1364
1431
  const channel = this.channels[channelNumber];
1365
1432
  channel.vibratoRate = vibratoRate / 64;
1433
+ if (channel.vibratoDepth <= 0)
1434
+ return;
1435
+ const now = this.audioContext.currentTime;
1436
+ const activeNotes = this.getActiveNotes(channel, now);
1437
+ activeNotes.forEach((activeNote) => {
1438
+ activeNote.vibratoLFO.frequency
1439
+ .cancelScheduledValues(now)
1440
+ .setValueAtTime(channel.vibratoRate, now);
1441
+ });
1366
1442
  }
1367
1443
  setVibratoDepth(channelNumber, vibratoDepth) {
1368
1444
  const channel = this.channels[channelNumber];
@@ -1422,9 +1498,11 @@ export class Midy {
1422
1498
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1423
1499
  }
1424
1500
  }
1501
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1425
1502
  dataIncrement(channelNumber) {
1426
1503
  this.handleRPN(channelNumber, 1);
1427
1504
  }
1505
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
1428
1506
  dataDecrement(channelNumber) {
1429
1507
  this.handleRPN(channelNumber, -1);
1430
1508
  }
@@ -1547,20 +1625,22 @@ export class Midy {
1547
1625
  }
1548
1626
  }
1549
1627
  GM1SystemOn() {
1550
- this.channels.forEach((channel) => {
1628
+ for (let i = 0; i < this.channels.length; i++) {
1629
+ const channel = this.channels[i];
1551
1630
  channel.bankMSB = 0;
1552
1631
  channel.bankLSB = 0;
1553
1632
  channel.bank = 0;
1554
- });
1633
+ }
1555
1634
  this.channels[9].bankMSB = 1;
1556
1635
  this.channels[9].bank = 128;
1557
1636
  }
1558
1637
  GM2SystemOn() {
1559
- this.channels.forEach((channel) => {
1638
+ for (let i = 0; i < this.channels.length; i++) {
1639
+ const channel = this.channels[i];
1560
1640
  channel.bankMSB = 121;
1561
1641
  channel.bankLSB = 0;
1562
1642
  channel.bank = 121 * 128;
1563
- });
1643
+ }
1564
1644
  this.channels[9].bankMSB = 120;
1565
1645
  this.channels[9].bank = 120 * 128;
1566
1646
  }
@@ -1869,7 +1949,7 @@ Object.defineProperty(Midy, "channelSettings", {
1869
1949
  currentBufferSource: null,
1870
1950
  volume: 100 / 127,
1871
1951
  pan: 64,
1872
- portamentoTime: 0,
1952
+ portamentoTime: 1, // sec
1873
1953
  filterResonance: 1,
1874
1954
  releaseTime: 1,
1875
1955
  attackTime: 1,