@marmooo/midy 0.0.4 → 0.0.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.
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.d.ts +13 -6
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.d.ts.map +1 -0
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.js +5 -5
- package/esm/midy-GM1.d.ts +36 -42
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +214 -148
- package/esm/midy-GM2.d.ts +133 -25
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +230 -163
- package/esm/midy-GMLite.d.ts +37 -43
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +215 -149
- package/esm/midy.d.ts +41 -33
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +263 -179
- package/package.json +1 -1
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.d.ts +13 -6
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.d.ts.map +1 -0
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.js +5 -5
- package/script/midy-GM1.d.ts +36 -42
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +214 -148
- package/script/midy-GM2.d.ts +133 -25
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +230 -163
- package/script/midy-GMLite.d.ts +37 -43
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +215 -149
- package/script/midy.d.ts +41 -33
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +263 -179
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +0 -1
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +0 -1
package/esm/midy-GM1.js
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
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.
|
|
2
|
+
import { parse, SoundFont, } from "./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.js";
|
|
3
|
+
class Note {
|
|
4
|
+
constructor(noteNumber, velocity, startTime, instrumentKey) {
|
|
5
|
+
Object.defineProperty(this, "bufferSource", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: void 0
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "gainNode", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "filterNode", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: void 0
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "modLFO", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "modLFOGain", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: void 0
|
|
34
|
+
});
|
|
35
|
+
this.noteNumber = noteNumber;
|
|
36
|
+
this.velocity = velocity;
|
|
37
|
+
this.startTime = startTime;
|
|
38
|
+
this.instrumentKey = instrumentKey;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
3
41
|
export class MidyGM1 {
|
|
4
42
|
constructor(audioContext) {
|
|
5
43
|
Object.defineProperty(this, "ticksPerBeat", {
|
|
@@ -145,20 +183,16 @@ export class MidyGM1 {
|
|
|
145
183
|
this.totalTime = this.calcTotalTime();
|
|
146
184
|
}
|
|
147
185
|
setChannelAudioNodes(audioContext) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
modulationEffect.lfo.start();
|
|
156
|
-
pannerNode.connect(gainNode);
|
|
157
|
-
gainNode.connect(this.masterGain);
|
|
186
|
+
const { gainLeft, gainRight } = this.panToGain(MidyGM1.channelSettings.pan);
|
|
187
|
+
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
188
|
+
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
189
|
+
const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
|
|
190
|
+
gainL.connect(merger, 0, 0);
|
|
191
|
+
gainR.connect(merger, 0, 1);
|
|
192
|
+
merger.connect(this.masterGain);
|
|
158
193
|
return {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
modulationEffect,
|
|
194
|
+
gainL,
|
|
195
|
+
gainR,
|
|
162
196
|
};
|
|
163
197
|
}
|
|
164
198
|
createChannels(audioContext) {
|
|
@@ -168,16 +202,15 @@ export class MidyGM1 {
|
|
|
168
202
|
...MidyGM1.effectSettings,
|
|
169
203
|
...this.setChannelAudioNodes(audioContext),
|
|
170
204
|
scheduledNotes: new Map(),
|
|
171
|
-
sostenutoNotes: new Map(),
|
|
172
205
|
};
|
|
173
206
|
});
|
|
174
207
|
return channels;
|
|
175
208
|
}
|
|
176
|
-
async createNoteBuffer(
|
|
177
|
-
const sampleEnd =
|
|
209
|
+
async createNoteBuffer(instrumentKey, isSF3) {
|
|
210
|
+
const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
|
|
178
211
|
if (isSF3) {
|
|
179
|
-
const sample = new Uint8Array(
|
|
180
|
-
sample.set(
|
|
212
|
+
const sample = new Uint8Array(instrumentKey.sample.length);
|
|
213
|
+
sample.set(instrumentKey.sample);
|
|
181
214
|
const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
|
|
182
215
|
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
|
|
183
216
|
const channelData = audioBuffer.getChannelData(channel);
|
|
@@ -186,26 +219,27 @@ export class MidyGM1 {
|
|
|
186
219
|
return audioBuffer;
|
|
187
220
|
}
|
|
188
221
|
else {
|
|
189
|
-
const sample =
|
|
222
|
+
const sample = instrumentKey.sample.subarray(0, sampleEnd);
|
|
190
223
|
const floatSample = this.convertToFloat32Array(sample);
|
|
191
224
|
const audioBuffer = new AudioBuffer({
|
|
192
225
|
numberOfChannels: 1,
|
|
193
226
|
length: sample.length,
|
|
194
|
-
sampleRate:
|
|
227
|
+
sampleRate: instrumentKey.sampleRate,
|
|
195
228
|
});
|
|
196
229
|
const channelData = audioBuffer.getChannelData(0);
|
|
197
230
|
channelData.set(floatSample);
|
|
198
231
|
return audioBuffer;
|
|
199
232
|
}
|
|
200
233
|
}
|
|
201
|
-
async createNoteBufferNode(
|
|
234
|
+
async createNoteBufferNode(instrumentKey, isSF3) {
|
|
202
235
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
203
|
-
const audioBuffer = await this.createNoteBuffer(
|
|
236
|
+
const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
|
|
204
237
|
bufferSource.buffer = audioBuffer;
|
|
205
|
-
bufferSource.loop =
|
|
238
|
+
bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
|
|
206
239
|
if (bufferSource.loop) {
|
|
207
|
-
bufferSource.loopStart =
|
|
208
|
-
|
|
240
|
+
bufferSource.loopStart = instrumentKey.loopStart /
|
|
241
|
+
instrumentKey.sampleRate;
|
|
242
|
+
bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
|
|
209
243
|
}
|
|
210
244
|
return bufferSource;
|
|
211
245
|
}
|
|
@@ -243,7 +277,7 @@ export class MidyGM1 {
|
|
|
243
277
|
this.handleProgramChange(event.channel, event.programNumber);
|
|
244
278
|
break;
|
|
245
279
|
case "pitchBend":
|
|
246
|
-
this.
|
|
280
|
+
this.setPitchBend(event.channel, event.value);
|
|
247
281
|
break;
|
|
248
282
|
case "sysEx":
|
|
249
283
|
this.handleSysEx(event.data);
|
|
@@ -323,7 +357,6 @@ export class MidyGM1 {
|
|
|
323
357
|
const tmpChannels = new Array(16);
|
|
324
358
|
for (let i = 0; i < tmpChannels.length; i++) {
|
|
325
359
|
tmpChannels[i] = {
|
|
326
|
-
durationTicks: new Map(),
|
|
327
360
|
programNumber: -1,
|
|
328
361
|
bank: this.channels[i].bank,
|
|
329
362
|
};
|
|
@@ -340,16 +373,6 @@ export class MidyGM1 {
|
|
|
340
373
|
instruments.add(`${channel.bank}:0`);
|
|
341
374
|
channel.programNumber = 0;
|
|
342
375
|
}
|
|
343
|
-
channel.durationTicks.set(event.noteNumber, {
|
|
344
|
-
ticks: event.ticks,
|
|
345
|
-
noteOn: event,
|
|
346
|
-
});
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
case "noteOff": {
|
|
350
|
-
const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
|
|
351
|
-
.get(event.noteNumber);
|
|
352
|
-
noteOn.durationTicks = event.ticks - ticks;
|
|
353
376
|
break;
|
|
354
377
|
}
|
|
355
378
|
case "programChange": {
|
|
@@ -363,8 +386,8 @@ export class MidyGM1 {
|
|
|
363
386
|
});
|
|
364
387
|
});
|
|
365
388
|
const priority = {
|
|
366
|
-
|
|
367
|
-
|
|
389
|
+
controller: 0,
|
|
390
|
+
sysEx: 1,
|
|
368
391
|
};
|
|
369
392
|
timeline.sort((a, b) => {
|
|
370
393
|
if (a.ticks !== b.ticks)
|
|
@@ -447,30 +470,26 @@ export class MidyGM1 {
|
|
|
447
470
|
const now = this.audioContext.currentTime;
|
|
448
471
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
449
472
|
}
|
|
450
|
-
getActiveNotes(channel) {
|
|
473
|
+
getActiveNotes(channel, time) {
|
|
451
474
|
const activeNotes = new Map();
|
|
452
|
-
channel.scheduledNotes.forEach((
|
|
453
|
-
const activeNote = this.
|
|
475
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
476
|
+
const activeNote = this.getActiveNote(noteList, time);
|
|
454
477
|
if (activeNote) {
|
|
455
478
|
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
456
479
|
}
|
|
457
480
|
});
|
|
458
481
|
return activeNotes;
|
|
459
482
|
}
|
|
460
|
-
|
|
461
|
-
for (let i =
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
464
|
-
return
|
|
483
|
+
getActiveNote(noteList, time) {
|
|
484
|
+
for (let i = noteList.length - 1; i >= 0; i--) {
|
|
485
|
+
const note = noteList[i];
|
|
486
|
+
if (!note)
|
|
487
|
+
return;
|
|
488
|
+
if (time < note.startTime)
|
|
489
|
+
continue;
|
|
490
|
+
return (note.ending) ? null : note;
|
|
465
491
|
}
|
|
466
|
-
|
|
467
|
-
createModulationEffect(audioContext) {
|
|
468
|
-
const lfo = new OscillatorNode(audioContext, {
|
|
469
|
-
frequency: 5,
|
|
470
|
-
});
|
|
471
|
-
return {
|
|
472
|
-
lfo,
|
|
473
|
-
};
|
|
492
|
+
return noteList[0];
|
|
474
493
|
}
|
|
475
494
|
connectNoteEffects(channel, gainNode) {
|
|
476
495
|
gainNode.connect(channel.pannerNode);
|
|
@@ -485,71 +504,87 @@ export class MidyGM1 {
|
|
|
485
504
|
const tuning = channel.coarseTuning + channel.fineTuning;
|
|
486
505
|
return channel.pitchBend * channel.pitchBendRange + tuning;
|
|
487
506
|
}
|
|
488
|
-
calcPlaybackRate(
|
|
489
|
-
return
|
|
507
|
+
calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
|
|
508
|
+
return instrumentKey.playbackRate(noteNumber) *
|
|
509
|
+
Math.pow(2, semitoneOffset / 12);
|
|
490
510
|
}
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
|
|
495
|
-
// volume envelope
|
|
496
|
-
const gainNode = new GainNode(this.audioContext, {
|
|
497
|
-
gain: 0,
|
|
498
|
-
});
|
|
511
|
+
setVolumeEnvelope(channel, note) {
|
|
512
|
+
const { instrumentKey, startTime, velocity } = note;
|
|
513
|
+
note.gainNode = new GainNode(this.audioContext, { gain: 0 });
|
|
499
514
|
let volume = (velocity / 127) * channel.volume * channel.expression;
|
|
500
515
|
if (volume === 0)
|
|
501
516
|
volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
|
|
502
|
-
const attackVolume = this.cbToRatio(-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
|
|
517
|
+
const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
|
|
518
|
+
volume;
|
|
519
|
+
const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
|
|
520
|
+
const volDelay = startTime + instrumentKey.volDelay;
|
|
521
|
+
const volAttack = volDelay + instrumentKey.volAttack;
|
|
522
|
+
const volHold = volAttack + instrumentKey.volHold;
|
|
523
|
+
const volDecay = volHold + instrumentKey.volDecay;
|
|
524
|
+
note.gainNode.gain
|
|
509
525
|
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
510
526
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
511
527
|
.setValueAtTime(attackVolume, volHold)
|
|
512
528
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
513
|
-
|
|
529
|
+
}
|
|
530
|
+
setFilterEnvelope(channel, note) {
|
|
531
|
+
const { instrumentKey, startTime, noteNumber } = note;
|
|
532
|
+
const softPedalFactor = 1 -
|
|
533
|
+
(0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
|
|
514
534
|
const maxFreq = this.audioContext.sampleRate / 2;
|
|
515
|
-
const baseFreq = this.centToHz(
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
535
|
+
const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
|
|
536
|
+
softPedalFactor;
|
|
537
|
+
const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
|
|
538
|
+
const sustainFreq = (baseFreq +
|
|
539
|
+
(peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
|
|
540
|
+
const modDelay = startTime + instrumentKey.modDelay;
|
|
541
|
+
const modAttack = modDelay + instrumentKey.modAttack;
|
|
542
|
+
const modHold = modAttack + instrumentKey.modHold;
|
|
543
|
+
const modDecay = modHold + instrumentKey.modDecay;
|
|
519
544
|
const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
|
|
520
545
|
const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
|
|
521
546
|
const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
|
|
522
|
-
|
|
547
|
+
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
523
548
|
type: "lowpass",
|
|
524
|
-
Q:
|
|
549
|
+
Q: instrumentKey.initialFilterQ / 10, // dB
|
|
525
550
|
frequency: adjustedBaseFreq,
|
|
526
551
|
});
|
|
527
|
-
|
|
528
|
-
const modAttack = modDelay + noteInfo.modAttack;
|
|
529
|
-
const modHold = modAttack + noteInfo.modHold;
|
|
530
|
-
const modDecay = modHold + noteInfo.modDecay;
|
|
531
|
-
filterNode.frequency
|
|
552
|
+
note.filterNode.frequency
|
|
532
553
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
533
554
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
534
555
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
535
556
|
.linearRampToValueAtTime(adjustedSustainFreq, modDecay);
|
|
536
|
-
|
|
557
|
+
note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
|
|
558
|
+
}
|
|
559
|
+
startModulation(channel, note, time) {
|
|
560
|
+
const { instrumentKey } = note;
|
|
561
|
+
note.modLFOGain = new GainNode(this.audioContext, {
|
|
562
|
+
gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
|
|
563
|
+
});
|
|
564
|
+
note.modLFO = new OscillatorNode(this.audioContext, {
|
|
565
|
+
frequency: this.centToHz(instrumentKey.freqModLFO),
|
|
566
|
+
});
|
|
567
|
+
note.modLFO.start(time);
|
|
568
|
+
note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
|
|
569
|
+
note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
|
|
570
|
+
note.modLFO.connect(note.modLFOGain);
|
|
571
|
+
note.modLFOGain.connect(note.bufferSource.detune);
|
|
572
|
+
}
|
|
573
|
+
async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
|
|
574
|
+
const semitoneOffset = this.calcSemitoneOffset(channel);
|
|
575
|
+
const note = new Note(noteNumber, velocity, startTime, instrumentKey);
|
|
576
|
+
note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
|
|
577
|
+
note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
|
|
578
|
+
this.setVolumeEnvelope(channel, note);
|
|
579
|
+
this.setFilterEnvelope(channel, note);
|
|
537
580
|
if (channel.modulation > 0) {
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
lfoGain = new GainNode(this.audioContext, {
|
|
541
|
-
gain: 0,
|
|
542
|
-
});
|
|
543
|
-
lfoGain.gain
|
|
544
|
-
.setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
545
|
-
.exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
|
|
546
|
-
channel.modulationEffect.lfo.connect(lfoGain);
|
|
547
|
-
lfoGain.connect(bufferSource.detune);
|
|
581
|
+
const delayModLFO = startTime + instrumentKey.delayModLFO;
|
|
582
|
+
this.startModulation(channel, note, delayModLFO);
|
|
548
583
|
}
|
|
549
|
-
bufferSource.connect(filterNode);
|
|
550
|
-
filterNode.connect(gainNode);
|
|
551
|
-
bufferSource.start(startTime,
|
|
552
|
-
return
|
|
584
|
+
note.bufferSource.connect(note.filterNode);
|
|
585
|
+
note.filterNode.connect(note.gainNode);
|
|
586
|
+
note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
|
|
587
|
+
return note;
|
|
553
588
|
}
|
|
554
589
|
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
555
590
|
const channel = this.channels[channelNumber];
|
|
@@ -559,27 +594,17 @@ export class MidyGM1 {
|
|
|
559
594
|
return;
|
|
560
595
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
561
596
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
562
|
-
const
|
|
563
|
-
if (!
|
|
597
|
+
const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
|
|
598
|
+
if (!instrumentKey)
|
|
564
599
|
return;
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
this.connectNoteEffects(channel, gainNode);
|
|
600
|
+
const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
|
|
601
|
+
this.connectNoteEffects(channel, note.gainNode);
|
|
568
602
|
const scheduledNotes = channel.scheduledNotes;
|
|
569
|
-
const scheduledNote = {
|
|
570
|
-
bufferSource,
|
|
571
|
-
filterNode,
|
|
572
|
-
gainNode,
|
|
573
|
-
lfoGain,
|
|
574
|
-
noteInfo,
|
|
575
|
-
noteNumber,
|
|
576
|
-
startTime,
|
|
577
|
-
};
|
|
578
603
|
if (scheduledNotes.has(noteNumber)) {
|
|
579
|
-
scheduledNotes.get(noteNumber).push(
|
|
604
|
+
scheduledNotes.get(noteNumber).push(note);
|
|
580
605
|
}
|
|
581
606
|
else {
|
|
582
|
-
scheduledNotes.set(noteNumber, [
|
|
607
|
+
scheduledNotes.set(noteNumber, [note]);
|
|
583
608
|
}
|
|
584
609
|
}
|
|
585
610
|
noteOn(channelNumber, noteNumber, velocity) {
|
|
@@ -599,15 +624,15 @@ export class MidyGM1 {
|
|
|
599
624
|
continue;
|
|
600
625
|
if (targetNote.ending)
|
|
601
626
|
continue;
|
|
602
|
-
const { bufferSource, filterNode, gainNode,
|
|
627
|
+
const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, instrumentKey, } = targetNote;
|
|
603
628
|
const velocityRate = (velocity + 127) / 127;
|
|
604
|
-
const volEndTime = stopTime +
|
|
629
|
+
const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
|
|
605
630
|
gainNode.gain.cancelScheduledValues(stopTime);
|
|
606
631
|
gainNode.gain.linearRampToValueAtTime(0, volEndTime);
|
|
607
632
|
const maxFreq = this.audioContext.sampleRate / 2;
|
|
608
|
-
const baseFreq = this.centToHz(
|
|
633
|
+
const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
|
|
609
634
|
const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
|
|
610
|
-
const modEndTime = stopTime +
|
|
635
|
+
const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
|
|
611
636
|
filterNode.frequency
|
|
612
637
|
.cancelScheduledValues(stopTime)
|
|
613
638
|
.linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
|
|
@@ -621,8 +646,10 @@ export class MidyGM1 {
|
|
|
621
646
|
bufferSource.disconnect(0);
|
|
622
647
|
filterNode.disconnect(0);
|
|
623
648
|
gainNode.disconnect(0);
|
|
624
|
-
if (
|
|
625
|
-
|
|
649
|
+
if (modLFOGain)
|
|
650
|
+
modLFOGain.disconnect(0);
|
|
651
|
+
if (modLFO)
|
|
652
|
+
modLFO.stop();
|
|
626
653
|
resolve();
|
|
627
654
|
};
|
|
628
655
|
bufferSource.stop(volEndTime);
|
|
@@ -673,20 +700,22 @@ export class MidyGM1 {
|
|
|
673
700
|
}
|
|
674
701
|
handlePitchBendMessage(channelNumber, lsb, msb) {
|
|
675
702
|
const pitchBend = msb * 128 + lsb;
|
|
676
|
-
this.
|
|
703
|
+
this.setPitchBend(channelNumber, pitchBend);
|
|
677
704
|
}
|
|
678
|
-
|
|
705
|
+
setPitchBend(channelNumber, pitchBend) {
|
|
679
706
|
const now = this.audioContext.currentTime;
|
|
680
707
|
const channel = this.channels[channelNumber];
|
|
708
|
+
const prevPitchBend = channel.pitchBend;
|
|
681
709
|
channel.pitchBend = (pitchBend - 8192) / 8192;
|
|
682
|
-
const
|
|
683
|
-
|
|
710
|
+
const detuneChange = (channel.pitchBend - prevPitchBend) *
|
|
711
|
+
channel.pitchBendRange * 100;
|
|
712
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
684
713
|
activeNotes.forEach((activeNote) => {
|
|
685
|
-
const { bufferSource
|
|
686
|
-
const
|
|
687
|
-
bufferSource.
|
|
714
|
+
const { bufferSource } = activeNote;
|
|
715
|
+
const detune = bufferSource.detune.value + detuneChange;
|
|
716
|
+
bufferSource.detune
|
|
688
717
|
.cancelScheduledValues(now)
|
|
689
|
-
.setValueAtTime(
|
|
718
|
+
.setValueAtTime(detune, now);
|
|
690
719
|
});
|
|
691
720
|
}
|
|
692
721
|
handleControlChange(channelNumber, controller, value) {
|
|
@@ -720,21 +749,37 @@ export class MidyGM1 {
|
|
|
720
749
|
}
|
|
721
750
|
}
|
|
722
751
|
setModulation(channelNumber, modulation) {
|
|
752
|
+
const now = this.audioContext.currentTime;
|
|
723
753
|
const channel = this.channels[channelNumber];
|
|
724
754
|
channel.modulation = (modulation / 127) *
|
|
725
755
|
(channel.modulationDepthRange * 100);
|
|
756
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
757
|
+
activeNotes.forEach((activeNote) => {
|
|
758
|
+
if (activeNote.modLFO) {
|
|
759
|
+
activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
|
|
760
|
+
channel.modulation, now);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
this.startModulation(channel, activeNote, now);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
726
766
|
}
|
|
727
767
|
setVolume(channelNumber, volume) {
|
|
728
768
|
const channel = this.channels[channelNumber];
|
|
729
769
|
channel.volume = volume / 127;
|
|
730
770
|
this.updateChannelGain(channel);
|
|
731
771
|
}
|
|
772
|
+
panToGain(pan) {
|
|
773
|
+
const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
|
|
774
|
+
return {
|
|
775
|
+
gainLeft: Math.cos(theta),
|
|
776
|
+
gainRight: Math.sin(theta),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
732
779
|
setPan(channelNumber, pan) {
|
|
733
|
-
const now = this.audioContext.currentTime;
|
|
734
780
|
const channel = this.channels[channelNumber];
|
|
735
|
-
channel.pan = pan
|
|
736
|
-
|
|
737
|
-
channel.pannerNode.pan.setValueAtTime(channel.pan, now);
|
|
781
|
+
channel.pan = pan;
|
|
782
|
+
this.updateChannelGain(channel);
|
|
738
783
|
}
|
|
739
784
|
setExpression(channelNumber, expression) {
|
|
740
785
|
const channel = this.channels[channelNumber];
|
|
@@ -744,8 +789,13 @@ export class MidyGM1 {
|
|
|
744
789
|
updateChannelGain(channel) {
|
|
745
790
|
const now = this.audioContext.currentTime;
|
|
746
791
|
const volume = channel.volume * channel.expression;
|
|
747
|
-
channel.
|
|
748
|
-
channel.
|
|
792
|
+
const { gainLeft, gainRight } = this.panToGain(channel.pan);
|
|
793
|
+
channel.gainL.gain
|
|
794
|
+
.cancelScheduledValues(now)
|
|
795
|
+
.setValueAtTime(volume * gainLeft, now);
|
|
796
|
+
channel.gainR.gain
|
|
797
|
+
.cancelScheduledValues(now)
|
|
798
|
+
.setValueAtTime(volume * gainRight, now);
|
|
749
799
|
}
|
|
750
800
|
setSustainPedal(channelNumber, value) {
|
|
751
801
|
const isOn = value >= 64;
|
|
@@ -767,8 +817,7 @@ export class MidyGM1 {
|
|
|
767
817
|
const { dataMSB, dataLSB } = channel;
|
|
768
818
|
switch (rpn) {
|
|
769
819
|
case 0:
|
|
770
|
-
|
|
771
|
-
break;
|
|
820
|
+
return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
|
|
772
821
|
case 1:
|
|
773
822
|
channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
|
|
774
823
|
break;
|
|
@@ -779,14 +828,34 @@ export class MidyGM1 {
|
|
|
779
828
|
console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
|
|
780
829
|
}
|
|
781
830
|
}
|
|
831
|
+
handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
|
|
832
|
+
const pitchBendRange = dataMSB + dataLSB / 100;
|
|
833
|
+
this.setPitchBendRange(channelNumber, pitchBendRange);
|
|
834
|
+
}
|
|
835
|
+
setPitchBendRange(channelNumber, pitchBendRange) {
|
|
836
|
+
const now = this.audioContext.currentTime;
|
|
837
|
+
const channel = this.channels[channelNumber];
|
|
838
|
+
const prevPitchBendRange = channel.pitchBendRange;
|
|
839
|
+
channel.pitchBendRange = pitchBendRange;
|
|
840
|
+
const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
|
|
841
|
+
channel.pitchBend * 100;
|
|
842
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
843
|
+
activeNotes.forEach((activeNote) => {
|
|
844
|
+
const { bufferSource } = activeNote;
|
|
845
|
+
const detune = bufferSource.detune.value + detuneChange;
|
|
846
|
+
bufferSource.detune
|
|
847
|
+
.cancelScheduledValues(now)
|
|
848
|
+
.setValueAtTime(detune, now);
|
|
849
|
+
});
|
|
850
|
+
}
|
|
782
851
|
allSoundOff(channelNumber) {
|
|
783
852
|
const now = this.audioContext.currentTime;
|
|
784
853
|
const channel = this.channels[channelNumber];
|
|
785
854
|
const velocity = 0;
|
|
786
855
|
const stopPedal = true;
|
|
787
856
|
const promises = [];
|
|
788
|
-
channel.scheduledNotes.forEach((
|
|
789
|
-
const activeNote = this.
|
|
857
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
858
|
+
const activeNote = this.getActiveNote(noteList, now);
|
|
790
859
|
if (activeNote) {
|
|
791
860
|
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
792
861
|
promises.push(notePromise);
|
|
@@ -803,8 +872,8 @@ export class MidyGM1 {
|
|
|
803
872
|
const velocity = 0;
|
|
804
873
|
const stopPedal = false;
|
|
805
874
|
const promises = [];
|
|
806
|
-
channel.scheduledNotes.forEach((
|
|
807
|
-
const activeNote = this.
|
|
875
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
876
|
+
const activeNote = this.getActiveNote(noteList, now);
|
|
808
877
|
if (activeNote) {
|
|
809
878
|
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
810
879
|
promises.push(notePromise);
|
|
@@ -854,9 +923,9 @@ export class MidyGM1 {
|
|
|
854
923
|
}
|
|
855
924
|
handleMasterVolumeSysEx(data) {
|
|
856
925
|
const volume = (data[5] * 128 + data[4]) / 16383;
|
|
857
|
-
this.
|
|
926
|
+
this.setMasterVolume(volume);
|
|
858
927
|
}
|
|
859
|
-
|
|
928
|
+
setMasterVolume(volume) {
|
|
860
929
|
if (volume < 0 && 1 < volume) {
|
|
861
930
|
console.error("Master Volume is out of range");
|
|
862
931
|
}
|
|
@@ -897,10 +966,7 @@ Object.defineProperty(MidyGM1, "channelSettings", {
|
|
|
897
966
|
writable: true,
|
|
898
967
|
value: {
|
|
899
968
|
volume: 100 / 127,
|
|
900
|
-
pan:
|
|
901
|
-
vibratoRate: 5,
|
|
902
|
-
vibratoDepth: 0.5,
|
|
903
|
-
vibratoDelay: 2.5,
|
|
969
|
+
pan: 64,
|
|
904
970
|
bank: 0,
|
|
905
971
|
dataMSB: 0,
|
|
906
972
|
dataLSB: 0,
|