littlejsengine 1.18.26 → 1.18.28
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/dist/littlejs.d.ts +61 -2
- package/dist/littlejs.esm.js +175 -45
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +169 -45
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +169 -45
- package/package.json +2 -1
- package/plugins/threejs.js +3 -0
- package/src/engine.js +1 -1
- package/src/engineAudio.js +103 -39
- package/src/engineDraw.js +15 -0
- package/src/engineExport.js +6 -0
- package/src/engineInput.js +34 -5
- package/src/engineSettings.js +13 -0
package/dist/littlejs.d.ts
CHANGED
|
@@ -514,6 +514,13 @@ declare module "littlejsengine" {
|
|
|
514
514
|
* @default
|
|
515
515
|
* @memberof Settings */
|
|
516
516
|
export let gamepadDirectionEmulateStick: boolean;
|
|
517
|
+
/** If true, axes that do not rest near center are ignored on gamepads without
|
|
518
|
+
* standard mapping. Steering wheels and flight sticks report pedal and throttle
|
|
519
|
+
* axes that rest at full deflection, which otherwise reads as a stick held down.
|
|
520
|
+
* @type {boolean}
|
|
521
|
+
* @default
|
|
522
|
+
* @memberof Settings */
|
|
523
|
+
export let gamepadAxisFilterEnable: boolean;
|
|
517
524
|
/** If true the WASD keys are also routed to the direction keys (for better accessibility)
|
|
518
525
|
* @type {boolean}
|
|
519
526
|
* @default
|
|
@@ -780,6 +787,10 @@ declare module "littlejsengine" {
|
|
|
780
787
|
* @param {boolean} enable
|
|
781
788
|
* @memberof Settings */
|
|
782
789
|
export function setGamepadDirectionEmulateStick(enable: boolean): void;
|
|
790
|
+
/** Set if axes that do not rest near center are ignored on non-standard gamepads
|
|
791
|
+
* @param {boolean} enable
|
|
792
|
+
* @memberof Settings */
|
|
793
|
+
export function setGamepadAxisFilterEnable(enable: boolean): void;
|
|
783
794
|
/** Set if true the WASD keys are also routed to the direction keys
|
|
784
795
|
* @param {boolean} enable
|
|
785
796
|
* @memberof Settings */
|
|
@@ -1817,6 +1828,11 @@ declare module "littlejsengine" {
|
|
|
1817
1828
|
* @type {OffscreenCanvasRenderingContext2D}
|
|
1818
1829
|
* @memberof Draw */
|
|
1819
1830
|
export let workReadContext: OffscreenCanvasRenderingContext2D;
|
|
1831
|
+
/** Extra canvas to composite behind the engine canvases when combining canvases
|
|
1832
|
+
* Set by plugins that render to their own canvas below the LittleJS canvases
|
|
1833
|
+
* @type {HTMLCanvasElement}
|
|
1834
|
+
* @memberof Draw */
|
|
1835
|
+
export let backgroundCanvas: HTMLCanvasElement;
|
|
1820
1836
|
/** The size of the main canvas (and other secondary canvases)
|
|
1821
1837
|
* @type {Vector2}
|
|
1822
1838
|
* @memberof Draw */
|
|
@@ -2050,6 +2066,12 @@ declare module "littlejsengine" {
|
|
|
2050
2066
|
* @param {boolean} [additive]
|
|
2051
2067
|
* @memberof Draw */
|
|
2052
2068
|
export function setAdditiveBlendMode(additive?: boolean): void;
|
|
2069
|
+
/** Set an extra canvas to composite behind the engine canvases when combining
|
|
2070
|
+
* Plugins that insert their own canvas below the LittleJS canvases should set
|
|
2071
|
+
* this so it appears in screenshots and video capture
|
|
2072
|
+
* @param {HTMLCanvasElement} [canvas]
|
|
2073
|
+
* @memberof Draw */
|
|
2074
|
+
export function setBackgroundCanvas(canvas?: HTMLCanvasElement): void;
|
|
2053
2075
|
/** Combines LittleJS canvases onto the main canvas
|
|
2054
2076
|
* This is necessary for things like screenshots and video
|
|
2055
2077
|
* @memberof Draw */
|
|
@@ -2565,11 +2587,29 @@ declare module "littlejsengine" {
|
|
|
2565
2587
|
randomness: any;
|
|
2566
2588
|
/** @property {number} - Sample rate for this sound */
|
|
2567
2589
|
sampleRate: number;
|
|
2568
|
-
/** @property {number} -
|
|
2590
|
+
/** @property {number} - How many samples per channel this sound has */
|
|
2591
|
+
sampleLength: number;
|
|
2592
|
+
/** @property {AudioBuffer} - Decoded audio shared by every play of this sound
|
|
2593
|
+
* @type {AudioBuffer} */
|
|
2594
|
+
sampleBuffer: AudioBuffer;
|
|
2595
|
+
/** @private @type {Array<Array<number>|Float32Array>} */
|
|
2596
|
+
private _sampleChannels;
|
|
2597
|
+
/** @property {number} - Percentage of this sound currently loaded, sounds
|
|
2598
|
+
* fetched from a url stay at 0 until decoding completes */
|
|
2569
2599
|
loadedPercent: number;
|
|
2570
2600
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
2571
2601
|
onloadCallback: (sound: Sound) => Sound;
|
|
2572
|
-
sampleChannels
|
|
2602
|
+
/** @param {Array<Array<number>|Float32Array>} sampleChannels */
|
|
2603
|
+
set sampleChannels(arg: (number[] | Float32Array)[]);
|
|
2604
|
+
/** Sample data for each channel
|
|
2605
|
+
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
2606
|
+
* the arrays from it and caches them. The copies are safe to hold onto,
|
|
2607
|
+
* playing a sound detaches the buffer's own channel arrays.
|
|
2608
|
+
* @type {Array<Array<number>|Float32Array>} */
|
|
2609
|
+
get sampleChannels(): (number[] | Float32Array)[];
|
|
2610
|
+
/** Move this sound's samples into an audio buffer that every play can share
|
|
2611
|
+
* Does nothing if there is already a buffer or no samples to build one from */
|
|
2612
|
+
buildSampleBuffer(): void;
|
|
2573
2613
|
/** Play the sound
|
|
2574
2614
|
* Sounds may not play until a user interaction occurs
|
|
2575
2615
|
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
@@ -2722,6 +2762,25 @@ declare module "littlejsengine" {
|
|
|
2722
2762
|
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
2723
2763
|
* @memberof Audio */
|
|
2724
2764
|
export function playSamples(sampleChannels: any[], volume?: number, rate?: number, pan?: number, loop?: boolean, sampleRate?: number, gainNode?: GainNode, offset?: number, onended?: AudioEndedCallback): AudioBufferSourceNode;
|
|
2765
|
+
/** Play an audio buffer with given settings
|
|
2766
|
+
* The buffer can be shared by any number of sounds playing at once
|
|
2767
|
+
* @param {AudioBuffer} buffer - The audio buffer to play
|
|
2768
|
+
* @param {number} [volume] - How much to scale volume by
|
|
2769
|
+
* @param {number} [rate] - The playback rate to use
|
|
2770
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
2771
|
+
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
2772
|
+
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
2773
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
2774
|
+
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
2775
|
+
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
2776
|
+
* @memberof Audio */
|
|
2777
|
+
export function playAudioBuffer(buffer: AudioBuffer, volume?: number, rate?: number, pan?: number, loop?: boolean, gainNode?: GainNode, offset?: number, onended?: AudioEndedCallback): AudioBufferSourceNode;
|
|
2778
|
+
/** Copy arrays of samples into a new audio buffer
|
|
2779
|
+
* @param {Array} sampleChannels - Array of arrays of samples (for stereo playback)
|
|
2780
|
+
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
2781
|
+
* @return {AudioBuffer} - The audio buffer holding the samples
|
|
2782
|
+
* @memberof Audio */
|
|
2783
|
+
export function createAudioBuffer(sampleChannels: any[], sampleRate?: number): AudioBuffer;
|
|
2725
2784
|
/** Generate and play a ZzFX sound
|
|
2726
2785
|
*
|
|
2727
2786
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
package/dist/littlejs.esm.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.18.
|
|
38
|
+
const engineVersion = '1.18.28';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -3037,6 +3037,14 @@ let gamepadsEnable = true;
|
|
|
3037
3037
|
* @memberof Settings */
|
|
3038
3038
|
let gamepadDirectionEmulateStick = true;
|
|
3039
3039
|
|
|
3040
|
+
/** If true, axes that do not rest near center are ignored on gamepads without
|
|
3041
|
+
* standard mapping. Steering wheels and flight sticks report pedal and throttle
|
|
3042
|
+
* axes that rest at full deflection, which otherwise reads as a stick held down.
|
|
3043
|
+
* @type {boolean}
|
|
3044
|
+
* @default
|
|
3045
|
+
* @memberof Settings */
|
|
3046
|
+
let gamepadAxisFilterEnable = true;
|
|
3047
|
+
|
|
3040
3048
|
/** If true the WASD keys are also routed to the direction keys (for better accessibility)
|
|
3041
3049
|
* @type {boolean}
|
|
3042
3050
|
* @default
|
|
@@ -3375,6 +3383,11 @@ function setGamepadsEnable(enable) { gamepadsEnable = enable; }
|
|
|
3375
3383
|
* @memberof Settings */
|
|
3376
3384
|
function setGamepadDirectionEmulateStick(enable) { gamepadDirectionEmulateStick = enable; }
|
|
3377
3385
|
|
|
3386
|
+
/** Set if axes that do not rest near center are ignored on non-standard gamepads
|
|
3387
|
+
* @param {boolean} enable
|
|
3388
|
+
* @memberof Settings */
|
|
3389
|
+
function setGamepadAxisFilterEnable(enable) { gamepadAxisFilterEnable = enable; }
|
|
3390
|
+
|
|
3378
3391
|
/** Set if true the WASD keys are also routed to the direction keys
|
|
3379
3392
|
* @param {boolean} enable
|
|
3380
3393
|
* @memberof Settings */
|
|
@@ -4119,6 +4132,12 @@ let workReadCanvas;
|
|
|
4119
4132
|
* @memberof Draw */
|
|
4120
4133
|
let workReadContext;
|
|
4121
4134
|
|
|
4135
|
+
/** Extra canvas to composite behind the engine canvases when combining canvases
|
|
4136
|
+
* Set by plugins that render to their own canvas below the LittleJS canvases
|
|
4137
|
+
* @type {HTMLCanvasElement}
|
|
4138
|
+
* @memberof Draw */
|
|
4139
|
+
let backgroundCanvas;
|
|
4140
|
+
|
|
4122
4141
|
/** The size of the main canvas (and other secondary canvases)
|
|
4123
4142
|
* @type {Vector2}
|
|
4124
4143
|
* @memberof Draw */
|
|
@@ -5270,6 +5289,13 @@ function setAdditiveBlendMode(additive=true)
|
|
|
5270
5289
|
drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
|
|
5271
5290
|
}
|
|
5272
5291
|
|
|
5292
|
+
/** Set an extra canvas to composite behind the engine canvases when combining
|
|
5293
|
+
* Plugins that insert their own canvas below the LittleJS canvases should set
|
|
5294
|
+
* this so it appears in screenshots and video capture
|
|
5295
|
+
* @param {HTMLCanvasElement} [canvas]
|
|
5296
|
+
* @memberof Draw */
|
|
5297
|
+
function setBackgroundCanvas(canvas) { backgroundCanvas = canvas; }
|
|
5298
|
+
|
|
5273
5299
|
/** Combines LittleJS canvases onto the main canvas
|
|
5274
5300
|
* This is necessary for things like screenshots and video
|
|
5275
5301
|
* @memberof Draw */
|
|
@@ -5282,6 +5308,8 @@ function combineCanvases()
|
|
|
5282
5308
|
// leaving workContext.fillStyle transparent can't silently no-op this
|
|
5283
5309
|
workContext.fillStyle = '#000';
|
|
5284
5310
|
workContext.fillRect(0,0,w,h);
|
|
5311
|
+
if (backgroundCanvas)
|
|
5312
|
+
workContext.drawImage(backgroundCanvas, 0, 0, w, h);
|
|
5285
5313
|
glCopyToContext(workContext);
|
|
5286
5314
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
5287
5315
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -5680,6 +5708,7 @@ function inputClear()
|
|
|
5680
5708
|
touchGamepadStickPointerId.length = 0; // release floating sticks so they re-anchor
|
|
5681
5709
|
gamepadStickData.length = 0;
|
|
5682
5710
|
gamepadDpadData.length = 0;
|
|
5711
|
+
gamepadAxisCentered.length = 0;
|
|
5683
5712
|
}
|
|
5684
5713
|
|
|
5685
5714
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -5917,6 +5946,11 @@ const inputData = [[]];
|
|
|
5917
5946
|
|
|
5918
5947
|
// gamepad internal variables
|
|
5919
5948
|
const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
5949
|
+
// per gamepad, how many consecutive frames each axis has rested inside the
|
|
5950
|
+
// dead zone, used to tell stick axes from axes that rest at full deflection
|
|
5951
|
+
const gamepadAxisCentered = [];
|
|
5952
|
+
// how long an axis must rest inside the dead zone before it counts as a stick
|
|
5953
|
+
const gamepadAxisCenteredFrames = 15;
|
|
5920
5954
|
|
|
5921
5955
|
// touch gamepad internal variables
|
|
5922
5956
|
const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
|
|
@@ -6165,7 +6199,7 @@ function inputUpdate()
|
|
|
6165
6199
|
// gamepad: any button held or stick moved
|
|
6166
6200
|
let gamepadActive = false;
|
|
6167
6201
|
for (let s = gamepadStickCount(); s-- && !gamepadActive;)
|
|
6168
|
-
gamepadActive = gamepadStick(s).lengthSquared() > .
|
|
6202
|
+
gamepadActive = gamepadStick(s).lengthSquared() > .2;
|
|
6169
6203
|
for (let b = 17; b-- && !gamepadActive;)
|
|
6170
6204
|
gamepadActive = gamepadIsDown(b);
|
|
6171
6205
|
|
|
@@ -6193,12 +6227,12 @@ function inputUpdate()
|
|
|
6193
6227
|
// gamepads are updated by engine every frame automatically
|
|
6194
6228
|
function gamepadsUpdate()
|
|
6195
6229
|
{
|
|
6230
|
+
const deadZoneMin=.3, deadZoneMax=.8;
|
|
6196
6231
|
const applyDeadZones = (v)=>
|
|
6197
6232
|
{
|
|
6198
|
-
const min=.3, max=.8;
|
|
6199
6233
|
const deadZone = (v)=>
|
|
6200
|
-
v >
|
|
6201
|
-
v < -
|
|
6234
|
+
v > deadZoneMin ? percent(v, deadZoneMin, deadZoneMax) :
|
|
6235
|
+
v < -deadZoneMin ? -percent(-v, deadZoneMin, deadZoneMax) : 0;
|
|
6202
6236
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
6203
6237
|
};
|
|
6204
6238
|
|
|
@@ -6282,6 +6316,7 @@ function inputUpdate()
|
|
|
6282
6316
|
gamepadStickData[i] = undefined;
|
|
6283
6317
|
gamepadDpadData[i] = undefined;
|
|
6284
6318
|
gamepadHadInput[i] = undefined;
|
|
6319
|
+
gamepadAxisCentered[i] = undefined;
|
|
6285
6320
|
continue;
|
|
6286
6321
|
}
|
|
6287
6322
|
|
|
@@ -6290,8 +6325,30 @@ function inputUpdate()
|
|
|
6290
6325
|
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
6291
6326
|
|
|
6292
6327
|
// read analog sticks
|
|
6328
|
+
// gamepads without standard mapping (steering wheels, flight sticks)
|
|
6329
|
+
// can report axes that rest at full deflection instead of center,
|
|
6330
|
+
// which would otherwise read as a stick held down forever, so only
|
|
6331
|
+
// trust an axis once it has rested inside the dead zone for a moment
|
|
6332
|
+
const isStandard = gamepad.mapping === 'standard';
|
|
6333
|
+
const centered = gamepadAxisCentered[i] ?? (gamepadAxisCentered[i] = []);
|
|
6334
|
+
const readAxis = (j)=>
|
|
6335
|
+
{
|
|
6336
|
+
const v = gamepad.axes[j];
|
|
6337
|
+
if (isStandard && j < 4)
|
|
6338
|
+
return v; // spec guarantees axes 0-3 are the two sticks
|
|
6339
|
+
if (!gamepadAxisFilterEnable)
|
|
6340
|
+
return v;
|
|
6341
|
+
|
|
6342
|
+
// once an axis has proven it rests at center it stays trusted,
|
|
6343
|
+
// otherwise moving it would immediately disqualify it again
|
|
6344
|
+
const frames = centered[j] | 0;
|
|
6345
|
+
if (frames > gamepadAxisCenteredFrames)
|
|
6346
|
+
return v;
|
|
6347
|
+
centered[j] = abs(v) < deadZoneMin ? frames + 1 : 0;
|
|
6348
|
+
return 0;
|
|
6349
|
+
};
|
|
6293
6350
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
6294
|
-
sticks[j>>1] = applyDeadZones(vec2(
|
|
6351
|
+
sticks[j>>1] = applyDeadZones(vec2(readAxis(j), readAxis(j+1)));
|
|
6295
6352
|
|
|
6296
6353
|
// read buttons
|
|
6297
6354
|
let hadInput = false;
|
|
@@ -6917,7 +6974,15 @@ class Sound
|
|
|
6917
6974
|
this.randomness = randomness ?? 0;
|
|
6918
6975
|
/** @property {number} - Sample rate for this sound */
|
|
6919
6976
|
this.sampleRate = audioDefaultSampleRate;
|
|
6920
|
-
/** @property {number} -
|
|
6977
|
+
/** @property {number} - How many samples per channel this sound has */
|
|
6978
|
+
this.sampleLength = 0;
|
|
6979
|
+
/** @property {AudioBuffer} - Decoded audio shared by every play of this sound
|
|
6980
|
+
* @type {AudioBuffer} */
|
|
6981
|
+
this.sampleBuffer = undefined;
|
|
6982
|
+
/** @private @type {Array<Array<number>|Float32Array>} */
|
|
6983
|
+
this._sampleChannels = undefined;
|
|
6984
|
+
/** @property {number} - Percentage of this sound currently loaded, sounds
|
|
6985
|
+
* fetched from a url stay at 0 until decoding completes */
|
|
6921
6986
|
this.loadedPercent = 0;
|
|
6922
6987
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
6923
6988
|
this.onloadCallback = onloadCallback;
|
|
@@ -6933,8 +6998,10 @@ class Sound
|
|
|
6933
6998
|
this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
|
|
6934
6999
|
zzfxSound[randomnessIndex] = 0;
|
|
6935
7000
|
|
|
6936
|
-
// generate the zzfx samples
|
|
7001
|
+
// generate the zzfx samples, then hand them to an audio buffer so
|
|
7002
|
+
// the plain arrays can be released and every play shares the buffer
|
|
6937
7003
|
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
7004
|
+
this.buildSampleBuffer();
|
|
6938
7005
|
this.loadedPercent = 1;
|
|
6939
7006
|
onloadCallback?.(this);
|
|
6940
7007
|
}
|
|
@@ -6946,6 +7013,45 @@ class Sound
|
|
|
6946
7013
|
}
|
|
6947
7014
|
}
|
|
6948
7015
|
|
|
7016
|
+
/** Sample data for each channel
|
|
7017
|
+
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
7018
|
+
* the arrays from it and caches them. The copies are safe to hold onto,
|
|
7019
|
+
* playing a sound detaches the buffer's own channel arrays.
|
|
7020
|
+
* @type {Array<Array<number>|Float32Array>} */
|
|
7021
|
+
get sampleChannels()
|
|
7022
|
+
{
|
|
7023
|
+
const buffer = this.sampleBuffer;
|
|
7024
|
+
if (!this._sampleChannels && buffer)
|
|
7025
|
+
{
|
|
7026
|
+
const channels = [];
|
|
7027
|
+
for (let i = 0; i < buffer.numberOfChannels; i++)
|
|
7028
|
+
channels.push(buffer.getChannelData(i).slice());
|
|
7029
|
+
this._sampleChannels = channels;
|
|
7030
|
+
}
|
|
7031
|
+
return this._sampleChannels;
|
|
7032
|
+
}
|
|
7033
|
+
|
|
7034
|
+
/** @param {Array<Array<number>|Float32Array>} sampleChannels */
|
|
7035
|
+
set sampleChannels(sampleChannels)
|
|
7036
|
+
{
|
|
7037
|
+
// new samples invalidate the buffer built from the old ones
|
|
7038
|
+
this._sampleChannels = sampleChannels;
|
|
7039
|
+
this.sampleBuffer = undefined;
|
|
7040
|
+
this.sampleLength = sampleChannels?.[0]?.length || 0;
|
|
7041
|
+
}
|
|
7042
|
+
|
|
7043
|
+
/** Move this sound's samples into an audio buffer that every play can share
|
|
7044
|
+
* Does nothing if there is already a buffer or no samples to build one from */
|
|
7045
|
+
buildSampleBuffer()
|
|
7046
|
+
{
|
|
7047
|
+
if (this.sampleBuffer || !this._sampleChannels || headlessMode) return;
|
|
7048
|
+
|
|
7049
|
+
this.sampleBuffer = createAudioBuffer(this._sampleChannels, this.sampleRate);
|
|
7050
|
+
|
|
7051
|
+
// the buffer owns the samples now, release the arrays we built it from
|
|
7052
|
+
this._sampleChannels = undefined;
|
|
7053
|
+
}
|
|
7054
|
+
|
|
6949
7055
|
/** Play the sound
|
|
6950
7056
|
* Sounds may not play until a user interaction occurs
|
|
6951
7057
|
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
@@ -6964,7 +7070,7 @@ class Sound
|
|
|
6964
7070
|
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
6965
7071
|
|
|
6966
7072
|
if (!soundEnable || headlessMode) return;
|
|
6967
|
-
if (!this.
|
|
7073
|
+
if (!this.sampleBuffer && !this._sampleChannels) return;
|
|
6968
7074
|
|
|
6969
7075
|
let pan;
|
|
6970
7076
|
if (pos)
|
|
@@ -7031,7 +7137,7 @@ class Sound
|
|
|
7031
7137
|
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
7032
7138
|
*/
|
|
7033
7139
|
getDuration()
|
|
7034
|
-
{ return this.
|
|
7140
|
+
{ return this.sampleLength / this.sampleRate || 0; }
|
|
7035
7141
|
|
|
7036
7142
|
/** Check if sound is loaded, for sounds fetched from a url
|
|
7037
7143
|
* @return {boolean} - True if sound is loaded and ready to play
|
|
@@ -7049,36 +7155,11 @@ class Sound
|
|
|
7049
7155
|
const arrayBuffer = await response.arrayBuffer();
|
|
7050
7156
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
7051
7157
|
|
|
7052
|
-
//
|
|
7053
|
-
|
|
7054
|
-
const samplesPerFrame = 1e5;
|
|
7055
|
-
const sampleChannels = [];
|
|
7056
|
-
for (let channel = 0; channel < channelCount; channel++)
|
|
7057
|
-
{
|
|
7058
|
-
const channelData = audioBuffer.getChannelData(channel);
|
|
7059
|
-
const channelLength = channelData.length;
|
|
7060
|
-
sampleChannels[channel] = new Array(channelLength);
|
|
7061
|
-
let sampleIndex = 0;
|
|
7062
|
-
while (sampleIndex < channelLength)
|
|
7063
|
-
{
|
|
7064
|
-
// yield to next frame
|
|
7065
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
7066
|
-
|
|
7067
|
-
// copy chunk of samples
|
|
7068
|
-
const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
|
|
7069
|
-
for (; sampleIndex < endIndex; sampleIndex++)
|
|
7070
|
-
sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
|
|
7071
|
-
|
|
7072
|
-
// update loaded percent
|
|
7073
|
-
const samplesTotal = channelCount * channelLength;
|
|
7074
|
-
const samplesProcessed = channel * channelLength + sampleIndex;
|
|
7075
|
-
this.loadedPercent = samplesProcessed / samplesTotal;
|
|
7076
|
-
}
|
|
7077
|
-
}
|
|
7078
|
-
|
|
7079
|
-
// setup the sound to be played
|
|
7158
|
+
// keep the decoded buffer as is, it is exactly what playback needs and
|
|
7159
|
+
// every play shares it, no channel data is read or copied
|
|
7080
7160
|
this.sampleRate = audioBuffer.sampleRate;
|
|
7081
|
-
this.
|
|
7161
|
+
this.sampleLength = audioBuffer.length;
|
|
7162
|
+
this.sampleBuffer = audioBuffer;
|
|
7082
7163
|
this.loadedPercent = 1;
|
|
7083
7164
|
this.onloadCallback?.(this);
|
|
7084
7165
|
}
|
|
@@ -7154,7 +7235,12 @@ class SoundInstance
|
|
|
7154
7235
|
if (this.isPlaying())
|
|
7155
7236
|
this.stop();
|
|
7156
7237
|
this.gainNode = audioContext.createGain();
|
|
7157
|
-
|
|
7238
|
+
|
|
7239
|
+
// build the shared buffer if it was not made at load time, then play it
|
|
7240
|
+
this.sound.buildSampleBuffer();
|
|
7241
|
+
this.source = this.sound.sampleBuffer ?
|
|
7242
|
+
playAudioBuffer(this.sound.sampleBuffer, this.volume, this.rate, this.pan, this.loop, this.gainNode, offset, this.onendedCallback) :
|
|
7243
|
+
playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
|
|
7158
7244
|
if (this.source)
|
|
7159
7245
|
{
|
|
7160
7246
|
this.startTime = audioContext.currentTime - offset;
|
|
@@ -7329,19 +7415,54 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
7329
7415
|
|
|
7330
7416
|
if (!audioIsRunning())
|
|
7331
7417
|
{
|
|
7332
|
-
// fix stalled audio,
|
|
7418
|
+
// fix stalled audio, don't build a buffer that can't be played
|
|
7333
7419
|
audioContext.resume();
|
|
7334
7420
|
return;
|
|
7335
7421
|
}
|
|
7336
7422
|
|
|
7337
|
-
|
|
7423
|
+
const buffer = createAudioBuffer(sampleChannels, sampleRate);
|
|
7424
|
+
return playAudioBuffer(buffer, volume, rate, pan, loop, gainNode, offset, onended);
|
|
7425
|
+
}
|
|
7426
|
+
|
|
7427
|
+
/** Copy arrays of samples into a new audio buffer
|
|
7428
|
+
* @param {Array} sampleChannels - Array of arrays of samples (for stereo playback)
|
|
7429
|
+
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
7430
|
+
* @return {AudioBuffer} - The audio buffer holding the samples
|
|
7431
|
+
* @memberof Audio */
|
|
7432
|
+
function createAudioBuffer(sampleChannels, sampleRate=audioDefaultSampleRate)
|
|
7433
|
+
{
|
|
7338
7434
|
const channelCount = sampleChannels.length;
|
|
7339
7435
|
const sampleLength = sampleChannels[0].length;
|
|
7340
7436
|
const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
|
|
7341
|
-
const source = audioContext.createBufferSource();
|
|
7342
|
-
|
|
7343
|
-
// copy samples to buffer and setup source
|
|
7344
7437
|
sampleChannels.forEach((c,i)=> buffer.getChannelData(i).set(c));
|
|
7438
|
+
return buffer;
|
|
7439
|
+
}
|
|
7440
|
+
|
|
7441
|
+
/** Play an audio buffer with given settings
|
|
7442
|
+
* The buffer can be shared by any number of sounds playing at once
|
|
7443
|
+
* @param {AudioBuffer} buffer - The audio buffer to play
|
|
7444
|
+
* @param {number} [volume] - How much to scale volume by
|
|
7445
|
+
* @param {number} [rate] - The playback rate to use
|
|
7446
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
7447
|
+
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
7448
|
+
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
7449
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
7450
|
+
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
7451
|
+
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
7452
|
+
* @memberof Audio */
|
|
7453
|
+
function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode, offset=0, onended)
|
|
7454
|
+
{
|
|
7455
|
+
if (!soundEnable || headlessMode) return;
|
|
7456
|
+
|
|
7457
|
+
if (!audioIsRunning())
|
|
7458
|
+
{
|
|
7459
|
+
// fix stalled audio, this sound won't be able to play
|
|
7460
|
+
audioContext.resume();
|
|
7461
|
+
return;
|
|
7462
|
+
}
|
|
7463
|
+
|
|
7464
|
+
// setup source, many sources can share one buffer
|
|
7465
|
+
const source = audioContext.createBufferSource();
|
|
7345
7466
|
source.buffer = buffer;
|
|
7346
7467
|
source.playbackRate.value = rate;
|
|
7347
7468
|
source.loop = loop;
|
|
@@ -16808,6 +16929,9 @@ class ThreeJSPlugin
|
|
|
16808
16929
|
rootElement.insertBefore(threeCanvas, rootElement.firstChild);
|
|
16809
16930
|
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
16810
16931
|
|
|
16932
|
+
// composite the 3D canvas into screenshots and video capture
|
|
16933
|
+
setBackgroundCanvas(threeCanvas);
|
|
16934
|
+
|
|
16811
16935
|
// render automatically each frame after the engine renders
|
|
16812
16936
|
engineAddPlugin(undefined, ()=> this.render());
|
|
16813
16937
|
}
|
|
@@ -16992,6 +17116,7 @@ export
|
|
|
16992
17116
|
glCircleSides,
|
|
16993
17117
|
gamepadsEnable,
|
|
16994
17118
|
gamepadDirectionEmulateStick,
|
|
17119
|
+
gamepadAxisFilterEnable,
|
|
16995
17120
|
inputWASDEmulateDirection,
|
|
16996
17121
|
touchInputEnable,
|
|
16997
17122
|
touchGamepadEnable,
|
|
@@ -17048,6 +17173,7 @@ export
|
|
|
17048
17173
|
setTouchInputEnable,
|
|
17049
17174
|
setGamepadsEnable,
|
|
17050
17175
|
setGamepadDirectionEmulateStick,
|
|
17176
|
+
setGamepadAxisFilterEnable,
|
|
17051
17177
|
setInputWASDEmulateDirection,
|
|
17052
17178
|
setTouchGamepadEnable,
|
|
17053
17179
|
setTouchGamepadPassthrough,
|
|
@@ -17163,6 +17289,7 @@ export
|
|
|
17163
17289
|
workContext,
|
|
17164
17290
|
workReadCanvas,
|
|
17165
17291
|
workReadContext,
|
|
17292
|
+
backgroundCanvas,
|
|
17166
17293
|
mainCanvasSize,
|
|
17167
17294
|
textureInfos,
|
|
17168
17295
|
drawCount,
|
|
@@ -17188,6 +17315,7 @@ export
|
|
|
17188
17315
|
drawText,
|
|
17189
17316
|
drawTextScreen,
|
|
17190
17317
|
setAdditiveBlendMode,
|
|
17318
|
+
setBackgroundCanvas,
|
|
17191
17319
|
combineCanvases,
|
|
17192
17320
|
engineImageFont,
|
|
17193
17321
|
ImageFont,
|
|
@@ -17275,6 +17403,8 @@ export
|
|
|
17275
17403
|
speakStop,
|
|
17276
17404
|
getNoteFrequency,
|
|
17277
17405
|
playSamples,
|
|
17406
|
+
playAudioBuffer,
|
|
17407
|
+
createAudioBuffer,
|
|
17278
17408
|
zzfx,
|
|
17279
17409
|
zzfxG,
|
|
17280
17410
|
|