littlejsengine 1.18.27 → 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.
@@ -2587,11 +2587,29 @@ declare module "littlejsengine" {
2587
2587
  randomness: any;
2588
2588
  /** @property {number} - Sample rate for this sound */
2589
2589
  sampleRate: number;
2590
- /** @property {number} - Percentage of this sound currently loaded */
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 */
2591
2599
  loadedPercent: number;
2592
2600
  /** @property {SoundLoadCallback} - function to call when sound is loaded */
2593
2601
  onloadCallback: (sound: Sound) => Sound;
2594
- sampleChannels: any[][];
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;
2595
2613
  /** Play the sound
2596
2614
  * Sounds may not play until a user interaction occurs
2597
2615
  * @param {Vector2} [pos] - World space position to play the sound if any
@@ -2744,6 +2762,25 @@ declare module "littlejsengine" {
2744
2762
  * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
2745
2763
  * @memberof Audio */
2746
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;
2747
2784
  /** Generate and play a ZzFX sound
2748
2785
  *
2749
2786
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.27';
38
+ const engineVersion = '1.18.28';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -6974,7 +6974,15 @@ class Sound
6974
6974
  this.randomness = randomness ?? 0;
6975
6975
  /** @property {number} - Sample rate for this sound */
6976
6976
  this.sampleRate = audioDefaultSampleRate;
6977
- /** @property {number} - Percentage of this sound currently loaded */
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 */
6978
6986
  this.loadedPercent = 0;
6979
6987
  /** @property {SoundLoadCallback} - function to call when sound is loaded */
6980
6988
  this.onloadCallback = onloadCallback;
@@ -6990,8 +6998,10 @@ class Sound
6990
6998
  this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
6991
6999
  zzfxSound[randomnessIndex] = 0;
6992
7000
 
6993
- // 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
6994
7003
  this.sampleChannels = [zzfxG(...zzfxSound)];
7004
+ this.buildSampleBuffer();
6995
7005
  this.loadedPercent = 1;
6996
7006
  onloadCallback?.(this);
6997
7007
  }
@@ -7003,6 +7013,45 @@ class Sound
7003
7013
  }
7004
7014
  }
7005
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
+
7006
7055
  /** Play the sound
7007
7056
  * Sounds may not play until a user interaction occurs
7008
7057
  * @param {Vector2} [pos] - World space position to play the sound if any
@@ -7021,7 +7070,7 @@ class Sound
7021
7070
  ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
7022
7071
 
7023
7072
  if (!soundEnable || headlessMode) return;
7024
- if (!this.sampleChannels) return;
7073
+ if (!this.sampleBuffer && !this._sampleChannels) return;
7025
7074
 
7026
7075
  let pan;
7027
7076
  if (pos)
@@ -7088,7 +7137,7 @@ class Sound
7088
7137
  * @return {number} - How long the sound is in seconds (0 if loading)
7089
7138
  */
7090
7139
  getDuration()
7091
- { return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
7140
+ { return this.sampleLength / this.sampleRate || 0; }
7092
7141
 
7093
7142
  /** Check if sound is loaded, for sounds fetched from a url
7094
7143
  * @return {boolean} - True if sound is loaded and ready to play
@@ -7106,36 +7155,11 @@ class Sound
7106
7155
  const arrayBuffer = await response.arrayBuffer();
7107
7156
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
7108
7157
 
7109
- // convert audio buffer to sample channels across multiple frames
7110
- const channelCount = audioBuffer.numberOfChannels;
7111
- const samplesPerFrame = 1e5;
7112
- const sampleChannels = [];
7113
- for (let channel = 0; channel < channelCount; channel++)
7114
- {
7115
- const channelData = audioBuffer.getChannelData(channel);
7116
- const channelLength = channelData.length;
7117
- sampleChannels[channel] = new Array(channelLength);
7118
- let sampleIndex = 0;
7119
- while (sampleIndex < channelLength)
7120
- {
7121
- // yield to next frame
7122
- await new Promise(resolve => setTimeout(resolve, 0));
7123
-
7124
- // copy chunk of samples
7125
- const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
7126
- for (; sampleIndex < endIndex; sampleIndex++)
7127
- sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
7128
-
7129
- // update loaded percent
7130
- const samplesTotal = channelCount * channelLength;
7131
- const samplesProcessed = channel * channelLength + sampleIndex;
7132
- this.loadedPercent = samplesProcessed / samplesTotal;
7133
- }
7134
- }
7135
-
7136
- // 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
7137
7160
  this.sampleRate = audioBuffer.sampleRate;
7138
- this.sampleChannels = sampleChannels;
7161
+ this.sampleLength = audioBuffer.length;
7162
+ this.sampleBuffer = audioBuffer;
7139
7163
  this.loadedPercent = 1;
7140
7164
  this.onloadCallback?.(this);
7141
7165
  }
@@ -7211,7 +7235,12 @@ class SoundInstance
7211
7235
  if (this.isPlaying())
7212
7236
  this.stop();
7213
7237
  this.gainNode = audioContext.createGain();
7214
- this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
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);
7215
7244
  if (this.source)
7216
7245
  {
7217
7246
  this.startTime = audioContext.currentTime - offset;
@@ -7386,19 +7415,54 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
7386
7415
 
7387
7416
  if (!audioIsRunning())
7388
7417
  {
7389
- // fix stalled audio, this sound won't be able to play
7418
+ // fix stalled audio, don't build a buffer that can't be played
7390
7419
  audioContext.resume();
7391
7420
  return;
7392
7421
  }
7393
7422
 
7394
- // create buffer and source
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
+ {
7395
7434
  const channelCount = sampleChannels.length;
7396
7435
  const sampleLength = sampleChannels[0].length;
7397
7436
  const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
7398
- const source = audioContext.createBufferSource();
7399
-
7400
- // copy samples to buffer and setup source
7401
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();
7402
7466
  source.buffer = buffer;
7403
7467
  source.playbackRate.value = rate;
7404
7468
  source.loop = loop;
@@ -17339,6 +17403,8 @@ export
17339
17403
  speakStop,
17340
17404
  getNoteFrequency,
17341
17405
  playSamples,
17406
+ playAudioBuffer,
17407
+ createAudioBuffer,
17342
17408
  zzfx,
17343
17409
  zzfxG,
17344
17410