@marmooo/midy 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGMLite = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
+ // 2-3 times faster than Map
7
+ class SparseMap {
8
+ constructor(size) {
9
+ this.data = new Array(size);
10
+ this.activeIndices = [];
11
+ }
12
+ set(key, value) {
13
+ if (this.data[key] === undefined) {
14
+ this.activeIndices.push(key);
15
+ }
16
+ this.data[key] = value;
17
+ }
18
+ get(key) {
19
+ return this.data[key];
20
+ }
21
+ delete(key) {
22
+ if (this.data[key] !== undefined) {
23
+ this.data[key] = undefined;
24
+ const index = this.activeIndices.indexOf(key);
25
+ if (index !== -1) {
26
+ this.activeIndices.splice(index, 1);
27
+ }
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ has(key) {
33
+ return this.data[key] !== undefined;
34
+ }
35
+ get size() {
36
+ return this.activeIndices.length;
37
+ }
38
+ clear() {
39
+ for (let i = 0; i < this.activeIndices.length; i++) {
40
+ const key = this.activeIndices[i];
41
+ this.data[key] = undefined;
42
+ }
43
+ this.activeIndices = [];
44
+ }
45
+ *[Symbol.iterator]() {
46
+ for (let i = 0; i < this.activeIndices.length; i++) {
47
+ const key = this.activeIndices[i];
48
+ yield [key, this.data[key]];
49
+ }
50
+ }
51
+ forEach(callback) {
52
+ for (let i = 0; i < this.activeIndices.length; i++) {
53
+ const key = this.activeIndices[i];
54
+ callback(this.data[key], key, this);
55
+ }
56
+ }
57
+ }
6
58
  class Note {
7
59
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
60
  Object.defineProperty(this, "bufferSource", {
@@ -169,6 +221,18 @@ class MidyGMLite {
169
221
  writable: true,
170
222
  value: this.initSoundFontTable()
171
223
  });
224
+ Object.defineProperty(this, "audioBufferCounter", {
225
+ enumerable: true,
226
+ configurable: true,
227
+ writable: true,
228
+ value: new Map()
229
+ });
230
+ Object.defineProperty(this, "audioBufferCache", {
231
+ enumerable: true,
232
+ configurable: true,
233
+ writable: true,
234
+ value: new Map()
235
+ });
172
236
  Object.defineProperty(this, "isPlaying", {
173
237
  enumerable: true,
174
238
  configurable: true,
@@ -221,7 +285,7 @@ class MidyGMLite {
221
285
  enumerable: true,
222
286
  configurable: true,
223
287
  writable: true,
224
- value: new Map()
288
+ value: new SparseMap(128)
225
289
  });
226
290
  this.audioContext = audioContext;
227
291
  this.masterVolume = new GainNode(audioContext);
@@ -234,7 +298,7 @@ class MidyGMLite {
234
298
  initSoundFontTable() {
235
299
  const table = new Array(128);
236
300
  for (let i = 0; i < 128; i++) {
237
- table[i] = new Map();
301
+ table[i] = new SparseMap(128);
238
302
  }
239
303
  return table;
240
304
  }
@@ -287,7 +351,7 @@ class MidyGMLite {
287
351
  ...this.constructor.channelSettings,
288
352
  state: new ControllerState(),
289
353
  ...this.setChannelAudioNodes(audioContext),
290
- scheduledNotes: new Map(),
354
+ scheduledNotes: new SparseMap(128),
291
355
  };
292
356
  });
293
357
  return channels;
@@ -321,9 +385,8 @@ class MidyGMLite {
321
385
  return audioBuffer;
322
386
  }
323
387
  }
324
- async createNoteBufferNode(voiceParams, isSF3) {
388
+ createNoteBufferNode(audioBuffer, voiceParams) {
325
389
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
326
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
327
390
  bufferSource.buffer = audioBuffer;
328
391
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
329
392
  if (bufferSource.loop) {
@@ -388,6 +451,7 @@ class MidyGMLite {
388
451
  await Promise.all(this.notePromises);
389
452
  this.notePromises = [];
390
453
  this.exclusiveClassMap.clear();
454
+ this.audioBufferCache.clear();
391
455
  resolve();
392
456
  return;
393
457
  }
@@ -403,8 +467,9 @@ class MidyGMLite {
403
467
  }
404
468
  else if (this.isStopping) {
405
469
  await this.stopNotes(0, true);
406
- this.exclusiveClassMap.clear();
407
470
  this.notePromises = [];
471
+ this.exclusiveClassMap.clear();
472
+ this.audioBufferCache.clear();
408
473
  resolve();
409
474
  this.isStopping = false;
410
475
  this.isPaused = false;
@@ -435,6 +500,9 @@ class MidyGMLite {
435
500
  secondToTicks(second, secondsPerBeat) {
436
501
  return second * this.ticksPerBeat / secondsPerBeat;
437
502
  }
503
+ getAudioBufferId(programNumber, noteNumber, velocity) {
504
+ return `${programNumber}:${noteNumber}:${velocity}`;
505
+ }
438
506
  extractMidiData(midi) {
439
507
  const instruments = new Set();
440
508
  const timeline = [];
@@ -455,6 +523,8 @@ class MidyGMLite {
455
523
  switch (event.type) {
456
524
  case "noteOn": {
457
525
  const channel = tmpChannels[event.channel];
526
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
527
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
458
528
  if (channel.programNumber < 0) {
459
529
  instruments.add(`${channel.bank}:0`);
460
530
  channel.programNumber = 0;
@@ -471,6 +541,10 @@ class MidyGMLite {
471
541
  timeline.push(event);
472
542
  }
473
543
  }
544
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
545
+ if (count === 1)
546
+ this.audioBufferCounter.delete(audioBufferId);
547
+ }
474
548
  const priority = {
475
549
  controller: 0,
476
550
  sysEx: 1,
@@ -561,7 +635,7 @@ class MidyGMLite {
561
635
  return this.resumeTime + now - this.startTime - this.startDelay;
562
636
  }
563
637
  getActiveNotes(channel, time) {
564
- const activeNotes = new Map();
638
+ const activeNotes = new SparseMap(128);
565
639
  channel.scheduledNotes.forEach((noteList) => {
566
640
  const activeNote = this.getActiveNote(noteList, time);
567
641
  if (activeNote) {
@@ -701,11 +775,31 @@ class MidyGMLite {
701
775
  note.modulationLFO.connect(note.volumeDepth);
702
776
  note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
703
777
  }
778
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
779
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
780
+ const cache = this.audioBufferCache.get(audioBufferId);
781
+ if (cache) {
782
+ cache.counter += 1;
783
+ if (cache.maxCount <= cache.counter) {
784
+ this.audioBufferCache.delete(audioBufferId);
785
+ }
786
+ return cache.audioBuffer;
787
+ }
788
+ else {
789
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
790
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
791
+ const cache = { audioBuffer, maxCount, counter: 1 };
792
+ this.audioBufferCache.set(audioBufferId, cache);
793
+ return audioBuffer;
794
+ }
795
+ }
704
796
  async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
705
797
  const state = channel.state;
706
- const voiceParams = voice.getAllParams(state.array);
798
+ const controllerState = this.getControllerState(channel, noteNumber, velocity);
799
+ const voiceParams = voice.getAllParams(controllerState);
707
800
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
708
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
801
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
802
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
709
803
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
710
804
  note.filterNode = new BiquadFilterNode(this.audioContext, {
711
805
  type: "lowpass",
@@ -729,10 +823,10 @@ class MidyGMLite {
729
823
  if (soundFontIndex === undefined)
730
824
  return;
731
825
  const soundFont = this.soundFonts[soundFontIndex];
732
- const isSF3 = soundFont.parsed.info.version.major === 3;
733
826
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
734
827
  if (!voice)
735
828
  return;
829
+ const isSF3 = soundFont.parsed.info.version.major === 3;
736
830
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
737
831
  note.volumeEnvelopeNode.connect(channel.gainL);
738
832
  note.volumeEnvelopeNode.connect(channel.gainR);
package/script/midy.d.ts CHANGED
@@ -2,7 +2,7 @@ export class Midy {
2
2
  static channelSettings: {
3
3
  currentBufferSource: null;
4
4
  detune: number;
5
- scaleOctaveTuningTable: any[];
5
+ scaleOctaveTuningTable: Float32Array<ArrayBuffer>;
6
6
  channelPressureTable: Uint8Array<ArrayBuffer>;
7
7
  polyphonicKeyPressureTable: Uint8Array<ArrayBuffer>;
8
8
  keyBasedInstrumentControlTable: Int8Array<ArrayBuffer>;
@@ -48,6 +48,8 @@ export class Midy {
48
48
  resumeTime: number;
49
49
  soundFonts: any[];
50
50
  soundFontTable: any[];
51
+ audioBufferCounter: Map<any, any>;
52
+ audioBufferCache: Map<any, any>;
51
53
  isPlaying: boolean;
52
54
  isPausing: boolean;
53
55
  isPaused: boolean;
@@ -56,7 +58,7 @@ export class Midy {
56
58
  timeline: any[];
57
59
  instruments: any[];
58
60
  notePromises: any[];
59
- exclusiveClassMap: Map<any, any>;
61
+ exclusiveClassMap: SparseMap;
60
62
  defaultOptions: {
61
63
  reverbAlgorithm: (audioContext: any) => {
62
64
  input: any;
@@ -144,13 +146,14 @@ export class Midy {
144
146
  };
145
147
  createChannels(audioContext: any): any[];
146
148
  createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
147
- createNoteBufferNode(voiceParams: any, isSF3: any): Promise<any>;
149
+ createNoteBufferNode(audioBuffer: any, voiceParams: any): any;
148
150
  findPortamentoTarget(queueIndex: any): any;
149
151
  scheduleTimelineEvents(t: any, offset: any, queueIndex: any): Promise<any>;
150
152
  getQueueIndex(second: any): number;
151
153
  playNotes(): Promise<any>;
152
154
  ticksToSecond(ticks: any, secondsPerBeat: any): number;
153
155
  secondToTicks(second: any, secondsPerBeat: any): number;
156
+ getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
154
157
  extractMidiData(midi: any): {
155
158
  instruments: Set<any>;
156
159
  timeline: any[];
@@ -164,7 +167,7 @@ export class Midy {
164
167
  seekTo(second: any): void;
165
168
  calcTotalTime(): number;
166
169
  currentTime(): number;
167
- getActiveNotes(channel: any, time: any): Map<any, any>;
170
+ getActiveNotes(channel: any, time: any): SparseMap;
168
171
  getActiveNote(noteList: any, time: any): any;
169
172
  createConvolutionReverbImpulse(audioContext: any, decay: any, preDecay: any): any;
170
173
  createConvolutionReverb(audioContext: any, impulse: any): {
@@ -205,6 +208,7 @@ export class Midy {
205
208
  setFilterEnvelope(channel: any, note: any, pressure: any): void;
206
209
  startModulation(channel: any, note: any, startTime: any): void;
207
210
  startVibrato(channel: any, note: any, startTime: any): void;
211
+ getAudioBuffer(program: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
208
212
  createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, portamento: any, isSF3: any): Promise<Note>;
209
213
  calcBank(channel: any, channelNumber: any): any;
210
214
  scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any, portamento: any): Promise<void>;
@@ -362,7 +366,8 @@ export class Midy {
362
366
  setChorusSendToReverb(value: any): void;
363
367
  getChorusSendToReverb(value: any): number;
364
368
  getChannelBitmap(data: any): any[];
365
- handleScaleOctaveTuning1ByteFormatSysEx(data: any): void;
369
+ handleScaleOctaveTuning1ByteFormatSysEx(data: any, realtime: any): void;
370
+ handleScaleOctaveTuning2ByteFormatSysEx(data: any, realtime: any): void;
366
371
  applyDestinationSettings(channel: any, note: any, table: any): void;
367
372
  handleChannelPressureSysEx(data: any, tableName: any): void;
368
373
  initControlTable(): Uint8Array<ArrayBuffer>;
@@ -374,6 +379,19 @@ export class Midy {
374
379
  handleSysEx(data: any): any;
375
380
  scheduleTask(callback: any, startTime: any): Promise<any>;
376
381
  }
382
+ declare class SparseMap {
383
+ constructor(size: any);
384
+ data: any[];
385
+ activeIndices: any[];
386
+ set(key: any, value: any): void;
387
+ get(key: any): any;
388
+ delete(key: any): boolean;
389
+ has(key: any): boolean;
390
+ get size(): number;
391
+ clear(): void;
392
+ forEach(callback: any): void;
393
+ [Symbol.iterator](): Generator<any[], void, unknown>;
394
+ }
377
395
  declare class Note {
378
396
  constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
379
397
  bufferSource: any;
@@ -1 +1 @@
1
- {"version":3,"file":"midy.d.ts","sourceRoot":"","sources":["../src/midy.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"midy.d.ts","sourceRoot":"","sources":["../src/midy.js"],"names":[],"mappings":"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"}
package/script/midy.js CHANGED
@@ -3,6 +3,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Midy = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
+ // 2-3 times faster than Map
7
+ class SparseMap {
8
+ constructor(size) {
9
+ this.data = new Array(size);
10
+ this.activeIndices = [];
11
+ }
12
+ set(key, value) {
13
+ if (this.data[key] === undefined) {
14
+ this.activeIndices.push(key);
15
+ }
16
+ this.data[key] = value;
17
+ }
18
+ get(key) {
19
+ return this.data[key];
20
+ }
21
+ delete(key) {
22
+ if (this.data[key] !== undefined) {
23
+ this.data[key] = undefined;
24
+ const index = this.activeIndices.indexOf(key);
25
+ if (index !== -1) {
26
+ this.activeIndices.splice(index, 1);
27
+ }
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ has(key) {
33
+ return this.data[key] !== undefined;
34
+ }
35
+ get size() {
36
+ return this.activeIndices.length;
37
+ }
38
+ clear() {
39
+ for (let i = 0; i < this.activeIndices.length; i++) {
40
+ const key = this.activeIndices[i];
41
+ this.data[key] = undefined;
42
+ }
43
+ this.activeIndices = [];
44
+ }
45
+ *[Symbol.iterator]() {
46
+ for (let i = 0; i < this.activeIndices.length; i++) {
47
+ const key = this.activeIndices[i];
48
+ yield [key, this.data[key]];
49
+ }
50
+ }
51
+ forEach(callback) {
52
+ for (let i = 0; i < this.activeIndices.length; i++) {
53
+ const key = this.activeIndices[i];
54
+ callback(this.data[key], key, this);
55
+ }
56
+ }
57
+ }
6
58
  class Note {
7
59
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
60
  Object.defineProperty(this, "bufferSource", {
@@ -290,6 +342,18 @@ class Midy {
290
342
  writable: true,
291
343
  value: this.initSoundFontTable()
292
344
  });
345
+ Object.defineProperty(this, "audioBufferCounter", {
346
+ enumerable: true,
347
+ configurable: true,
348
+ writable: true,
349
+ value: new Map()
350
+ });
351
+ Object.defineProperty(this, "audioBufferCache", {
352
+ enumerable: true,
353
+ configurable: true,
354
+ writable: true,
355
+ value: new Map()
356
+ });
293
357
  Object.defineProperty(this, "isPlaying", {
294
358
  enumerable: true,
295
359
  configurable: true,
@@ -342,7 +406,7 @@ class Midy {
342
406
  enumerable: true,
343
407
  configurable: true,
344
408
  writable: true,
345
- value: new Map()
409
+ value: new SparseMap(128)
346
410
  });
347
411
  Object.defineProperty(this, "defaultOptions", {
348
412
  enumerable: true,
@@ -382,7 +446,7 @@ class Midy {
382
446
  initSoundFontTable() {
383
447
  const table = new Array(128);
384
448
  for (let i = 0; i < 128; i++) {
385
- table[i] = new Map();
449
+ table[i] = new SparseMap(128);
386
450
  }
387
451
  return table;
388
452
  }
@@ -436,8 +500,8 @@ class Midy {
436
500
  state: new ControllerState(),
437
501
  controlTable: this.initControlTable(),
438
502
  ...this.setChannelAudioNodes(audioContext),
439
- scheduledNotes: new Map(),
440
- sostenutoNotes: new Map(),
503
+ scheduledNotes: new SparseMap(128),
504
+ sostenutoNotes: new SparseMap(128),
441
505
  };
442
506
  });
443
507
  return channels;
@@ -471,9 +535,8 @@ class Midy {
471
535
  return audioBuffer;
472
536
  }
473
537
  }
474
- async createNoteBufferNode(voiceParams, isSF3) {
538
+ createNoteBufferNode(audioBuffer, voiceParams) {
475
539
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
476
- const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
477
540
  bufferSource.buffer = audioBuffer;
478
541
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
479
542
  if (bufferSource.loop) {
@@ -565,6 +628,7 @@ class Midy {
565
628
  await Promise.all(this.notePromises);
566
629
  this.notePromises = [];
567
630
  this.exclusiveClassMap.clear();
631
+ this.audioBufferCache.clear();
568
632
  resolve();
569
633
  return;
570
634
  }
@@ -580,8 +644,9 @@ class Midy {
580
644
  }
581
645
  else if (this.isStopping) {
582
646
  await this.stopNotes(0, true);
583
- this.exclusiveClassMap.clear();
584
647
  this.notePromises = [];
648
+ this.exclusiveClassMap.clear();
649
+ this.audioBufferCache.clear();
585
650
  resolve();
586
651
  this.isStopping = false;
587
652
  this.isPaused = false;
@@ -612,6 +677,9 @@ class Midy {
612
677
  secondToTicks(second, secondsPerBeat) {
613
678
  return second * this.ticksPerBeat / secondsPerBeat;
614
679
  }
680
+ getAudioBufferId(programNumber, noteNumber, velocity) {
681
+ return `${programNumber}:${noteNumber}:${velocity}`;
682
+ }
615
683
  extractMidiData(midi) {
616
684
  const instruments = new Set();
617
685
  const timeline = [];
@@ -633,6 +701,8 @@ class Midy {
633
701
  switch (event.type) {
634
702
  case "noteOn": {
635
703
  const channel = tmpChannels[event.channel];
704
+ const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
705
+ this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
636
706
  if (channel.programNumber < 0) {
637
707
  channel.programNumber = event.programNumber;
638
708
  switch (channel.bankMSB) {
@@ -682,6 +752,10 @@ class Midy {
682
752
  timeline.push(event);
683
753
  }
684
754
  }
755
+ for (const [audioBufferId, count] of this.audioBufferCounter) {
756
+ if (count === 1)
757
+ this.audioBufferCounter.delete(audioBufferId);
758
+ }
685
759
  const priority = {
686
760
  controller: 0,
687
761
  sysEx: 1,
@@ -775,7 +849,7 @@ class Midy {
775
849
  return this.resumeTime + now - this.startTime - this.startDelay;
776
850
  }
777
851
  getActiveNotes(channel, time) {
778
- const activeNotes = new Map();
852
+ const activeNotes = new SparseMap(128);
779
853
  channel.scheduledNotes.forEach((noteList) => {
780
854
  const activeNote = this.getActiveNote(noteList, time);
781
855
  if (activeNote) {
@@ -1113,12 +1187,31 @@ class Midy {
1113
1187
  note.vibratoLFO.connect(note.vibratoDepth);
1114
1188
  note.vibratoDepth.connect(note.bufferSource.detune);
1115
1189
  }
1190
+ async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1191
+ const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1192
+ const cache = this.audioBufferCache.get(audioBufferId);
1193
+ if (cache) {
1194
+ cache.counter += 1;
1195
+ if (cache.maxCount <= cache.counter) {
1196
+ this.audioBufferCache.delete(audioBufferId);
1197
+ }
1198
+ return cache.audioBuffer;
1199
+ }
1200
+ else {
1201
+ const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
1202
+ const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
1203
+ const cache = { audioBuffer, maxCount, counter: 1 };
1204
+ this.audioBufferCache.set(audioBufferId, cache);
1205
+ return audioBuffer;
1206
+ }
1207
+ }
1116
1208
  async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1117
1209
  const state = channel.state;
1118
1210
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1119
1211
  const voiceParams = voice.getAllParams(controllerState);
1120
1212
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1121
- note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
1213
+ const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1214
+ note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1122
1215
  note.volumeNode = new GainNode(this.audioContext);
1123
1216
  note.gainL = new GainNode(this.audioContext);
1124
1217
  note.gainR = new GainNode(this.audioContext);
@@ -1178,10 +1271,10 @@ class Midy {
1178
1271
  if (soundFontIndex === undefined)
1179
1272
  return;
1180
1273
  const soundFont = this.soundFonts[soundFontIndex];
1181
- const isSF3 = soundFont.parsed.info.version.major === 3;
1182
1274
  const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1183
1275
  if (!voice)
1184
1276
  return;
1277
+ const isSF3 = soundFont.parsed.info.version.major === 3;
1185
1278
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1186
1279
  note.gainL.connect(channel.gainL);
1187
1280
  note.gainR.connect(channel.gainR);
@@ -1365,6 +1458,7 @@ class Midy {
1365
1458
  channel.program = program;
1366
1459
  }
1367
1460
  handleChannelPressure(channelNumber, value) {
1461
+ const now = this.audioContext.currentTime;
1368
1462
  const channel = this.channels[channelNumber];
1369
1463
  const prev = channel.state.channelPressure;
1370
1464
  const next = value / 127;
@@ -1374,13 +1468,8 @@ class Midy {
1374
1468
  channel.detune += pressureDepth * (next - prev);
1375
1469
  }
1376
1470
  const table = channel.channelPressureTable;
1377
- channel.scheduledNotes.forEach((noteList) => {
1378
- for (let i = 0; i < noteList.length; i++) {
1379
- const note = noteList[i];
1380
- if (!note)
1381
- continue;
1382
- this.applyDestinationSettings(channel, note, table);
1383
- }
1471
+ this.getActiveNotes(channel, now).forEach((note) => {
1472
+ this.applyDestinationSettings(channel, note, table);
1384
1473
  });
1385
1474
  // this.applyVoiceParams(channel, 13);
1386
1475
  }
@@ -1791,8 +1880,7 @@ class Midy {
1791
1880
  channel.state.sostenutoPedal = value / 127;
1792
1881
  if (64 <= value) {
1793
1882
  const now = this.audioContext.currentTime;
1794
- const activeNotes = this.getActiveNotes(channel, now);
1795
- channel.sostenutoNotes = new Map(activeNotes);
1883
+ channel.sostenutoNotes = this.getActiveNotes(channel, now);
1796
1884
  }
1797
1885
  else {
1798
1886
  this.releaseSostenutoPedal(channelNumber, value);
@@ -2173,7 +2261,10 @@ class Midy {
2173
2261
  switch (data[3]) {
2174
2262
  case 8:
2175
2263
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2176
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2264
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
2265
+ case 9:
2266
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2267
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, false);
2177
2268
  default:
2178
2269
  console.warn(`Unsupported Exclusive Message: ${data}`);
2179
2270
  }
@@ -2235,8 +2326,10 @@ class Midy {
2235
2326
  case 8:
2236
2327
  switch (data[3]) {
2237
2328
  case 8: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2238
- // TODO: realtime
2239
- return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
2329
+ return this.handleScaleOctaveTuning1ByteFormatSysEx(data, true);
2330
+ case 9:
2331
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
2332
+ return this.handleScaleOctaveTuning2ByteFormatSysEx(data, true);
2240
2333
  default:
2241
2334
  console.warn(`Unsupported Exclusive Message: ${data}`);
2242
2335
  }
@@ -2506,8 +2599,26 @@ class Midy {
2506
2599
  }
2507
2600
  return bitmap;
2508
2601
  }
2509
- handleScaleOctaveTuning1ByteFormatSysEx(data) {
2510
- if (data.length < 18) {
2602
+ handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
2603
+ if (data.length < 19) {
2604
+ console.error("Data length is too short");
2605
+ return;
2606
+ }
2607
+ const channelBitmap = this.getChannelBitmap(data);
2608
+ for (let i = 0; i < channelBitmap.length; i++) {
2609
+ if (!channelBitmap[i])
2610
+ continue;
2611
+ const channel = this.channels[i];
2612
+ for (let j = 0; j < 12; j++) {
2613
+ const centValue = data[j + 7] - 64;
2614
+ channel.scaleOctaveTuningTable[j] = centValue;
2615
+ }
2616
+ if (realtime)
2617
+ this.updateChannelDetune(channel);
2618
+ }
2619
+ }
2620
+ handleScaleOctaveTuning2ByteFormatSysEx(data, realtime) {
2621
+ if (data.length < 31) {
2511
2622
  console.error("Data length is too short");
2512
2623
  return;
2513
2624
  }
@@ -2515,10 +2626,17 @@ class Midy {
2515
2626
  for (let i = 0; i < channelBitmap.length; i++) {
2516
2627
  if (!channelBitmap[i])
2517
2628
  continue;
2629
+ const channel = this.channels[i];
2518
2630
  for (let j = 0; j < 12; j++) {
2519
- const value = data[j + 7] - 64; // cent
2520
- this.channels[i].scaleOctaveTuningTable[j] = value;
2631
+ const index = 7 + j * 2;
2632
+ const msb = data[index] & 0x7F;
2633
+ const lsb = data[index + 1] & 0x7F;
2634
+ const value14bit = msb * 128 + lsb;
2635
+ const centValue = (value14bit - 8192) / 8.192;
2636
+ channel.scaleOctaveTuningTable[j] = centValue;
2521
2637
  }
2638
+ if (realtime)
2639
+ this.updateChannelDetune(channel);
2522
2640
  }
2523
2641
  }
2524
2642
  applyDestinationSettings(channel, note, table) {
@@ -2670,7 +2788,7 @@ Object.defineProperty(Midy, "channelSettings", {
2670
2788
  value: {
2671
2789
  currentBufferSource: null,
2672
2790
  detune: 0,
2673
- scaleOctaveTuningTable: new Array(12).fill(0), // cent
2791
+ scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
2674
2792
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2675
2793
  polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
2676
2794
  keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]