littlejsengine 1.13.1 → 1.14.2
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/README.md +15 -11
- package/copyToGithub.bat +28 -0
- package/dist/littlejs.d.ts +430 -304
- package/dist/littlejs.esm.js +1745 -982
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1716 -965
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1673 -922
- package/examples/box2d/game.js +2 -2
- package/examples/box2d/gameObjects.js +2 -2
- package/examples/box2d/scenes.js +1 -1
- package/examples/breakout/game.js +30 -25
- package/examples/breakoutTutorial/README.md +44 -41
- package/examples/breakoutTutorial/game.js +2 -2
- package/examples/electron/build.bat +7 -0
- package/examples/electron/build.js +124 -0
- package/examples/electron/electron.js +35 -0
- package/examples/electron/game.js +140 -0
- package/examples/electron/index.html +13 -0
- package/examples/electron/package.json +12 -0
- package/examples/electron/tiles.png +0 -0
- package/examples/empty/game.js +1 -0
- package/examples/htmlMenu/game.js +1 -1
- package/examples/index.html +332 -160
- package/examples/module/game.js +5 -2
- package/examples/particles/index.html +12 -3
- package/examples/platformer/gameEffects.js +20 -19
- package/examples/platformer/gameLevel.js +6 -1
- package/examples/shorts/base.html +1 -1
- package/examples/shorts/blending.js +9 -5
- package/examples/shorts/box2d.js +1 -1
- package/examples/shorts/box2dCar.js +1 -1
- package/examples/shorts/clock.js +1 -1
- package/examples/shorts/colors.js +2 -2
- package/examples/shorts/flappyGame.js +2 -1
- package/examples/shorts/helloWorld.js +1 -1
- package/examples/shorts/maze.js +1 -1
- package/examples/shorts/music.js +106 -0
- package/examples/shorts/nineSlice.js +9 -9
- package/examples/shorts/parallax.js +71 -0
- package/examples/shorts/piano.js +9 -10
- package/examples/shorts/postProcess.js +25 -8
- package/examples/shorts/raycasting.js +2 -2
- package/examples/shorts/shapes.js +7 -15
- package/examples/shorts/slidingPuzzle.js +2 -2
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/starfield.js +5 -7
- package/examples/shorts/systemFont.js +1 -1
- package/examples/shorts/uiSystem.js +16 -8
- package/examples/starter/build.js +9 -8
- package/examples/starter/game.js +5 -2
- package/examples/starter/index.html +2 -2
- package/examples/stress/index.html +13 -8
- package/examples/style.css +19 -6
- package/examples/typescript/build.js +1 -1
- package/examples/typescript/game.js +2 -2
- package/examples/typescript/game.ts +5 -2
- package/examples/uiSystem/game.js +13 -12
- package/package.json +2 -2
- package/plugins/box2d.js +24 -97
- package/plugins/drawUtilities.js +29 -23
- package/plugins/newgrounds.js +6 -6
- package/plugins/pluginExport.js +1 -1
- package/plugins/postProcess.js +7 -0
- package/plugins/uiSystem.js +106 -82
- package/plugins/zzfxm.js +10 -10
- package/reference.md +90 -54
- package/src/engine.js +28 -23
- package/src/engineAudio.js +285 -110
- package/src/engineBuild.js +9 -9
- package/src/engineDebug.js +28 -28
- package/src/engineDraw.js +346 -202
- package/src/engineExport.js +28 -16
- package/src/engineInput.js +58 -68
- package/src/engineMedals.js +29 -29
- package/src/engineObject.js +28 -25
- package/src/engineParticles.js +67 -60
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +70 -16
- package/src/engineTileLayer.js +34 -34
- package/src/engineUtilities.js +69 -62
- package/src/engineWebGL.js +467 -65
- /package/examples/shorts/{playSound.js → sound.js} +0 -0
package/src/engineAudio.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
2
|
* LittleJS Audio System
|
|
3
3
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
4
4
|
* - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
|
|
@@ -21,10 +21,21 @@ let audioContext = new AudioContext;
|
|
|
21
21
|
* @memberof Audio */
|
|
22
22
|
let audioMasterGain;
|
|
23
23
|
|
|
24
|
+
/** Default sample rate used for sounds
|
|
25
|
+
* @default 44100
|
|
26
|
+
* @memberof Audio */
|
|
27
|
+
const audioDefaultSampleRate = 44100;
|
|
28
|
+
|
|
29
|
+
/** Check if the audio context is running and available for playback
|
|
30
|
+
* @return {boolean} - True if the audio context is running
|
|
31
|
+
* @memberof Audio */
|
|
32
|
+
function audioIsRunning()
|
|
33
|
+
{ return audioContext.state === 'running'; }
|
|
34
|
+
|
|
24
35
|
function audioInit()
|
|
25
36
|
{
|
|
26
37
|
if (!soundEnable || headlessMode) return;
|
|
27
|
-
|
|
38
|
+
|
|
28
39
|
audioMasterGain = audioContext.createGain();
|
|
29
40
|
audioMasterGain.connect(audioContext.destination);
|
|
30
41
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -32,14 +43,14 @@ function audioInit()
|
|
|
32
43
|
|
|
33
44
|
///////////////////////////////////////////////////////////////////////////////
|
|
34
45
|
|
|
35
|
-
/**
|
|
46
|
+
/**
|
|
36
47
|
* Sound Object - Stores a sound for later use and can be played positionally
|
|
37
|
-
*
|
|
48
|
+
*
|
|
38
49
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
39
50
|
* @example
|
|
40
51
|
* // create a sound
|
|
41
52
|
* const sound_example = new Sound([.5,.5]);
|
|
42
|
-
*
|
|
53
|
+
*
|
|
43
54
|
* // play the sound
|
|
44
55
|
* sound_example.play();
|
|
45
56
|
*/
|
|
@@ -56,33 +67,41 @@ class Sound
|
|
|
56
67
|
|
|
57
68
|
/** @property {number} - World space max range of sound */
|
|
58
69
|
this.range = range;
|
|
59
|
-
|
|
60
70
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
61
71
|
this.taper = taper;
|
|
62
|
-
|
|
63
72
|
/** @property {number} - How much to randomize frequency each time sound plays */
|
|
64
73
|
this.randomness = 0;
|
|
74
|
+
/** @property {number} - Sample rate for this sound */
|
|
75
|
+
this.sampleRate = audioDefaultSampleRate;
|
|
76
|
+
/** @property {number} - Percentage of this sound currently loaded */
|
|
77
|
+
this.loadedPercent = 0;
|
|
65
78
|
|
|
79
|
+
// generate zzfx sound now for fast playback
|
|
66
80
|
if (zzfxSound)
|
|
67
81
|
{
|
|
68
|
-
//
|
|
69
|
-
const defaultRandomness = .05;
|
|
70
|
-
this.randomness = zzfxSound[
|
|
71
|
-
|
|
82
|
+
// remove randomness so it can be applied on playback
|
|
83
|
+
const randomnessIndex = 1, defaultRandomness = .05;
|
|
84
|
+
this.randomness = zzfxSound[randomnessIndex] !== undefined ?
|
|
85
|
+
zzfxSound[randomnessIndex] : defaultRandomness;
|
|
86
|
+
zzfxSound[randomnessIndex] = 0;
|
|
87
|
+
|
|
88
|
+
// generate the zzfx samples
|
|
72
89
|
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
73
|
-
this.
|
|
90
|
+
this.loadedPercent = 1;
|
|
74
91
|
}
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
/** Play the sound
|
|
78
|
-
*
|
|
79
|
-
* @param {
|
|
80
|
-
* @param {number} [
|
|
81
|
-
* @param {number} [
|
|
82
|
-
* @param {
|
|
83
|
-
* @
|
|
95
|
+
* Sounds may not play until a user interaction occurs
|
|
96
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
97
|
+
* @param {number} [volume] - How much to scale volume by
|
|
98
|
+
* @param {number} [pitch] - How much to scale pitch by
|
|
99
|
+
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
100
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
101
|
+
* @param {boolean} [paused] - Should the sound start paused
|
|
102
|
+
* @return {SoundInstance} - The audio source node
|
|
84
103
|
*/
|
|
85
|
-
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
|
|
104
|
+
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
86
105
|
{
|
|
87
106
|
if (!soundEnable || headlessMode) return;
|
|
88
107
|
if (!this.sampleChannels) return;
|
|
@@ -105,75 +124,55 @@ class Sound
|
|
|
105
124
|
// get pan from screen space coords
|
|
106
125
|
pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
|
|
107
126
|
}
|
|
108
|
-
|
|
109
|
-
// play the sound
|
|
110
|
-
const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
111
|
-
this.gainNode = audioContext.createGain();
|
|
112
|
-
this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
|
|
113
|
-
return this.source;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Set the sound volume of the most recently played instance of this sound
|
|
117
|
-
* @param {number} [volume] - How much to scale volume by
|
|
118
|
-
*/
|
|
119
|
-
setVolume(volume=1)
|
|
120
|
-
{
|
|
121
|
-
if (this.gainNode)
|
|
122
|
-
this.gainNode.gain.value = volume;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Stop the last instance of this sound that was played
|
|
126
|
-
* @param {number} [fadeTime] - How long to fade out (seconds)
|
|
127
|
-
*/
|
|
128
|
-
stop(fadeTime=0)
|
|
129
|
-
{
|
|
130
|
-
if (!this.source)
|
|
131
|
-
return;
|
|
132
127
|
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
this.gainNode.gain.linearRampToValueAtTime(1, startFade);
|
|
137
|
-
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
138
|
-
this.source.stop(endFade);
|
|
139
|
-
this.source = undefined;
|
|
128
|
+
// Create and return sound instance
|
|
129
|
+
const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
130
|
+
return new SoundInstance(this, volume, rate, pan, loop, paused);
|
|
140
131
|
}
|
|
141
132
|
|
|
142
|
-
/**
|
|
143
|
-
* @
|
|
133
|
+
/** Play a music track that loops by default
|
|
134
|
+
* @param {number} [volume] - Volume to play the music at
|
|
135
|
+
* @param {boolean} [loop] - Should the music loop?
|
|
136
|
+
* @param {boolean} [paused] - Should the music start paused
|
|
137
|
+
* @return {SoundInstance} - The audio source node
|
|
144
138
|
*/
|
|
145
|
-
|
|
139
|
+
playMusic(volume=1, loop=true, paused=false)
|
|
140
|
+
{ return this.play(undefined, volume, 1, 0, loop, paused); }
|
|
146
141
|
|
|
147
|
-
/** Play the sound as a note with a semitone offset
|
|
142
|
+
/** Play the sound as a musical note with a semitone offset
|
|
143
|
+
* This can be used to play music with chromatic scales
|
|
148
144
|
* @param {number} semitoneOffset - How many semitones to offset pitch
|
|
149
|
-
* @param {Vector2} [pos] - World space position to play the sound
|
|
150
|
-
* @param {number} [volume=1] - How much to scale volume by
|
|
151
|
-
* @return {
|
|
145
|
+
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
146
|
+
* @param {number} [volume=1] - How much to scale volume by
|
|
147
|
+
* @return {SoundInstance} - The audio source node
|
|
152
148
|
*/
|
|
153
149
|
playNote(semitoneOffset, pos, volume)
|
|
154
|
-
{
|
|
150
|
+
{
|
|
151
|
+
const pitch = getNoteFrequency(semitoneOffset, 1);
|
|
152
|
+
return this.play(pos, volume, pitch, 0);
|
|
153
|
+
}
|
|
155
154
|
|
|
156
155
|
/** Get how long this sound is in seconds
|
|
157
156
|
* @return {number} - How long the sound is in seconds (undefined if loading)
|
|
158
157
|
*/
|
|
159
|
-
getDuration()
|
|
158
|
+
getDuration()
|
|
160
159
|
{ return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
|
|
161
|
-
|
|
162
|
-
/** Check if sound is
|
|
163
|
-
* @return {boolean} - True if sound is
|
|
160
|
+
|
|
161
|
+
/** Check if sound is loaded, for sounds fetched from a url
|
|
162
|
+
* @return {boolean} - True if sound is loaded and ready to play
|
|
164
163
|
*/
|
|
165
|
-
|
|
164
|
+
isLoaded() { return this.loadedPercent === 1; }
|
|
166
165
|
}
|
|
167
166
|
|
|
168
167
|
///////////////////////////////////////////////////////////////////////////////
|
|
169
168
|
|
|
170
|
-
/**
|
|
169
|
+
/**
|
|
171
170
|
* Sound Wave Object - Stores a wave sound for later use and can be played positionally
|
|
172
171
|
* - this can be used to play wave, mp3, and ogg files
|
|
173
172
|
* @example
|
|
174
173
|
* // create a sound
|
|
175
174
|
* const sound_example = new SoundWave('sound.mp3');
|
|
176
|
-
*
|
|
175
|
+
*
|
|
177
176
|
* // play the sound
|
|
178
177
|
* sound_example.play();
|
|
179
178
|
*/
|
|
@@ -205,26 +204,202 @@ class SoundWave extends Sound
|
|
|
205
204
|
const response = await fetch(filename);
|
|
206
205
|
const arrayBuffer = await response.arrayBuffer();
|
|
207
206
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
|
|
208
|
+
// convert audio buffer to sample channels across multiple frames
|
|
209
|
+
const channelCount = audioBuffer.numberOfChannels;
|
|
210
|
+
const samplesPerFrame = 1e5;
|
|
211
|
+
const sampleChannels = [];
|
|
212
|
+
for (let channel = 0; channel < channelCount; channel++)
|
|
213
|
+
{
|
|
214
|
+
const channelData = audioBuffer.getChannelData(channel);
|
|
215
|
+
const channelLength = channelData.length;
|
|
216
|
+
sampleChannels[channel] = new Array(channelLength);
|
|
217
|
+
let sampleIndex = 0;
|
|
218
|
+
while (sampleIndex < channelLength)
|
|
219
|
+
{
|
|
220
|
+
// yield to next frame
|
|
221
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
222
|
+
|
|
223
|
+
// copy chunk of samples
|
|
224
|
+
const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
|
|
225
|
+
for (; sampleIndex < endIndex; sampleIndex++)
|
|
226
|
+
sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
|
|
227
|
+
|
|
228
|
+
// update loaded percent
|
|
229
|
+
const samplesTotal = channelCount * channelLength;
|
|
230
|
+
const samplesProcessed = channel * channelLength + sampleIndex;
|
|
231
|
+
this.loadedPercent = samplesProcessed / samplesTotal;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// setup the sound to be played
|
|
211
236
|
this.sampleRate = audioBuffer.sampleRate;
|
|
237
|
+
this.sampleChannels = sampleChannels;
|
|
238
|
+
this.loadedPercent = 1;
|
|
212
239
|
if (this.onloadCallback)
|
|
213
240
|
this.onloadCallback();
|
|
214
241
|
}
|
|
215
242
|
}
|
|
216
243
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
|
|
244
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
|
|
248
|
+
* Represents a single playing instance of a sound with pause/resume capabilities
|
|
249
|
+
* @example
|
|
250
|
+
* // Play a sound and get an instance for control
|
|
251
|
+
* const jumpSound = new Sound([.5,.5,220]);
|
|
252
|
+
* const instance = jumpSound.play();
|
|
253
|
+
*
|
|
254
|
+
* // Control the individual instance
|
|
255
|
+
* instance.setVolume(.5);
|
|
256
|
+
* instance.pause();
|
|
257
|
+
* instance.unpause();
|
|
258
|
+
* instance.stop();
|
|
259
|
+
*/
|
|
260
|
+
class SoundInstance
|
|
224
261
|
{
|
|
225
|
-
|
|
262
|
+
/** Create a sound instance
|
|
263
|
+
* @param {Sound} sound - The sound object
|
|
264
|
+
* @param {number} [volume] - How much to scale volume by
|
|
265
|
+
* @param {number} [rate] - The playback rate to use
|
|
266
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
267
|
+
* @param {boolean} [loop] - Should the sound loop?
|
|
268
|
+
* @param {boolean} [paused] - Should the sound start paused? */
|
|
269
|
+
constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
|
|
270
|
+
{
|
|
271
|
+
ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
|
|
272
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
273
|
+
ASSERT(rate >= 0, 'Sound rate must be positive or zero');
|
|
274
|
+
ASSERT(isNumber(pan), 'Sound pan must be a number');
|
|
275
|
+
|
|
276
|
+
/** @property {Sound} - The sound object */
|
|
277
|
+
this.sound = sound;
|
|
278
|
+
/** @property {number} - How much to scale volume by */
|
|
279
|
+
this.volume = volume;
|
|
280
|
+
/** @property {number} - The playback rate to use */
|
|
281
|
+
this.rate = rate;
|
|
282
|
+
/** @property {number} - How much to apply stereo panning */
|
|
283
|
+
this.pan = pan;
|
|
284
|
+
/** @property {boolean} - Should the sound loop */
|
|
285
|
+
this.loop = loop;
|
|
286
|
+
/** @property {number} - Timestamp for audio context when paused */
|
|
287
|
+
this.pausedTime = 0;
|
|
288
|
+
/** @property {number} - Timestamp for audio context when started */
|
|
289
|
+
this.startTime = undefined;
|
|
290
|
+
/** @property {GainNode} - Gain node for the sound */
|
|
291
|
+
this.gainNode = undefined;
|
|
292
|
+
/** @property {AudioBufferSourceNode} - Source node of the audio */
|
|
293
|
+
this.source = undefined;
|
|
294
|
+
// setup end callback and start sound
|
|
295
|
+
this.onendedCallback = (source)=>
|
|
296
|
+
{
|
|
297
|
+
if (source === this.source)
|
|
298
|
+
this.source = undefined;
|
|
299
|
+
};
|
|
300
|
+
if (!paused)
|
|
301
|
+
this.start();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Start playing the sound instance from the offset time
|
|
305
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
306
|
+
*/
|
|
307
|
+
start(offset=0)
|
|
308
|
+
{
|
|
309
|
+
ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
|
|
310
|
+
if (this.isPlaying())
|
|
311
|
+
this.stop();
|
|
312
|
+
this.gainNode = audioContext.createGain();
|
|
313
|
+
this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
|
|
314
|
+
this.startTime = audioContext.currentTime - offset;
|
|
315
|
+
this.pausedTime = undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Set the volume of this sound instance
|
|
319
|
+
* @param {number} volume */
|
|
320
|
+
setVolume(volume)
|
|
321
|
+
{
|
|
322
|
+
ASSERT(volume >= 0, 'Sound volume must be positive or zero');
|
|
323
|
+
this.volume = volume;
|
|
324
|
+
if (this.gainNode)
|
|
325
|
+
this.gainNode.gain.value = volume;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Stop this sound instance and reset position to the start */
|
|
329
|
+
stop(fadeTime=0)
|
|
330
|
+
{
|
|
331
|
+
ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
|
|
332
|
+
if (this.isPlaying())
|
|
333
|
+
{
|
|
334
|
+
if (fadeTime)
|
|
335
|
+
{
|
|
336
|
+
// ramp off gain
|
|
337
|
+
const startFade = audioContext.currentTime;
|
|
338
|
+
const endFade = startFade + fadeTime;
|
|
339
|
+
this.gainNode.gain.linearRampToValueAtTime(1, startFade);
|
|
340
|
+
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
341
|
+
this.source.stop(endFade);
|
|
342
|
+
}
|
|
343
|
+
else
|
|
344
|
+
this.source.stop();
|
|
345
|
+
}
|
|
346
|
+
this.pausedTime = 0;
|
|
347
|
+
this.source = undefined;
|
|
348
|
+
this.startTime = undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Pause this sound instance */
|
|
352
|
+
pause()
|
|
353
|
+
{
|
|
354
|
+
if (this.isPaused())
|
|
355
|
+
return;
|
|
356
|
+
|
|
357
|
+
// save current time and stop sound
|
|
358
|
+
this.pausedTime = this.getCurrentTime();
|
|
359
|
+
this.source.stop();
|
|
360
|
+
this.source = undefined;
|
|
361
|
+
this.startTime = undefined;
|
|
362
|
+
}
|
|
226
363
|
|
|
227
|
-
|
|
364
|
+
/** Unpauses this sound instance */
|
|
365
|
+
resume()
|
|
366
|
+
{
|
|
367
|
+
if (!this.isPaused())
|
|
368
|
+
return;
|
|
369
|
+
|
|
370
|
+
// restart sound from paused time
|
|
371
|
+
this.start(this.pausedTime);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Check if this instance is currently playing
|
|
375
|
+
* @return {boolean} - True if playing
|
|
376
|
+
*/
|
|
377
|
+
isPlaying() { return !!this.source; }
|
|
378
|
+
|
|
379
|
+
/** Check if this instance is paused and was not stopped
|
|
380
|
+
* @return {boolean} - True if paused
|
|
381
|
+
*/
|
|
382
|
+
isPaused() { return !this.isPlaying(); }
|
|
383
|
+
|
|
384
|
+
/** Get the current playback time in seconds
|
|
385
|
+
* @return {number} - Current playback time
|
|
386
|
+
*/
|
|
387
|
+
getCurrentTime()
|
|
388
|
+
{
|
|
389
|
+
const deltaTime = mod(audioContext.currentTime - this.startTime,
|
|
390
|
+
this.getDuration());
|
|
391
|
+
return this.isPlaying() ? deltaTime : this.pausedTime;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Get the total duration of this sound
|
|
395
|
+
* @return {number} - Total duration in seconds
|
|
396
|
+
*/
|
|
397
|
+
getDuration() { return this.sound.getDuration() / this.rate; }
|
|
398
|
+
|
|
399
|
+
/** Get source of this sound instance
|
|
400
|
+
* @return {AudioBufferSourceNode}
|
|
401
|
+
*/
|
|
402
|
+
getSource() { return this.source; }
|
|
228
403
|
}
|
|
229
404
|
|
|
230
405
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -278,9 +453,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
|
278
453
|
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
279
454
|
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
280
455
|
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing
|
|
456
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
457
|
+
* @param {Function} [onended] - Callback for when the sound ends
|
|
281
458
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
282
459
|
* @memberof Audio */
|
|
283
|
-
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=
|
|
460
|
+
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
|
|
284
461
|
{
|
|
285
462
|
if (!soundEnable || headlessMode) return;
|
|
286
463
|
|
|
@@ -305,16 +482,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
305
482
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
306
483
|
source.connect(pannerNode).connect(gainNode);
|
|
307
484
|
|
|
308
|
-
//
|
|
309
|
-
if (
|
|
485
|
+
// callback when the sound ends
|
|
486
|
+
if (onended)
|
|
487
|
+
source.addEventListener('ended', ()=> onended(source));
|
|
488
|
+
|
|
489
|
+
if (!audioIsRunning())
|
|
310
490
|
{
|
|
311
|
-
// fix stalled audio
|
|
312
|
-
audioContext.resume()
|
|
491
|
+
// fix stalled audio, this sound won't be able to play
|
|
492
|
+
audioContext.resume();
|
|
493
|
+
return;
|
|
313
494
|
}
|
|
314
|
-
else
|
|
315
|
-
source.start();
|
|
316
495
|
|
|
317
|
-
// return sound
|
|
496
|
+
// play and return sound
|
|
497
|
+
const startOffset = offset * rate;
|
|
498
|
+
source.start(0, startOffset);
|
|
318
499
|
return source;
|
|
319
500
|
}
|
|
320
501
|
|
|
@@ -322,18 +503,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
322
503
|
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
|
|
323
504
|
|
|
324
505
|
/** Generate and play a ZzFX sound
|
|
325
|
-
*
|
|
506
|
+
*
|
|
326
507
|
* <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
|
|
327
508
|
* @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
|
|
328
509
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
329
510
|
* @memberof Audio */
|
|
330
511
|
function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
|
|
331
512
|
|
|
332
|
-
/** Sample rate used for all ZzFX sounds
|
|
333
|
-
* @default 44100
|
|
334
|
-
* @memberof Audio */
|
|
335
|
-
const zzfxR = 44100;
|
|
336
|
-
|
|
337
513
|
/** Generate samples for a ZzFX sound
|
|
338
514
|
* @param {number} [volume] - Volume scale (percent)
|
|
339
515
|
* @param {number} [randomness] - How much to randomize frequency (percent Hz)
|
|
@@ -357,11 +533,10 @@ const zzfxR = 44100;
|
|
|
357
533
|
* @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
|
|
358
534
|
* @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
|
|
359
535
|
* @return {Array} - Array of audio samples
|
|
360
|
-
* @memberof Audio
|
|
361
|
-
*/
|
|
536
|
+
* @memberof Audio */
|
|
362
537
|
function zzfxG
|
|
363
538
|
(
|
|
364
|
-
volume = 1,
|
|
539
|
+
volume = 1,
|
|
365
540
|
randomness = .05,
|
|
366
541
|
frequency = 220,
|
|
367
542
|
attack = 0,
|
|
@@ -369,11 +544,11 @@ function zzfxG
|
|
|
369
544
|
release = .1,
|
|
370
545
|
shape = 0,
|
|
371
546
|
shapeCurve = 1,
|
|
372
|
-
slide = 0,
|
|
373
|
-
deltaSlide = 0,
|
|
374
|
-
pitchJump = 0,
|
|
375
|
-
pitchJumpTime = 0,
|
|
376
|
-
repeatTime = 0,
|
|
547
|
+
slide = 0,
|
|
548
|
+
deltaSlide = 0,
|
|
549
|
+
pitchJump = 0,
|
|
550
|
+
pitchJumpTime = 0,
|
|
551
|
+
repeatTime = 0,
|
|
377
552
|
noise = 0,
|
|
378
553
|
modulation = 0,
|
|
379
554
|
bitCrush = 0,
|
|
@@ -385,19 +560,19 @@ function zzfxG
|
|
|
385
560
|
)
|
|
386
561
|
{
|
|
387
562
|
// init parameters
|
|
388
|
-
let sampleRate =
|
|
389
|
-
PI2 = PI*2,
|
|
563
|
+
let sampleRate = audioDefaultSampleRate,
|
|
564
|
+
PI2 = PI*2,
|
|
390
565
|
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
|
|
391
|
-
startFrequency = frequency *=
|
|
566
|
+
startFrequency = frequency *=
|
|
392
567
|
(1 + rand(randomness,-randomness)) * PI2 / sampleRate,
|
|
393
|
-
modOffset = 0, // modulation offset
|
|
568
|
+
modOffset = 0, // modulation offset
|
|
394
569
|
repeat = 0, // repeat offset
|
|
395
570
|
crush = 0, // bit crush offset
|
|
396
571
|
jump = 1, // pitch jump timer
|
|
397
572
|
length, // sample length
|
|
398
573
|
b = [], // sample buffer
|
|
399
574
|
t = 0, // sample time
|
|
400
|
-
i = 0, // sample index
|
|
575
|
+
i = 0, // sample index
|
|
401
576
|
s = 0, // sample value
|
|
402
577
|
f, // wave frequency
|
|
403
578
|
|
|
@@ -405,7 +580,7 @@ function zzfxG
|
|
|
405
580
|
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
|
|
406
581
|
cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
|
|
407
582
|
a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
|
|
408
|
-
b0 = (1 + sign(filter) * cos) / 2 / a0,
|
|
583
|
+
b0 = (1 + sign(filter) * cos) / 2 / a0,
|
|
409
584
|
b1 = -(sign(filter) + cos) / a0, b2 = b0,
|
|
410
585
|
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
|
|
411
586
|
|
|
@@ -423,7 +598,7 @@ function zzfxG
|
|
|
423
598
|
repeatTime = repeatTime * sampleRate | 0;
|
|
424
599
|
|
|
425
600
|
// generate waveform
|
|
426
|
-
for(length = attack + decay + sustain + release + delay | 0;
|
|
601
|
+
for (length = attack + decay + sustain + release + delay | 0;
|
|
427
602
|
i < length; b[i++] = s * volume) // sample
|
|
428
603
|
{
|
|
429
604
|
if (!(++crush%(bitCrush*100|0))) // bit crush
|
|
@@ -451,7 +626,7 @@ function zzfxG
|
|
|
451
626
|
0); // post release
|
|
452
627
|
|
|
453
628
|
s = delay ? s/2 + (delay > i ? 0 : // delay
|
|
454
|
-
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
629
|
+
(i<length-delay? 1 : (length-i)/delay) * // release delay
|
|
455
630
|
b[i-delay|0]/2/volume) : s; // sample delay
|
|
456
631
|
|
|
457
632
|
if (filter) // apply filter
|
|
@@ -463,14 +638,14 @@ function zzfxG
|
|
|
463
638
|
t += f + f*noise*Math.sin(i**5); // noise
|
|
464
639
|
|
|
465
640
|
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
466
|
-
{
|
|
641
|
+
{
|
|
467
642
|
frequency += pitchJump; // apply pitch jump
|
|
468
643
|
startFrequency += pitchJump; // also apply to start
|
|
469
644
|
jump = 0; // stop pitch jump time
|
|
470
|
-
}
|
|
645
|
+
}
|
|
471
646
|
|
|
472
647
|
if (repeatTime && !(++repeat % repeatTime)) // repeat
|
|
473
|
-
{
|
|
648
|
+
{
|
|
474
649
|
frequency = startFrequency; // reset frequency
|
|
475
650
|
slide = startSlide; // reset slide
|
|
476
651
|
jump ||= 1; // reset pitch jump time
|
package/src/engineBuild.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
4
|
* LittleJS Build System
|
|
5
5
|
* - Combine input files
|
|
6
6
|
* - Run custom build steps
|
|
@@ -76,8 +76,8 @@ Build
|
|
|
76
76
|
`${BUILD_FOLDER}/${ENGINE_NAME}.js`,
|
|
77
77
|
[
|
|
78
78
|
`${SOURCE_FOLDER}/engine.js`,
|
|
79
|
-
`${SOURCE_FOLDER}/engineDebug.js`,
|
|
80
|
-
...engineSourceFiles,
|
|
79
|
+
`${SOURCE_FOLDER}/engineDebug.js`,
|
|
80
|
+
...engineSourceFiles,
|
|
81
81
|
...enginePluginFiles
|
|
82
82
|
],
|
|
83
83
|
[], true
|
|
@@ -89,8 +89,8 @@ Build
|
|
|
89
89
|
`${BUILD_FOLDER}/${ENGINE_NAME}.release.js`,
|
|
90
90
|
[
|
|
91
91
|
`${SOURCE_FOLDER}/engine.js`,
|
|
92
|
-
`${SOURCE_FOLDER}/engineRelease.js`,
|
|
93
|
-
...engineSourceFiles,
|
|
92
|
+
`${SOURCE_FOLDER}/engineRelease.js`,
|
|
93
|
+
...engineSourceFiles,
|
|
94
94
|
...enginePluginFiles
|
|
95
95
|
],
|
|
96
96
|
[], true
|
|
@@ -109,8 +109,8 @@ Build
|
|
|
109
109
|
'Build Engine -- ESM',
|
|
110
110
|
`${BUILD_FOLDER}/${ENGINE_NAME}.esm.js`,
|
|
111
111
|
[
|
|
112
|
-
`${BUILD_FOLDER}/${ENGINE_NAME}.js`,
|
|
113
|
-
`${SOURCE_FOLDER}/engineExport.js`,
|
|
112
|
+
`${BUILD_FOLDER}/${ENGINE_NAME}.js`,
|
|
113
|
+
`${SOURCE_FOLDER}/engineExport.js`,
|
|
114
114
|
`${PLUGIN_FOLDER}/pluginExport.js`
|
|
115
115
|
],
|
|
116
116
|
[typeScriptBuildStep]
|
|
@@ -121,8 +121,8 @@ Build
|
|
|
121
121
|
'Build Engine -- ESM minified release',
|
|
122
122
|
`${BUILD_FOLDER}/${ENGINE_NAME}.esm.min.js`,
|
|
123
123
|
[
|
|
124
|
-
`${BUILD_FOLDER}/${ENGINE_NAME}.release.js`,
|
|
125
|
-
`${SOURCE_FOLDER}/engineExport.js`,
|
|
124
|
+
`${BUILD_FOLDER}/${ENGINE_NAME}.release.js`,
|
|
125
|
+
`${SOURCE_FOLDER}/engineExport.js`,
|
|
126
126
|
`${PLUGIN_FOLDER}/pluginExport.js`
|
|
127
127
|
],
|
|
128
128
|
[uglifyBuildStep, addLicenseStep]
|