littlejsengine 1.18.27 → 1.18.29
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 +39 -2
- package/dist/littlejs.esm.js +111 -43
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +109 -43
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +109 -43
- package/package.json +1 -1
- package/src/engine.js +1 -1
- package/src/engineAudio.js +108 -42
- package/src/engineExport.js +2 -0
package/dist/littlejs.release.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.29';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -6283,7 +6283,15 @@ class Sound
|
|
|
6283
6283
|
this.randomness = randomness ?? 0;
|
|
6284
6284
|
/** @property {number} - Sample rate for this sound */
|
|
6285
6285
|
this.sampleRate = audioDefaultSampleRate;
|
|
6286
|
-
/** @property {number} -
|
|
6286
|
+
/** @property {number} - How many samples per channel this sound has */
|
|
6287
|
+
this.sampleLength = 0;
|
|
6288
|
+
/** @property {AudioBuffer} - Decoded audio shared by every play of this sound
|
|
6289
|
+
* @type {AudioBuffer} */
|
|
6290
|
+
this.sampleBuffer = undefined;
|
|
6291
|
+
/** @private @type {Array<Array<number>|Float32Array>} */
|
|
6292
|
+
this._sampleChannels = undefined;
|
|
6293
|
+
/** @property {number} - Percentage of this sound currently loaded, sounds
|
|
6294
|
+
* fetched from a url stay at 0 until decoding completes */
|
|
6287
6295
|
this.loadedPercent = 0;
|
|
6288
6296
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
6289
6297
|
this.onloadCallback = onloadCallback;
|
|
@@ -6299,19 +6307,62 @@ class Sound
|
|
|
6299
6307
|
this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
|
|
6300
6308
|
zzfxSound[randomnessIndex] = 0;
|
|
6301
6309
|
|
|
6302
|
-
// generate the zzfx samples
|
|
6310
|
+
// generate the zzfx samples, then hand them to an audio buffer so
|
|
6311
|
+
// the plain arrays can be released and every play shares the buffer
|
|
6303
6312
|
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
6313
|
+
this.buildSampleBuffer();
|
|
6304
6314
|
this.loadedPercent = 1;
|
|
6305
6315
|
onloadCallback?.(this);
|
|
6306
6316
|
}
|
|
6307
6317
|
else if (typeof asset === 'string')
|
|
6308
6318
|
{
|
|
6309
|
-
// load the audio file
|
|
6319
|
+
// load the audio file, report failures rather than leaving an
|
|
6320
|
+
// unhandled rejection, the sound just stays unloaded and silent
|
|
6310
6321
|
const filename = asset;
|
|
6311
|
-
this.loadSound(filename)
|
|
6322
|
+
this.loadSound(filename).catch(e=>
|
|
6323
|
+
LOG('Sound load failed for', filename, '-', e.message));
|
|
6312
6324
|
}
|
|
6313
6325
|
}
|
|
6314
6326
|
|
|
6327
|
+
/** Sample data for each channel
|
|
6328
|
+
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
6329
|
+
* the arrays from it and caches them. The copies are safe to hold onto,
|
|
6330
|
+
* playing a sound detaches the buffer's own channel arrays.
|
|
6331
|
+
* @type {Array<Array<number>|Float32Array>} */
|
|
6332
|
+
get sampleChannels()
|
|
6333
|
+
{
|
|
6334
|
+
const buffer = this.sampleBuffer;
|
|
6335
|
+
if (!this._sampleChannels && buffer)
|
|
6336
|
+
{
|
|
6337
|
+
const channels = [];
|
|
6338
|
+
for (let i = 0; i < buffer.numberOfChannels; i++)
|
|
6339
|
+
channels.push(buffer.getChannelData(i).slice());
|
|
6340
|
+
this._sampleChannels = channels;
|
|
6341
|
+
}
|
|
6342
|
+
return this._sampleChannels;
|
|
6343
|
+
}
|
|
6344
|
+
|
|
6345
|
+
/** @param {Array<Array<number>|Float32Array>} sampleChannels */
|
|
6346
|
+
set sampleChannels(sampleChannels)
|
|
6347
|
+
{
|
|
6348
|
+
// new samples invalidate the buffer built from the old ones
|
|
6349
|
+
this._sampleChannels = sampleChannels;
|
|
6350
|
+
this.sampleBuffer = undefined;
|
|
6351
|
+
this.sampleLength = sampleChannels?.[0]?.length || 0;
|
|
6352
|
+
}
|
|
6353
|
+
|
|
6354
|
+
/** Move this sound's samples into an audio buffer that every play can share
|
|
6355
|
+
* Does nothing if there is already a buffer or no samples to build one from */
|
|
6356
|
+
buildSampleBuffer()
|
|
6357
|
+
{
|
|
6358
|
+
if (this.sampleBuffer || !this._sampleChannels || headlessMode) return;
|
|
6359
|
+
|
|
6360
|
+
this.sampleBuffer = createAudioBuffer(this._sampleChannels, this.sampleRate);
|
|
6361
|
+
|
|
6362
|
+
// the buffer owns the samples now, release the arrays we built it from
|
|
6363
|
+
this._sampleChannels = undefined;
|
|
6364
|
+
}
|
|
6365
|
+
|
|
6315
6366
|
/** Play the sound
|
|
6316
6367
|
* Sounds may not play until a user interaction occurs
|
|
6317
6368
|
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
@@ -6330,7 +6381,7 @@ class Sound
|
|
|
6330
6381
|
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
6331
6382
|
|
|
6332
6383
|
if (!soundEnable || headlessMode) return;
|
|
6333
|
-
if (!this.
|
|
6384
|
+
if (!this.sampleBuffer && !this._sampleChannels) return;
|
|
6334
6385
|
|
|
6335
6386
|
let pan;
|
|
6336
6387
|
if (pos)
|
|
@@ -6397,7 +6448,7 @@ class Sound
|
|
|
6397
6448
|
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
6398
6449
|
*/
|
|
6399
6450
|
getDuration()
|
|
6400
|
-
{ return this.
|
|
6451
|
+
{ return this.sampleLength / this.sampleRate || 0; }
|
|
6401
6452
|
|
|
6402
6453
|
/** Check if sound is loaded, for sounds fetched from a url
|
|
6403
6454
|
* @return {boolean} - True if sound is loaded and ready to play
|
|
@@ -6415,36 +6466,11 @@ class Sound
|
|
|
6415
6466
|
const arrayBuffer = await response.arrayBuffer();
|
|
6416
6467
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
6417
6468
|
|
|
6418
|
-
//
|
|
6419
|
-
|
|
6420
|
-
const samplesPerFrame = 1e5;
|
|
6421
|
-
const sampleChannels = [];
|
|
6422
|
-
for (let channel = 0; channel < channelCount; channel++)
|
|
6423
|
-
{
|
|
6424
|
-
const channelData = audioBuffer.getChannelData(channel);
|
|
6425
|
-
const channelLength = channelData.length;
|
|
6426
|
-
sampleChannels[channel] = new Array(channelLength);
|
|
6427
|
-
let sampleIndex = 0;
|
|
6428
|
-
while (sampleIndex < channelLength)
|
|
6429
|
-
{
|
|
6430
|
-
// yield to next frame
|
|
6431
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
6432
|
-
|
|
6433
|
-
// copy chunk of samples
|
|
6434
|
-
const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
|
|
6435
|
-
for (; sampleIndex < endIndex; sampleIndex++)
|
|
6436
|
-
sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
|
|
6437
|
-
|
|
6438
|
-
// update loaded percent
|
|
6439
|
-
const samplesTotal = channelCount * channelLength;
|
|
6440
|
-
const samplesProcessed = channel * channelLength + sampleIndex;
|
|
6441
|
-
this.loadedPercent = samplesProcessed / samplesTotal;
|
|
6442
|
-
}
|
|
6443
|
-
}
|
|
6444
|
-
|
|
6445
|
-
// setup the sound to be played
|
|
6469
|
+
// keep the decoded buffer as is, it is exactly what playback needs and
|
|
6470
|
+
// every play shares it, no channel data is read or copied
|
|
6446
6471
|
this.sampleRate = audioBuffer.sampleRate;
|
|
6447
|
-
this.
|
|
6472
|
+
this.sampleLength = audioBuffer.length;
|
|
6473
|
+
this.sampleBuffer = audioBuffer;
|
|
6448
6474
|
this.loadedPercent = 1;
|
|
6449
6475
|
this.onloadCallback?.(this);
|
|
6450
6476
|
}
|
|
@@ -6520,7 +6546,12 @@ class SoundInstance
|
|
|
6520
6546
|
if (this.isPlaying())
|
|
6521
6547
|
this.stop();
|
|
6522
6548
|
this.gainNode = audioContext.createGain();
|
|
6523
|
-
|
|
6549
|
+
|
|
6550
|
+
// build the shared buffer if it was not made at load time, then play it
|
|
6551
|
+
this.sound.buildSampleBuffer();
|
|
6552
|
+
this.source = this.sound.sampleBuffer ?
|
|
6553
|
+
playAudioBuffer(this.sound.sampleBuffer, this.volume, this.rate, this.pan, this.loop, this.gainNode, offset, this.onendedCallback) :
|
|
6554
|
+
playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
|
|
6524
6555
|
if (this.source)
|
|
6525
6556
|
{
|
|
6526
6557
|
this.startTime = audioContext.currentTime - offset;
|
|
@@ -6646,7 +6677,7 @@ function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
|
6646
6677
|
// build utterance and speak
|
|
6647
6678
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
6648
6679
|
utterance.lang = language;
|
|
6649
|
-
utterance.volume =
|
|
6680
|
+
utterance.volume = volume*soundVolume;
|
|
6650
6681
|
utterance.rate = rate;
|
|
6651
6682
|
utterance.pitch = pitch;
|
|
6652
6683
|
speechSynthesis.speak(utterance);
|
|
@@ -6695,19 +6726,54 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
6695
6726
|
|
|
6696
6727
|
if (!audioIsRunning())
|
|
6697
6728
|
{
|
|
6698
|
-
// fix stalled audio,
|
|
6729
|
+
// fix stalled audio, don't build a buffer that can't be played
|
|
6699
6730
|
audioContext.resume();
|
|
6700
6731
|
return;
|
|
6701
6732
|
}
|
|
6702
6733
|
|
|
6703
|
-
|
|
6734
|
+
const buffer = createAudioBuffer(sampleChannels, sampleRate);
|
|
6735
|
+
return playAudioBuffer(buffer, volume, rate, pan, loop, gainNode, offset, onended);
|
|
6736
|
+
}
|
|
6737
|
+
|
|
6738
|
+
/** Copy arrays of samples into a new audio buffer
|
|
6739
|
+
* @param {Array} sampleChannels - Array of arrays of samples (for stereo playback)
|
|
6740
|
+
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
6741
|
+
* @return {AudioBuffer} - The audio buffer holding the samples
|
|
6742
|
+
* @memberof Audio */
|
|
6743
|
+
function createAudioBuffer(sampleChannels, sampleRate=audioDefaultSampleRate)
|
|
6744
|
+
{
|
|
6704
6745
|
const channelCount = sampleChannels.length;
|
|
6705
6746
|
const sampleLength = sampleChannels[0].length;
|
|
6706
6747
|
const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
|
|
6707
|
-
const source = audioContext.createBufferSource();
|
|
6708
|
-
|
|
6709
|
-
// copy samples to buffer and setup source
|
|
6710
6748
|
sampleChannels.forEach((c,i)=> buffer.getChannelData(i).set(c));
|
|
6749
|
+
return buffer;
|
|
6750
|
+
}
|
|
6751
|
+
|
|
6752
|
+
/** Play an audio buffer with given settings
|
|
6753
|
+
* The buffer can be shared by any number of sounds playing at once
|
|
6754
|
+
* @param {AudioBuffer} buffer - The audio buffer to play
|
|
6755
|
+
* @param {number} [volume] - How much to scale volume by
|
|
6756
|
+
* @param {number} [rate] - The playback rate to use
|
|
6757
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
6758
|
+
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
6759
|
+
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
6760
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
6761
|
+
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
6762
|
+
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
6763
|
+
* @memberof Audio */
|
|
6764
|
+
function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode, offset=0, onended)
|
|
6765
|
+
{
|
|
6766
|
+
if (!soundEnable || headlessMode) return;
|
|
6767
|
+
|
|
6768
|
+
if (!audioIsRunning())
|
|
6769
|
+
{
|
|
6770
|
+
// fix stalled audio, this sound won't be able to play
|
|
6771
|
+
audioContext.resume();
|
|
6772
|
+
return;
|
|
6773
|
+
}
|
|
6774
|
+
|
|
6775
|
+
// setup source, many sources can share one buffer
|
|
6776
|
+
const source = audioContext.createBufferSource();
|
|
6711
6777
|
source.buffer = buffer;
|
|
6712
6778
|
source.playbackRate.value = rate;
|
|
6713
6779
|
source.loop = loop;
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
package/src/engineAudio.js
CHANGED
|
@@ -98,7 +98,15 @@ class Sound
|
|
|
98
98
|
this.randomness = randomness ?? 0;
|
|
99
99
|
/** @property {number} - Sample rate for this sound */
|
|
100
100
|
this.sampleRate = audioDefaultSampleRate;
|
|
101
|
-
/** @property {number} -
|
|
101
|
+
/** @property {number} - How many samples per channel this sound has */
|
|
102
|
+
this.sampleLength = 0;
|
|
103
|
+
/** @property {AudioBuffer} - Decoded audio shared by every play of this sound
|
|
104
|
+
* @type {AudioBuffer} */
|
|
105
|
+
this.sampleBuffer = undefined;
|
|
106
|
+
/** @private @type {Array<Array<number>|Float32Array>} */
|
|
107
|
+
this._sampleChannels = undefined;
|
|
108
|
+
/** @property {number} - Percentage of this sound currently loaded, sounds
|
|
109
|
+
* fetched from a url stay at 0 until decoding completes */
|
|
102
110
|
this.loadedPercent = 0;
|
|
103
111
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
104
112
|
this.onloadCallback = onloadCallback;
|
|
@@ -114,19 +122,62 @@ class Sound
|
|
|
114
122
|
this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
|
|
115
123
|
zzfxSound[randomnessIndex] = 0;
|
|
116
124
|
|
|
117
|
-
// generate the zzfx samples
|
|
125
|
+
// generate the zzfx samples, then hand them to an audio buffer so
|
|
126
|
+
// the plain arrays can be released and every play shares the buffer
|
|
118
127
|
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
128
|
+
this.buildSampleBuffer();
|
|
119
129
|
this.loadedPercent = 1;
|
|
120
130
|
onloadCallback?.(this);
|
|
121
131
|
}
|
|
122
132
|
else if (typeof asset === 'string')
|
|
123
133
|
{
|
|
124
|
-
// load the audio file
|
|
134
|
+
// load the audio file, report failures rather than leaving an
|
|
135
|
+
// unhandled rejection, the sound just stays unloaded and silent
|
|
125
136
|
const filename = asset;
|
|
126
|
-
this.loadSound(filename)
|
|
137
|
+
this.loadSound(filename).catch(e=>
|
|
138
|
+
LOG('Sound load failed for', filename, '-', e.message));
|
|
127
139
|
}
|
|
128
140
|
}
|
|
129
141
|
|
|
142
|
+
/** Sample data for each channel
|
|
143
|
+
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
144
|
+
* the arrays from it and caches them. The copies are safe to hold onto,
|
|
145
|
+
* playing a sound detaches the buffer's own channel arrays.
|
|
146
|
+
* @type {Array<Array<number>|Float32Array>} */
|
|
147
|
+
get sampleChannels()
|
|
148
|
+
{
|
|
149
|
+
const buffer = this.sampleBuffer;
|
|
150
|
+
if (!this._sampleChannels && buffer)
|
|
151
|
+
{
|
|
152
|
+
const channels = [];
|
|
153
|
+
for (let i = 0; i < buffer.numberOfChannels; i++)
|
|
154
|
+
channels.push(buffer.getChannelData(i).slice());
|
|
155
|
+
this._sampleChannels = channels;
|
|
156
|
+
}
|
|
157
|
+
return this._sampleChannels;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** @param {Array<Array<number>|Float32Array>} sampleChannels */
|
|
161
|
+
set sampleChannels(sampleChannels)
|
|
162
|
+
{
|
|
163
|
+
// new samples invalidate the buffer built from the old ones
|
|
164
|
+
this._sampleChannels = sampleChannels;
|
|
165
|
+
this.sampleBuffer = undefined;
|
|
166
|
+
this.sampleLength = sampleChannels?.[0]?.length || 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Move this sound's samples into an audio buffer that every play can share
|
|
170
|
+
* Does nothing if there is already a buffer or no samples to build one from */
|
|
171
|
+
buildSampleBuffer()
|
|
172
|
+
{
|
|
173
|
+
if (this.sampleBuffer || !this._sampleChannels || headlessMode) return;
|
|
174
|
+
|
|
175
|
+
this.sampleBuffer = createAudioBuffer(this._sampleChannels, this.sampleRate);
|
|
176
|
+
|
|
177
|
+
// the buffer owns the samples now, release the arrays we built it from
|
|
178
|
+
this._sampleChannels = undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
130
181
|
/** Play the sound
|
|
131
182
|
* Sounds may not play until a user interaction occurs
|
|
132
183
|
* @param {Vector2} [pos] - World space position to play the sound if any
|
|
@@ -145,7 +196,7 @@ class Sound
|
|
|
145
196
|
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
146
197
|
|
|
147
198
|
if (!soundEnable || headlessMode) return;
|
|
148
|
-
if (!this.
|
|
199
|
+
if (!this.sampleBuffer && !this._sampleChannels) return;
|
|
149
200
|
|
|
150
201
|
let pan;
|
|
151
202
|
if (pos)
|
|
@@ -212,7 +263,7 @@ class Sound
|
|
|
212
263
|
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
213
264
|
*/
|
|
214
265
|
getDuration()
|
|
215
|
-
{ return this.
|
|
266
|
+
{ return this.sampleLength / this.sampleRate || 0; }
|
|
216
267
|
|
|
217
268
|
/** Check if sound is loaded, for sounds fetched from a url
|
|
218
269
|
* @return {boolean} - True if sound is loaded and ready to play
|
|
@@ -230,36 +281,11 @@ class Sound
|
|
|
230
281
|
const arrayBuffer = await response.arrayBuffer();
|
|
231
282
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
232
283
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
const samplesPerFrame = 1e5;
|
|
236
|
-
const sampleChannels = [];
|
|
237
|
-
for (let channel = 0; channel < channelCount; channel++)
|
|
238
|
-
{
|
|
239
|
-
const channelData = audioBuffer.getChannelData(channel);
|
|
240
|
-
const channelLength = channelData.length;
|
|
241
|
-
sampleChannels[channel] = new Array(channelLength);
|
|
242
|
-
let sampleIndex = 0;
|
|
243
|
-
while (sampleIndex < channelLength)
|
|
244
|
-
{
|
|
245
|
-
// yield to next frame
|
|
246
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
247
|
-
|
|
248
|
-
// copy chunk of samples
|
|
249
|
-
const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
|
|
250
|
-
for (; sampleIndex < endIndex; sampleIndex++)
|
|
251
|
-
sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
|
|
252
|
-
|
|
253
|
-
// update loaded percent
|
|
254
|
-
const samplesTotal = channelCount * channelLength;
|
|
255
|
-
const samplesProcessed = channel * channelLength + sampleIndex;
|
|
256
|
-
this.loadedPercent = samplesProcessed / samplesTotal;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// setup the sound to be played
|
|
284
|
+
// keep the decoded buffer as is, it is exactly what playback needs and
|
|
285
|
+
// every play shares it, no channel data is read or copied
|
|
261
286
|
this.sampleRate = audioBuffer.sampleRate;
|
|
262
|
-
this.
|
|
287
|
+
this.sampleLength = audioBuffer.length;
|
|
288
|
+
this.sampleBuffer = audioBuffer;
|
|
263
289
|
this.loadedPercent = 1;
|
|
264
290
|
this.onloadCallback?.(this);
|
|
265
291
|
}
|
|
@@ -335,7 +361,12 @@ class SoundInstance
|
|
|
335
361
|
if (this.isPlaying())
|
|
336
362
|
this.stop();
|
|
337
363
|
this.gainNode = audioContext.createGain();
|
|
338
|
-
|
|
364
|
+
|
|
365
|
+
// build the shared buffer if it was not made at load time, then play it
|
|
366
|
+
this.sound.buildSampleBuffer();
|
|
367
|
+
this.source = this.sound.sampleBuffer ?
|
|
368
|
+
playAudioBuffer(this.sound.sampleBuffer, this.volume, this.rate, this.pan, this.loop, this.gainNode, offset, this.onendedCallback) :
|
|
369
|
+
playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
|
|
339
370
|
if (this.source)
|
|
340
371
|
{
|
|
341
372
|
this.startTime = audioContext.currentTime - offset;
|
|
@@ -461,7 +492,7 @@ function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
|
461
492
|
// build utterance and speak
|
|
462
493
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
463
494
|
utterance.lang = language;
|
|
464
|
-
utterance.volume =
|
|
495
|
+
utterance.volume = volume*soundVolume;
|
|
465
496
|
utterance.rate = rate;
|
|
466
497
|
utterance.pitch = pitch;
|
|
467
498
|
speechSynthesis.speak(utterance);
|
|
@@ -510,19 +541,54 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
510
541
|
|
|
511
542
|
if (!audioIsRunning())
|
|
512
543
|
{
|
|
513
|
-
// fix stalled audio,
|
|
544
|
+
// fix stalled audio, don't build a buffer that can't be played
|
|
514
545
|
audioContext.resume();
|
|
515
546
|
return;
|
|
516
547
|
}
|
|
517
548
|
|
|
518
|
-
|
|
549
|
+
const buffer = createAudioBuffer(sampleChannels, sampleRate);
|
|
550
|
+
return playAudioBuffer(buffer, volume, rate, pan, loop, gainNode, offset, onended);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Copy arrays of samples into a new audio buffer
|
|
554
|
+
* @param {Array} sampleChannels - Array of arrays of samples (for stereo playback)
|
|
555
|
+
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
556
|
+
* @return {AudioBuffer} - The audio buffer holding the samples
|
|
557
|
+
* @memberof Audio */
|
|
558
|
+
function createAudioBuffer(sampleChannels, sampleRate=audioDefaultSampleRate)
|
|
559
|
+
{
|
|
519
560
|
const channelCount = sampleChannels.length;
|
|
520
561
|
const sampleLength = sampleChannels[0].length;
|
|
521
562
|
const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
|
|
522
|
-
const source = audioContext.createBufferSource();
|
|
523
|
-
|
|
524
|
-
// copy samples to buffer and setup source
|
|
525
563
|
sampleChannels.forEach((c,i)=> buffer.getChannelData(i).set(c));
|
|
564
|
+
return buffer;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Play an audio buffer with given settings
|
|
568
|
+
* The buffer can be shared by any number of sounds playing at once
|
|
569
|
+
* @param {AudioBuffer} buffer - The audio buffer to play
|
|
570
|
+
* @param {number} [volume] - How much to scale volume by
|
|
571
|
+
* @param {number} [rate] - The playback rate to use
|
|
572
|
+
* @param {number} [pan] - How much to apply stereo panning
|
|
573
|
+
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
574
|
+
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
575
|
+
* @param {number} [offset] - Offset in seconds to start playback from
|
|
576
|
+
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
577
|
+
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
578
|
+
* @memberof Audio */
|
|
579
|
+
function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode, offset=0, onended)
|
|
580
|
+
{
|
|
581
|
+
if (!soundEnable || headlessMode) return;
|
|
582
|
+
|
|
583
|
+
if (!audioIsRunning())
|
|
584
|
+
{
|
|
585
|
+
// fix stalled audio, this sound won't be able to play
|
|
586
|
+
audioContext.resume();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// setup source, many sources can share one buffer
|
|
591
|
+
const source = audioContext.createBufferSource();
|
|
526
592
|
source.buffer = buffer;
|
|
527
593
|
source.playbackRate.value = rate;
|
|
528
594
|
source.loop = loop;
|