excalibur 0.32.0-alpha.1600 → 0.32.0-alpha.17

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/CHANGELOG.md CHANGED
@@ -17,6 +17,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
17
17
  ### Added
18
18
 
19
19
 
20
+ - Added new parameter to `ex.Sounds` to schedule start time, this allows you to synchronize playback of multiple audio tracks
21
+ ```typescript
22
+ const start500MsFromNow = AudioContextFactory.currentTime() + 500;
23
+
24
+ Resources.MusicSurface.play({ volume: .5, scheduledStartTime: start500MsFromNow });
25
+ // Start layered tracks at 0 volume so they are synchronized
26
+ Resources.MusicIndDrums.play({ volume: 0, scheduledStartTime: start500MsFromNow });
27
+ Resources.MusicIndTopper.play({ volume: 0, scheduledStartTime: start500MsFromNow });
28
+ Resources.MusicGroovyDrums.play({ volume: 0, scheduledStartTime: start500MsFromNow });
29
+ Resources.MusicGroovyTopper.play({ volume: 0, scheduledStartTime: start500MsFromNow });
30
+ ```
20
31
  - Added new Timer events!
21
32
  ```typescript
22
33
  const timer = new ex.Timer({...});
@@ -4,4 +4,8 @@
4
4
  export declare class AudioContextFactory {
5
5
  private static _INSTANCE?;
6
6
  static create(): AudioContext;
7
+ /**
8
+ * Return the current audio context time in millseconds
9
+ */
10
+ static currentTime(): number;
7
11
  }
@@ -1,5 +1,6 @@
1
1
  import type { Audio } from '../../Interfaces/Audio';
2
2
  import type { Engine } from '../../Engine';
3
+ import { WebAudioInstance } from './WebAudioInstance';
3
4
  import { NativeSoundEvent, NativeSoundProcessedEvent } from '../../Events/MediaEvents';
4
5
  import type { Loadable } from '../../Interfaces/Index';
5
6
  import { Logger } from '../../Util/Log';
@@ -61,6 +62,26 @@ export interface SoundOptions {
61
62
  */
62
63
  position?: number;
63
64
  }
65
+ export interface PlayOptions {
66
+ /**
67
+ * Volume to play between [0, 1]
68
+ */
69
+ volume?: number;
70
+ /**
71
+ * Schedule time to play in milliseconds from the audio context origin
72
+ *
73
+ * Compute using the audio context
74
+ *
75
+ * ```typescript
76
+ * const sound: Sound = ...;
77
+ * const oneThousandMillisecondsFromNow = AudioContextFactory.currentTime + 1000;
78
+ *
79
+ * sound.play({ scheduledStartTime: oneThousandMillisecondsFromNow });
80
+ *
81
+ * ```
82
+ */
83
+ scheduledStartTime?: number;
84
+ }
64
85
  /**
65
86
  * The {@apilink Sound} object allows games built in Excalibur to load audio
66
87
  * components, from soundtracks to sound effects. {@apilink Sound} is an {@apilink Loadable}
@@ -105,6 +126,20 @@ export declare class Sound implements Audio, Loadable<AudioBuffer> {
105
126
  */
106
127
  get bustCache(): boolean;
107
128
  set bustCache(val: boolean);
129
+ /**
130
+ * Schedule time to play in milliseconds from the audio context origin
131
+ *
132
+ * Compute using the audio context
133
+ *
134
+ * ```typescript
135
+ * const sound: Sound = ...;
136
+ * const oneThousandMillisecondsFromNow = AudioContextFactory.currentTime + 1000;
137
+ *
138
+ * sound.scheduledStartTime = oneThousandMillisecondsFromNow;
139
+ *
140
+ * ```
141
+ */
142
+ scheduledStartTime: number;
108
143
  private _loop;
109
144
  private _volume;
110
145
  private _isStopped;
@@ -136,7 +171,7 @@ export declare class Sound implements Audio, Loadable<AudioBuffer> {
136
171
  * Play the sound, returns a promise that resolves when the sound is done playing
137
172
  * An optional volume argument can be passed in to play the sound. Max volume is 1.0
138
173
  */
139
- play(volume?: number): Promise<boolean>;
174
+ play(volumeOrConfig?: number | PlayOptions): Promise<boolean>;
140
175
  /**
141
176
  * Stop the sound, and do not rewind
142
177
  */
@@ -160,7 +195,7 @@ export declare class Sound implements Audio, Loadable<AudioBuffer> {
160
195
  * Get Id of provided AudioInstance in current trackList
161
196
  * @param track {@apilink Audio} which Id is to be given
162
197
  */
163
- getTrackId(track: Audio): number;
198
+ getTrackId(track: WebAudioInstance): number;
164
199
  private _resumePlayback;
165
200
  /**
166
201
  * Starts playback, returns a promise that resolves when playback is complete
@@ -45,4 +45,5 @@ export declare class WebAudioInstance implements Audio {
45
45
  private _playbackRate;
46
46
  set playbackRate(playbackRate: number);
47
47
  get playbackRate(): number;
48
+ scheduledStartTime: number;
48
49
  }
@@ -1,4 +1,4 @@
1
- /*! excalibur - 0.32.0-alpha.1600+ebc4aa2 - 2025-12-19
1
+ /*! excalibur - 0.32.0-alpha.17+d1b9643 - 2025-12-22
2
2
  https://github.com/excaliburjs/Excalibur
3
3
  Copyright (c) 2025 Excalibur.js <https://github.com/excaliburjs/Excalibur/graphs/contributors>
4
4
  Licensed BSD-2-Clause
@@ -23444,6 +23444,12 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
23444
23444
  }
23445
23445
  return this._INSTANCE;
23446
23446
  }
23447
+ /**
23448
+ * Return the current audio context time in millseconds
23449
+ */
23450
+ static currentTime() {
23451
+ return AudioContextFactory.create().currentTime * 1e3;
23452
+ }
23447
23453
  }
23448
23454
  function isLegacyWebAudioSource(source) {
23449
23455
  return !!source.playbackState;
@@ -23606,9 +23612,9 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
23606
23612
  this._createNewBufferSource();
23607
23613
  this._handleEnd();
23608
23614
  if (this.loop) {
23609
- this._instance.start(0, data.pausedAt * this._playbackRate);
23615
+ this._instance.start(this.scheduledStartTime, data.pausedAt * this._playbackRate);
23610
23616
  } else {
23611
- this._instance.start(0, data.pausedAt * this._playbackRate, this.duration);
23617
+ this._instance.start(this.scheduledStartTime, data.pausedAt * this._playbackRate, this.duration);
23612
23618
  }
23613
23619
  data.startedAt = this._audioContext.currentTime - data.pausedAt;
23614
23620
  data.pausedAt = 0;
@@ -23658,6 +23664,7 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
23658
23664
  this._playStarted = () => {
23659
23665
  };
23660
23666
  this._playbackRate = 1;
23667
+ this.scheduledStartTime = 0;
23661
23668
  this._createNewBufferSource();
23662
23669
  }
23663
23670
  _createNewBufferSource() {
@@ -23855,6 +23862,7 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
23855
23862
  constructor(...pathsOrSoundOption) {
23856
23863
  this.events = new EventEmitter();
23857
23864
  this.logger = Logger.getInstance();
23865
+ this.scheduledStartTime = 0;
23858
23866
  this._loop = false;
23859
23867
  this._volume = 1;
23860
23868
  this._isStopped = false;
@@ -24023,7 +24031,7 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
24023
24031
  * Play the sound, returns a promise that resolves when the sound is done playing
24024
24032
  * An optional volume argument can be passed in to play the sound. Max volume is 1.0
24025
24033
  */
24026
- play(volume) {
24034
+ play(volumeOrConfig) {
24027
24035
  if (!this.isLoaded()) {
24028
24036
  this.logger.warn("Cannot start playing. Resource", this.path, "is not loaded yet");
24029
24037
  return Promise.resolve(true);
@@ -24032,14 +24040,21 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
24032
24040
  this.logger.warn("Cannot start playing. Engine is in a stopped state.");
24033
24041
  return Promise.resolve(false);
24034
24042
  }
24035
- this.volume = volume != null ? volume : this.volume;
24043
+ let scheduledStart = 0;
24044
+ if (volumeOrConfig instanceof Object) {
24045
+ const { volume, scheduledStartTime } = volumeOrConfig;
24046
+ scheduledStart = (scheduledStartTime != null ? scheduledStartTime : 0) / 1e3 || scheduledStart;
24047
+ this.volume = volume != null ? volume : this.volume;
24048
+ } else {
24049
+ this.volume = volumeOrConfig != null ? volumeOrConfig : this.volume;
24050
+ }
24036
24051
  if (this.isPaused()) {
24037
24052
  return this._resumePlayback();
24038
24053
  } else {
24039
24054
  if (this.position) {
24040
24055
  this.seek(this.position);
24041
24056
  }
24042
- return this._startPlayback();
24057
+ return this._startPlayback(scheduledStart);
24043
24058
  }
24044
24059
  }
24045
24060
  /**
@@ -24109,10 +24124,11 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
24109
24124
  getTrackId(track) {
24110
24125
  return this._tracks.indexOf(track);
24111
24126
  }
24112
- async _resumePlayback() {
24127
+ async _resumePlayback(scheduledStart = 0) {
24113
24128
  if (this.isPaused()) {
24114
24129
  const resumed = [];
24115
24130
  for (const track of this._tracks) {
24131
+ track.scheduledStartTime = scheduledStart;
24116
24132
  resumed.push(
24117
24133
  track.play().then(() => {
24118
24134
  this._tracks.splice(this.getTrackId(track), 1);
@@ -24129,8 +24145,9 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
24129
24145
  /**
24130
24146
  * Starts playback, returns a promise that resolves when playback is complete
24131
24147
  */
24132
- async _startPlayback() {
24148
+ async _startPlayback(scheduledStartTime = 0) {
24133
24149
  const track = this._getTrackInstance(this.data);
24150
+ track.scheduledStartTime = scheduledStartTime;
24134
24151
  const complete = await track.play(() => {
24135
24152
  this.events.emit("playbackstart", new NativeSoundEvent(this, track));
24136
24153
  this.logger.debug("Playing new instance for sound", this.path);
@@ -33614,7 +33631,7 @@ Read more about this issue at https://excaliburjs.com/docs/performance`
33614
33631
  this._count += count;
33615
33632
  }
33616
33633
  }
33617
- const EX_VERSION = "0.32.0-alpha.1600+ebc4aa2";
33634
+ const EX_VERSION = "0.32.0-alpha.17+d1b9643";
33618
33635
  polyfill();
33619
33636
  exports2.ActionCompleteEvent = ActionCompleteEvent;
33620
33637
  exports2.ActionContext = ActionContext;
@@ -1,4 +1,4 @@
1
- /*! excalibur - 0.32.0-alpha.1600+ebc4aa2 - 2025-12-19
1
+ /*! excalibur - 0.32.0-alpha.17+d1b9643 - 2025-12-22
2
2
  https://github.com/excaliburjs/Excalibur
3
3
  Copyright (c) 2025 Excalibur.js <https://github.com/excaliburjs/Excalibur/graphs/contributors>
4
4
  Licensed BSD-2-Clause
@@ -23444,6 +23444,12 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
23444
23444
  }
23445
23445
  return this._INSTANCE;
23446
23446
  }
23447
+ /**
23448
+ * Return the current audio context time in millseconds
23449
+ */
23450
+ static currentTime() {
23451
+ return AudioContextFactory.create().currentTime * 1e3;
23452
+ }
23447
23453
  }
23448
23454
  function isLegacyWebAudioSource(source) {
23449
23455
  return !!source.playbackState;
@@ -23606,9 +23612,9 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
23606
23612
  this._createNewBufferSource();
23607
23613
  this._handleEnd();
23608
23614
  if (this.loop) {
23609
- this._instance.start(0, data.pausedAt * this._playbackRate);
23615
+ this._instance.start(this.scheduledStartTime, data.pausedAt * this._playbackRate);
23610
23616
  } else {
23611
- this._instance.start(0, data.pausedAt * this._playbackRate, this.duration);
23617
+ this._instance.start(this.scheduledStartTime, data.pausedAt * this._playbackRate, this.duration);
23612
23618
  }
23613
23619
  data.startedAt = this._audioContext.currentTime - data.pausedAt;
23614
23620
  data.pausedAt = 0;
@@ -23658,6 +23664,7 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
23658
23664
  this._playStarted = () => {
23659
23665
  };
23660
23666
  this._playbackRate = 1;
23667
+ this.scheduledStartTime = 0;
23661
23668
  this._createNewBufferSource();
23662
23669
  }
23663
23670
  _createNewBufferSource() {
@@ -23855,6 +23862,7 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
23855
23862
  constructor(...pathsOrSoundOption) {
23856
23863
  this.events = new EventEmitter();
23857
23864
  this.logger = Logger.getInstance();
23865
+ this.scheduledStartTime = 0;
23858
23866
  this._loop = false;
23859
23867
  this._volume = 1;
23860
23868
  this._isStopped = false;
@@ -24023,7 +24031,7 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
24023
24031
  * Play the sound, returns a promise that resolves when the sound is done playing
24024
24032
  * An optional volume argument can be passed in to play the sound. Max volume is 1.0
24025
24033
  */
24026
- play(volume) {
24034
+ play(volumeOrConfig) {
24027
24035
  if (!this.isLoaded()) {
24028
24036
  this.logger.warn("Cannot start playing. Resource", this.path, "is not loaded yet");
24029
24037
  return Promise.resolve(true);
@@ -24032,14 +24040,21 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
24032
24040
  this.logger.warn("Cannot start playing. Engine is in a stopped state.");
24033
24041
  return Promise.resolve(false);
24034
24042
  }
24035
- this.volume = volume != null ? volume : this.volume;
24043
+ let scheduledStart = 0;
24044
+ if (volumeOrConfig instanceof Object) {
24045
+ const { volume, scheduledStartTime } = volumeOrConfig;
24046
+ scheduledStart = (scheduledStartTime != null ? scheduledStartTime : 0) / 1e3 || scheduledStart;
24047
+ this.volume = volume != null ? volume : this.volume;
24048
+ } else {
24049
+ this.volume = volumeOrConfig != null ? volumeOrConfig : this.volume;
24050
+ }
24036
24051
  if (this.isPaused()) {
24037
24052
  return this._resumePlayback();
24038
24053
  } else {
24039
24054
  if (this.position) {
24040
24055
  this.seek(this.position);
24041
24056
  }
24042
- return this._startPlayback();
24057
+ return this._startPlayback(scheduledStart);
24043
24058
  }
24044
24059
  }
24045
24060
  /**
@@ -24109,10 +24124,11 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
24109
24124
  getTrackId(track) {
24110
24125
  return this._tracks.indexOf(track);
24111
24126
  }
24112
- async _resumePlayback() {
24127
+ async _resumePlayback(scheduledStart = 0) {
24113
24128
  if (this.isPaused()) {
24114
24129
  const resumed = [];
24115
24130
  for (const track of this._tracks) {
24131
+ track.scheduledStartTime = scheduledStart;
24116
24132
  resumed.push(
24117
24133
  track.play().then(() => {
24118
24134
  this._tracks.splice(this.getTrackId(track), 1);
@@ -24129,8 +24145,9 @@ If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPost
24129
24145
  /**
24130
24146
  * Starts playback, returns a promise that resolves when playback is complete
24131
24147
  */
24132
- async _startPlayback() {
24148
+ async _startPlayback(scheduledStartTime = 0) {
24133
24149
  const track = this._getTrackInstance(this.data);
24150
+ track.scheduledStartTime = scheduledStartTime;
24134
24151
  const complete = await track.play(() => {
24135
24152
  this.events.emit("playbackstart", new NativeSoundEvent(this, track));
24136
24153
  this.logger.debug("Playing new instance for sound", this.path);
@@ -33614,7 +33631,7 @@ Read more about this issue at https://excaliburjs.com/docs/performance`
33614
33631
  this._count += count;
33615
33632
  }
33616
33633
  }
33617
- const EX_VERSION = "0.32.0-alpha.1600+ebc4aa2";
33634
+ const EX_VERSION = "0.32.0-alpha.17+d1b9643";
33618
33635
  polyfill();
33619
33636
  exports2.ActionCompleteEvent = ActionCompleteEvent;
33620
33637
  exports2.ActionContext = ActionContext;