action-engine-js 1.0.0
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/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- package/package.json +35 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
// actionengine/sound/actionsoundfont.js
|
|
2
|
+
// ActionSoundFont - Custom SoundFont player implementation
|
|
3
|
+
// Drop-in replacement for 3rd party sf2-player library
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ActionSoundFont Class
|
|
7
|
+
* Provides SoundFont2 playback capabilities using Web Audio API
|
|
8
|
+
*/
|
|
9
|
+
class ActionSoundFont {
|
|
10
|
+
constructor(ctx) {
|
|
11
|
+
this.ctx = ctx;
|
|
12
|
+
this.synth = null;
|
|
13
|
+
this._channel = 0;
|
|
14
|
+
this._bankIndex = 0;
|
|
15
|
+
this._programIndex = 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
set channel(channel) {
|
|
19
|
+
this._channel = channel;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
set bank(index) {
|
|
23
|
+
this._bankIndex = index;
|
|
24
|
+
if (this.synth) {
|
|
25
|
+
this.synth.bankChange(this._channel, index);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get banks() {
|
|
30
|
+
if (!this.synth || !this.synth.programSet) return [];
|
|
31
|
+
return Object.keys(this.synth.programSet).map(id => ({
|
|
32
|
+
id,
|
|
33
|
+
name: ('000' + parseInt(id, 10)).slice(-3)
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set program(index) {
|
|
38
|
+
this._programIndex = index;
|
|
39
|
+
if (this.synth) {
|
|
40
|
+
this.synth.programChange(this._channel, index);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get programs() {
|
|
45
|
+
if (!this.synth || !this.synth.programSet) return [];
|
|
46
|
+
const { programSet } = this.synth;
|
|
47
|
+
return Object.keys(programSet[this._bankIndex] || {}).map(id => ({
|
|
48
|
+
id,
|
|
49
|
+
name: ('000' + (parseInt(id, 10) + 1)).slice(-3) + ':' + programSet[this._bankIndex][id]
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load SoundFont from base64 encoded data
|
|
55
|
+
* @param {string} base64String - Base64 encoded SF2 data
|
|
56
|
+
*/
|
|
57
|
+
async loadSoundFontFromBase64(base64String) {
|
|
58
|
+
const base64 = base64String.split(',')[1] || base64String;
|
|
59
|
+
const binaryString = atob(base64);
|
|
60
|
+
const len = binaryString.length;
|
|
61
|
+
const arrayBuffer = new ArrayBuffer(len);
|
|
62
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < len; i++) {
|
|
65
|
+
uint8Array[i] = binaryString.charCodeAt(i);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await this.bootSynth(arrayBuffer);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Boot the synthesizer with SF2 data
|
|
73
|
+
* @param {ArrayBuffer} arrayBuffer - SF2 file data
|
|
74
|
+
*/
|
|
75
|
+
async bootSynth(arrayBuffer) {
|
|
76
|
+
const input = new Uint8Array(arrayBuffer);
|
|
77
|
+
this.synth = new ActionSynthesizer(input, this.ctx);
|
|
78
|
+
this.synth.init();
|
|
79
|
+
this.synth.start();
|
|
80
|
+
|
|
81
|
+
// Wait for programSet to be populated
|
|
82
|
+
await this.waitForReference(() => this.synth.programSet);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Wait for a reference to be defined
|
|
87
|
+
* @param {Function} refGetter - Function that returns the reference to wait for
|
|
88
|
+
*/
|
|
89
|
+
waitForReference(refGetter) {
|
|
90
|
+
return new Promise(resolve => {
|
|
91
|
+
const check = () => {
|
|
92
|
+
const ref = refGetter();
|
|
93
|
+
if (ref !== undefined && Object.keys(ref).length > 0) {
|
|
94
|
+
resolve();
|
|
95
|
+
} else {
|
|
96
|
+
setTimeout(check, 16);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
check();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Play a MIDI note
|
|
105
|
+
* @param {number} midiNumber - MIDI note number (0-127)
|
|
106
|
+
* @param {number} velocity - Note velocity (0-127)
|
|
107
|
+
* @param {number} channel - MIDI channel (0-15)
|
|
108
|
+
*/
|
|
109
|
+
noteOn(midiNumber, velocity = 127, channel) {
|
|
110
|
+
if (!this.synth) return;
|
|
111
|
+
this.synth.noteOn(channel !== undefined ? channel : this._channel, midiNumber, velocity);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stop a MIDI note
|
|
116
|
+
* @param {number} midiNumber - MIDI note number (0-127)
|
|
117
|
+
* @param {number} velocity - Release velocity (0-127)
|
|
118
|
+
* @param {number} channel - MIDI channel (0-15)
|
|
119
|
+
*/
|
|
120
|
+
noteOff(midiNumber, velocity = 127, channel) {
|
|
121
|
+
if (!this.synth) return;
|
|
122
|
+
this.synth.noteOff(channel !== undefined ? channel : this._channel, midiNumber, velocity);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* ActionSynthesizer Class
|
|
128
|
+
* Core synthesizer engine for SoundFont playback
|
|
129
|
+
*/
|
|
130
|
+
class ActionSynthesizer {
|
|
131
|
+
constructor(input, ctx) {
|
|
132
|
+
this.input = input;
|
|
133
|
+
this.ctx = ctx || this.getAudioContext();
|
|
134
|
+
this.parser = null;
|
|
135
|
+
this.bank = 0;
|
|
136
|
+
this.bankSet = [];
|
|
137
|
+
this.programSet = {};
|
|
138
|
+
|
|
139
|
+
// Audio nodes
|
|
140
|
+
this.gainMaster = this.ctx.createGain();
|
|
141
|
+
this.bufSrc = this.ctx.createBufferSource();
|
|
142
|
+
|
|
143
|
+
// MIDI channel state (16 channels)
|
|
144
|
+
this.channelInstrument = new Array(16).fill(0);
|
|
145
|
+
this.channelBank = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 0]; // Channel 10 = drums
|
|
146
|
+
this.channelVolume = new Array(16).fill(127);
|
|
147
|
+
this.channelPanpot = new Array(16).fill(64);
|
|
148
|
+
this.channelPitchBend = new Array(16).fill(8192);
|
|
149
|
+
this.channelPitchBendSensitivity = new Array(16).fill(2);
|
|
150
|
+
this.channelExpression = new Array(16).fill(127);
|
|
151
|
+
this.channelHold = new Array(16).fill(false);
|
|
152
|
+
|
|
153
|
+
// Percussion settings
|
|
154
|
+
this.percussionPart = [false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false];
|
|
155
|
+
this.percussionVolume = new Array(128).fill(127);
|
|
156
|
+
|
|
157
|
+
// Active notes
|
|
158
|
+
this.currentNoteOn = Array.from({ length: 16 }, () => []);
|
|
159
|
+
|
|
160
|
+
// Volume settings
|
|
161
|
+
this.baseVolume = 1 / 0xffff;
|
|
162
|
+
this.masterVolume = 16384;
|
|
163
|
+
|
|
164
|
+
// Shared reverb
|
|
165
|
+
if (!ActionSynthesizer.sharedReverb) {
|
|
166
|
+
ActionSynthesizer.sharedReverb = new ActionReverb(this.ctx, { mix: 0.315 });
|
|
167
|
+
}
|
|
168
|
+
this.reverb = new Array(16).fill(ActionSynthesizer.sharedReverb);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
init(mode = 'GM') {
|
|
172
|
+
this.gainMaster.disconnect();
|
|
173
|
+
|
|
174
|
+
// Parse SF2 file
|
|
175
|
+
this.parser = new ActionParser(this.input, {
|
|
176
|
+
sampleRate: this.ctx.sampleRate
|
|
177
|
+
});
|
|
178
|
+
this.bankSet = this.createAllInstruments();
|
|
179
|
+
|
|
180
|
+
// Reset all channels
|
|
181
|
+
for (let i = 0; i < 16; i++) {
|
|
182
|
+
this.programChange(i, 0x00);
|
|
183
|
+
this.volumeChange(i, 0x64);
|
|
184
|
+
this.panpotChange(i, 0x40);
|
|
185
|
+
this.pitchBend(i, 0x00, 0x40);
|
|
186
|
+
this.pitchBendSensitivity(i, 2);
|
|
187
|
+
this.channelHold[i] = false;
|
|
188
|
+
this.channelExpression[i] = 127;
|
|
189
|
+
this.channelBank[i] = i === 9 ? 127 : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.setPercussionPart(9, true);
|
|
193
|
+
this.gainMaster.connect(this.ctx.destination);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
start() {
|
|
197
|
+
this.connect();
|
|
198
|
+
this.bufSrc.start(0);
|
|
199
|
+
this.setMasterVolume(16383);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
connect() {
|
|
203
|
+
this.bufSrc.connect(this.gainMaster);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setMasterVolume(volume) {
|
|
207
|
+
this.masterVolume = volume;
|
|
208
|
+
this.gainMaster.gain.value = this.baseVolume * (volume / 16384);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
createAllInstruments() {
|
|
212
|
+
const { parser } = this;
|
|
213
|
+
parser.parse();
|
|
214
|
+
|
|
215
|
+
const presets = parser.createPreset();
|
|
216
|
+
const instruments = parser.createInstrument();
|
|
217
|
+
const banks = [];
|
|
218
|
+
const programSet = [];
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < presets.length; i++) {
|
|
221
|
+
const preset = presets[i];
|
|
222
|
+
const presetNumber = preset.header.preset;
|
|
223
|
+
const bankNumber = preset.header.bank;
|
|
224
|
+
const presetName = preset.name.replace(/\0*$/, '');
|
|
225
|
+
|
|
226
|
+
if (typeof preset.instrument !== 'number') continue;
|
|
227
|
+
|
|
228
|
+
const instrument = instruments[preset.instrument];
|
|
229
|
+
if (instrument.name.replace(/\0*$/, '') === 'EOI') continue;
|
|
230
|
+
|
|
231
|
+
if (banks[bankNumber] === undefined) {
|
|
232
|
+
banks[bankNumber] = [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const bank = banks[bankNumber];
|
|
236
|
+
bank[presetNumber] = { name: presetName };
|
|
237
|
+
|
|
238
|
+
for (let j = 0; j < instrument.info.length; j++) {
|
|
239
|
+
this.createNoteInfo(parser, instrument.info[j], bank[presetNumber]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!programSet[bankNumber]) {
|
|
243
|
+
programSet[bankNumber] = {};
|
|
244
|
+
}
|
|
245
|
+
programSet[bankNumber][presetNumber] = presetName;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.programSet = programSet;
|
|
249
|
+
return banks;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
createNoteInfo(parser, info, preset) {
|
|
253
|
+
const generator = info.generator;
|
|
254
|
+
if (generator.keyRange === undefined || generator.sampleID === undefined) return;
|
|
255
|
+
|
|
256
|
+
const getAmount = (gen, key, defaultVal = 0) => {
|
|
257
|
+
return gen[key] ? gen[key].amount : defaultVal;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const volAttack = getAmount(generator, 'attackVolEnv', -12000);
|
|
261
|
+
const volDecay = getAmount(generator, 'decayVolEnv', -12000);
|
|
262
|
+
const volSustain = getAmount(generator, 'sustainVolEnv');
|
|
263
|
+
const volRelease = getAmount(generator, 'releaseVolEnv', -12000);
|
|
264
|
+
const tune = getAmount(generator, 'coarseTune') + getAmount(generator, 'fineTune') / 100;
|
|
265
|
+
const scale = getAmount(generator, 'scaleTuning', 100) / 100;
|
|
266
|
+
|
|
267
|
+
for (let i = generator.keyRange.lo; i <= generator.keyRange.hi; i++) {
|
|
268
|
+
if (preset[i]) continue;
|
|
269
|
+
|
|
270
|
+
const sampleId = getAmount(generator, 'sampleID');
|
|
271
|
+
const sampleHeader = parser.sampleHeader[sampleId];
|
|
272
|
+
|
|
273
|
+
preset[i] = {
|
|
274
|
+
sample: parser.sample[sampleId],
|
|
275
|
+
sampleRate: sampleHeader.sampleRate,
|
|
276
|
+
sampleModes: getAmount(generator, 'sampleModes'),
|
|
277
|
+
basePlaybackRate: Math.pow(1.0594630943592953, (
|
|
278
|
+
i - getAmount(generator, 'overridingRootKey', sampleHeader.originalPitch) +
|
|
279
|
+
tune + (sampleHeader.pitchCorrection / 100)
|
|
280
|
+
) * scale),
|
|
281
|
+
start: getAmount(generator, 'startAddrsCoarseOffset') * 32768 + getAmount(generator, 'startAddrsOffset'),
|
|
282
|
+
end: getAmount(generator, 'endAddrsCoarseOffset') * 32768 + getAmount(generator, 'endAddrsOffset'),
|
|
283
|
+
loopStart: sampleHeader.startLoop + getAmount(generator, 'startloopAddrsCoarseOffset') * 32768 + getAmount(generator, 'startloopAddrsOffset'),
|
|
284
|
+
loopEnd: sampleHeader.endLoop + getAmount(generator, 'endloopAddrsCoarseOffset') * 32768 + getAmount(generator, 'endloopAddrsOffset'),
|
|
285
|
+
volAttack: Math.pow(2, volAttack / 1200),
|
|
286
|
+
volDecay: Math.pow(2, volDecay / 1200),
|
|
287
|
+
volSustain: volSustain / 1000,
|
|
288
|
+
volRelease: Math.pow(2, volRelease / 1200),
|
|
289
|
+
initialAttenuation: getAmount(generator, 'initialAttenuation'),
|
|
290
|
+
initialFilterFc: getAmount(generator, 'initialFilterFc', 13500),
|
|
291
|
+
initialFilterQ: getAmount(generator, 'initialFilterQ'),
|
|
292
|
+
pan: getAmount(generator, 'pan') ? getAmount(generator, 'pan') / 1200 : undefined
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
noteOn(channel, key, velocity) {
|
|
298
|
+
const bankIndex = this.channelBank[channel];
|
|
299
|
+
let bank = this.bankSet[bankIndex] || this.bankSet[0];
|
|
300
|
+
|
|
301
|
+
if (!bank) {
|
|
302
|
+
console.warn('No valid bank found');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let instrument;
|
|
307
|
+
if (bank[this.channelInstrument[channel]]) {
|
|
308
|
+
instrument = bank[this.channelInstrument[channel]];
|
|
309
|
+
} else if (this.percussionPart[channel]) {
|
|
310
|
+
instrument = this.bankSet[128]?.[0] || this.bankSet[0]?.[0];
|
|
311
|
+
} else {
|
|
312
|
+
instrument = this.bankSet[0]?.[this.channelInstrument[channel]];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!instrument || !instrument[key]) {
|
|
316
|
+
console.warn(`Instrument not found: bank=${bankIndex} program=${this.channelInstrument[channel]} key=${key}`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const instrumentKey = instrument[key];
|
|
321
|
+
let panpot = this.channelPanpot[channel] === 0 ? (Math.random() * 127) | 0 : this.channelPanpot[channel] - 64;
|
|
322
|
+
panpot /= panpot < 0 ? 64 : 63;
|
|
323
|
+
|
|
324
|
+
// Create note information
|
|
325
|
+
instrumentKey.channel = channel;
|
|
326
|
+
instrumentKey.key = key;
|
|
327
|
+
instrumentKey.velocity = velocity;
|
|
328
|
+
instrumentKey.panpot = panpot;
|
|
329
|
+
instrumentKey.volume = this.channelVolume[channel] / 127;
|
|
330
|
+
instrumentKey.pitchBend = this.channelPitchBend[channel] - 8192;
|
|
331
|
+
instrumentKey.expression = this.channelExpression[channel];
|
|
332
|
+
instrumentKey.pitchBendSensitivity = Math.round(this.channelPitchBendSensitivity[channel]);
|
|
333
|
+
instrumentKey.reverb = this.reverb[channel];
|
|
334
|
+
|
|
335
|
+
// Play the note
|
|
336
|
+
const note = new ActionSynthesizerNote(this.ctx, this.gainMaster, instrumentKey);
|
|
337
|
+
note.noteOn();
|
|
338
|
+
this.currentNoteOn[channel].push(note);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
noteOff(channel, key) {
|
|
342
|
+
const currentNoteOn = this.currentNoteOn[channel];
|
|
343
|
+
const hold = this.channelHold[channel];
|
|
344
|
+
|
|
345
|
+
for (let i = currentNoteOn.length - 1; i >= 0; i--) {
|
|
346
|
+
const note = currentNoteOn[i];
|
|
347
|
+
if (note.key === key) {
|
|
348
|
+
note.noteOff();
|
|
349
|
+
if (!hold) {
|
|
350
|
+
note.release();
|
|
351
|
+
currentNoteOn.splice(i, 1);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
programChange(channel, instrument) {
|
|
358
|
+
this.channelInstrument[channel] = instrument;
|
|
359
|
+
this.bankChange(channel, this.channelBank[channel]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
bankChange(channel, bank) {
|
|
363
|
+
if (this.bankSet[bank]) {
|
|
364
|
+
this.channelBank[channel] = bank;
|
|
365
|
+
} else if (this.percussionPart[channel]) {
|
|
366
|
+
this.channelBank[channel] = 128;
|
|
367
|
+
} else {
|
|
368
|
+
this.channelBank[channel] = 0;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
volumeChange(channel, volume) {
|
|
373
|
+
this.channelVolume[channel] = volume;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
panpotChange(channel, panpot) {
|
|
377
|
+
this.channelPanpot[channel] = panpot;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
pitchBend(channel, lowerByte, higherByte) {
|
|
381
|
+
const bend = (lowerByte & 0x7f) | ((higherByte & 0x7f) << 7);
|
|
382
|
+
const calculated = bend - 8192;
|
|
383
|
+
|
|
384
|
+
const currentNoteOn = this.currentNoteOn[channel];
|
|
385
|
+
for (let i = 0; i < currentNoteOn.length; i++) {
|
|
386
|
+
currentNoteOn[i].updatePitchBend(calculated);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.channelPitchBend[channel] = bend;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
pitchBendSensitivity(channel, sensitivity) {
|
|
393
|
+
this.channelPitchBendSensitivity[channel] = sensitivity;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
setPercussionPart(channel, sw) {
|
|
397
|
+
this.channelBank[channel] = 128;
|
|
398
|
+
this.percussionPart[channel] = sw;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
getAudioContext() {
|
|
402
|
+
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
403
|
+
ctx.createGain = ctx.createGain || ctx.createGainNode;
|
|
404
|
+
return ctx;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
ActionSynthesizer.sharedReverb = null;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* ActionSynthesizerNote Class
|
|
412
|
+
* Represents a single playing note
|
|
413
|
+
*/
|
|
414
|
+
class ActionSynthesizerNote {
|
|
415
|
+
constructor(ctx, destination, instrument) {
|
|
416
|
+
this.ctx = ctx;
|
|
417
|
+
this.destination = destination;
|
|
418
|
+
this.instrument = instrument;
|
|
419
|
+
this.key = instrument.key;
|
|
420
|
+
this.velocity = instrument.velocity;
|
|
421
|
+
this.noteOffState = false;
|
|
422
|
+
|
|
423
|
+
// Audio nodes
|
|
424
|
+
this.bufferSource = ctx.createBufferSource();
|
|
425
|
+
this.panner = ctx.createPanner();
|
|
426
|
+
this.outputGainNode = ctx.createGain();
|
|
427
|
+
this.expressionGainNode = ctx.createGain();
|
|
428
|
+
this.filter = ctx.createBiquadFilter();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
noteOn() {
|
|
432
|
+
const ctx = this.ctx;
|
|
433
|
+
const instrument = this.instrument;
|
|
434
|
+
const now = ctx.currentTime;
|
|
435
|
+
|
|
436
|
+
// Create audio buffer from sample
|
|
437
|
+
const sample = instrument.sample.subarray(0, instrument.sample.length + (instrument.end || 0));
|
|
438
|
+
const audioBuffer = ctx.createBuffer(1, sample.length, instrument.sampleRate);
|
|
439
|
+
const channelData = audioBuffer.getChannelData(0);
|
|
440
|
+
channelData.set(sample);
|
|
441
|
+
|
|
442
|
+
// Setup buffer source
|
|
443
|
+
this.bufferSource.buffer = audioBuffer;
|
|
444
|
+
this.bufferSource.loop = (instrument.sampleModes & 1) !== 0;
|
|
445
|
+
this.bufferSource.loopStart = (instrument.loopStart || 0) / instrument.sampleRate;
|
|
446
|
+
this.bufferSource.loopEnd = (instrument.loopEnd || sample.length) / instrument.sampleRate;
|
|
447
|
+
this.bufferSource.playbackRate.value = instrument.basePlaybackRate;
|
|
448
|
+
|
|
449
|
+
// Setup panner
|
|
450
|
+
this.panner.panningModel = 'equalpower';
|
|
451
|
+
const pan = instrument.pan !== undefined ? instrument.pan : instrument.panpot;
|
|
452
|
+
this.panner.setPosition(
|
|
453
|
+
Math.sin(pan * Math.PI / 2),
|
|
454
|
+
0,
|
|
455
|
+
Math.cos(pan * Math.PI / 2)
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Setup filter
|
|
459
|
+
this.filter.type = 'lowpass';
|
|
460
|
+
this.filter.frequency.value = this.amountToFreq(instrument.initialFilterFc);
|
|
461
|
+
this.filter.Q.value = Math.pow(10, (instrument.initialFilterQ || 0) / 200);
|
|
462
|
+
|
|
463
|
+
// Setup expression
|
|
464
|
+
this.expressionGainNode.gain.value = (instrument.expression || 127) / 127;
|
|
465
|
+
|
|
466
|
+
// Setup volume envelope (ADSR)
|
|
467
|
+
let volume = instrument.volume * (this.velocity / 127) * (1 - (instrument.initialAttenuation || 0) / 1000);
|
|
468
|
+
volume = Math.max(0, volume);
|
|
469
|
+
|
|
470
|
+
const outputGain = this.outputGainNode.gain;
|
|
471
|
+
outputGain.setValueAtTime(0, now);
|
|
472
|
+
outputGain.setTargetAtTime(volume, now, instrument.volAttack || 0.01);
|
|
473
|
+
const attackEnd = now + (instrument.volAttack || 0.01) * 3;
|
|
474
|
+
outputGain.setValueAtTime(volume, attackEnd);
|
|
475
|
+
outputGain.linearRampToValueAtTime(volume * (instrument.volSustain || 0.7), attackEnd + (instrument.volDecay || 0.1));
|
|
476
|
+
|
|
477
|
+
// Connect nodes
|
|
478
|
+
this.bufferSource.connect(this.filter);
|
|
479
|
+
this.filter.connect(this.panner);
|
|
480
|
+
this.panner.connect(this.expressionGainNode);
|
|
481
|
+
this.expressionGainNode.connect(this.outputGainNode);
|
|
482
|
+
|
|
483
|
+
// Connect through reverb
|
|
484
|
+
this.connect();
|
|
485
|
+
|
|
486
|
+
// Start playback
|
|
487
|
+
const startTime = (instrument.start || 0) / instrument.sampleRate;
|
|
488
|
+
this.bufferSource.start(0, startTime);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
connect() {
|
|
492
|
+
if (this.instrument.reverb) {
|
|
493
|
+
this.instrument.reverb.connect(this.outputGainNode).connect(this.destination);
|
|
494
|
+
} else {
|
|
495
|
+
this.outputGainNode.connect(this.destination);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
disconnect() {
|
|
500
|
+
try {
|
|
501
|
+
this.outputGainNode.disconnect();
|
|
502
|
+
} catch (e) {}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
noteOff() {
|
|
506
|
+
this.noteOffState = true;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
isNoteOff() {
|
|
510
|
+
return this.noteOffState;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
release() {
|
|
514
|
+
const now = this.ctx.currentTime;
|
|
515
|
+
const instrument = this.instrument;
|
|
516
|
+
const releaseTime = (instrument.volRelease || 0.3);
|
|
517
|
+
|
|
518
|
+
const outputGain = this.outputGainNode.gain;
|
|
519
|
+
outputGain.cancelScheduledValues(now);
|
|
520
|
+
outputGain.setValueAtTime(outputGain.value, now);
|
|
521
|
+
outputGain.linearRampToValueAtTime(0, now + releaseTime);
|
|
522
|
+
|
|
523
|
+
if (instrument.sampleModes === 1 || instrument.sampleModes === 3) {
|
|
524
|
+
this.bufferSource.stop(now + releaseTime);
|
|
525
|
+
} else {
|
|
526
|
+
this.bufferSource.loop = false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
updatePitchBend(pitchBend) {
|
|
531
|
+
const semitones = (pitchBend / (pitchBend < 0 ? 8192 : 8191)) * this.instrument.pitchBendSensitivity;
|
|
532
|
+
const newRate = this.instrument.basePlaybackRate * Math.pow(2, semitones / 12);
|
|
533
|
+
this.bufferSource.playbackRate.setValueAtTime(newRate, this.ctx.currentTime);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
amountToFreq(val) {
|
|
537
|
+
return Math.pow(2, ((val || 0) - 6900) / 1200) * 440;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Export to window for global access (matching original library)
|
|
542
|
+
window.ActionSoundFont = ActionSoundFont;
|
|
543
|
+
window.SoundFont = ActionSoundFont; // Also expose as SoundFont for drop-in replacement
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* sf2synth.js
|
|
4
|
+
* SoundFont Synthesizer for WebMidiLink
|
|
5
|
+
* https://github.com/logue/sf2synth.js
|
|
6
|
+
*
|
|
7
|
+
* The MIT License
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2013 imaya / GREE Inc.
|
|
10
|
+
* 2013-2019 Logue
|
|
11
|
+
*
|
|
12
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
* in the Software without restriction, including without limitation the rights
|
|
15
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
* furnished to do so, subject to the following conditions:
|
|
18
|
+
*
|
|
19
|
+
* The above copyright notice and this permission notice shall be included in
|
|
20
|
+
* all copies or substantial portions of the Software.
|
|
21
|
+
*
|
|
22
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
28
|
+
* THE SOFTWARE.
|
|
29
|
+
*/
|