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.
Files changed (93) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +348 -0
  3. package/actionengine/3rdparty/goblin/goblin.js +9609 -0
  4. package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
  5. package/actionengine/camera/actioncamera.js +90 -0
  6. package/actionengine/camera/cameracollisionhandler.js +69 -0
  7. package/actionengine/character/actioncharacter.js +360 -0
  8. package/actionengine/character/actioncharacter3D.js +61 -0
  9. package/actionengine/core/app.js +430 -0
  10. package/actionengine/debug/basedebugpanel.js +858 -0
  11. package/actionengine/display/canvasmanager.js +75 -0
  12. package/actionengine/display/gl/programmanager.js +570 -0
  13. package/actionengine/display/gl/shaders/lineshader.js +118 -0
  14. package/actionengine/display/gl/shaders/objectshader.js +1756 -0
  15. package/actionengine/display/gl/shaders/particleshader.js +43 -0
  16. package/actionengine/display/gl/shaders/shadowshader.js +319 -0
  17. package/actionengine/display/gl/shaders/spriteshader.js +100 -0
  18. package/actionengine/display/gl/shaders/watershader.js +67 -0
  19. package/actionengine/display/graphics/actionmodel3D.js +191 -0
  20. package/actionengine/display/graphics/actionsprite3D.js +230 -0
  21. package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
  22. package/actionengine/display/graphics/lighting/actionlight.js +211 -0
  23. package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
  24. package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
  25. package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
  26. package/actionengine/display/graphics/renderableobject.js +44 -0
  27. package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
  28. package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
  29. package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
  30. package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
  31. package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
  32. package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
  33. package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
  34. package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
  35. package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
  36. package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
  37. package/actionengine/display/graphics/texture/texturemanager.js +242 -0
  38. package/actionengine/display/graphics/texture/textureregistry.js +177 -0
  39. package/actionengine/input/actionscrollablearea.js +1405 -0
  40. package/actionengine/input/inputhandler.js +1647 -0
  41. package/actionengine/math/geometry/geometrybuilder.js +161 -0
  42. package/actionengine/math/geometry/glbexporter.js +364 -0
  43. package/actionengine/math/geometry/glbloader.js +722 -0
  44. package/actionengine/math/geometry/modelcodegenerator.js +97 -0
  45. package/actionengine/math/geometry/triangle.js +33 -0
  46. package/actionengine/math/geometry/triangleutils.js +34 -0
  47. package/actionengine/math/mathutils.js +25 -0
  48. package/actionengine/math/matrix4.js +785 -0
  49. package/actionengine/math/physics/actionphysics.js +108 -0
  50. package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
  51. package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
  52. package/actionengine/math/physics/actionraycast.js +129 -0
  53. package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
  54. package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
  55. package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
  56. package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
  57. package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
  58. package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
  59. package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
  60. package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
  61. package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
  62. package/actionengine/math/quaternion.js +61 -0
  63. package/actionengine/math/vector2.js +277 -0
  64. package/actionengine/math/vector3.js +318 -0
  65. package/actionengine/math/viewfrustum.js +136 -0
  66. package/actionengine/network/ACTIONNETREADME.md +810 -0
  67. package/actionengine/network/client/ActionNetManager.js +802 -0
  68. package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
  69. package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
  70. package/actionengine/network/client/SyncSystem.js +422 -0
  71. package/actionengine/network/p2p/ActionNetPeer.js +142 -0
  72. package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
  73. package/actionengine/network/p2p/DataConnection.js +282 -0
  74. package/actionengine/network/p2p/README.md +510 -0
  75. package/actionengine/network/p2p/example.html +502 -0
  76. package/actionengine/network/server/ActionNetServer.js +577 -0
  77. package/actionengine/network/server/ActionNetServerSSL.js +579 -0
  78. package/actionengine/network/server/ActionNetServerUtils.js +458 -0
  79. package/actionengine/network/server/SERVERREADME.md +314 -0
  80. package/actionengine/network/server/package-lock.json +35 -0
  81. package/actionengine/network/server/package.json +13 -0
  82. package/actionengine/network/server/start.bat +27 -0
  83. package/actionengine/network/server/start.sh +25 -0
  84. package/actionengine/network/server/startwss.bat +27 -0
  85. package/actionengine/sound/audiomanager.js +1589 -0
  86. package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
  87. package/actionengine/sound/soundfont/actionparser.js +718 -0
  88. package/actionengine/sound/soundfont/actionreverb.js +252 -0
  89. package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
  90. package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
  91. package/actionengine/sound/soundfont/soundfont.js +2 -0
  92. package/dist/action-engine.min.js +328 -0
  93. 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
+ */