@energy8platform/game-engine 0.34.2 → 0.34.3
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/audio.cjs.js +114 -59
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +25 -0
- package/dist/audio.esm.js +114 -59
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +114 -59
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +25 -0
- package/dist/core.esm.js +114 -59
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +114 -59
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +25 -0
- package/dist/host.esm.js +114 -59
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +114 -59
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.esm.js +114 -59
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/audio/AudioManager.ts +111 -53
package/dist/host.cjs.js
CHANGED
|
@@ -753,6 +753,12 @@ class AudioManager {
|
|
|
753
753
|
_categories;
|
|
754
754
|
_masterGain = 1.0;
|
|
755
755
|
_currentMusic = null;
|
|
756
|
+
/** Duck factor (0..1) from duckMusic/unduckMusic. A presentation state, not a player setting. */
|
|
757
|
+
_musicDuck = 1;
|
|
758
|
+
/** Crossfade ramp (0..1) for the track that is fading IN. 1 whenever no fade is running. */
|
|
759
|
+
_musicFade = 1;
|
|
760
|
+
/** Generation counter so a superseded crossfade ramp stops writing over the new track's. */
|
|
761
|
+
_musicFadeToken = 0;
|
|
756
762
|
_unlocked = false;
|
|
757
763
|
_unlockHandler = null;
|
|
758
764
|
constructor(config) {
|
|
@@ -811,7 +817,10 @@ class AudioManager {
|
|
|
811
817
|
if (this._globalMuted || this._categories[category].muted)
|
|
812
818
|
return;
|
|
813
819
|
const { sound } = this._soundModule;
|
|
814
|
-
|
|
820
|
+
// The master gain lives on the GLOBAL bus (`sound.volumeAll`, see applyVolumes) and @pixi/sound
|
|
821
|
+
// already multiplies it in — folding it in here as well squared it, so a master of 0.5 played
|
|
822
|
+
// sfx at 0.25.
|
|
823
|
+
const vol = (options?.volume ?? 1) * this._categories[category].volume;
|
|
815
824
|
try {
|
|
816
825
|
sound.play(alias, {
|
|
817
826
|
volume: vol,
|
|
@@ -833,52 +842,50 @@ class AudioManager {
|
|
|
833
842
|
if (!this._initialized || !this._soundModule)
|
|
834
843
|
return;
|
|
835
844
|
const { sound } = this._soundModule;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
});
|
|
849
|
-
// Start new track at zero volume, fade in
|
|
850
|
-
try {
|
|
851
|
-
sound.play(alias, {
|
|
852
|
-
volume: 0,
|
|
853
|
-
loop: true,
|
|
845
|
+
const prevAlias = this._currentMusic;
|
|
846
|
+
const crossfade = !!prevAlias && prevAlias !== alias && fadeDuration > 0;
|
|
847
|
+
// Retire the outgoing track. Its own SOUND-level volume is the only thing still pointing at it,
|
|
848
|
+
// so fading that to 0 is safe — nothing else writes it once `_currentMusic` has moved on.
|
|
849
|
+
if (prevAlias) {
|
|
850
|
+
if (crossfade) {
|
|
851
|
+
const from = this.soundVolumeOf(prevAlias);
|
|
852
|
+
this.fadeVolume(prevAlias, from, 0, fadeDuration, () => {
|
|
853
|
+
try {
|
|
854
|
+
sound.stop(prevAlias);
|
|
855
|
+
}
|
|
856
|
+
catch { /* ignore */ }
|
|
854
857
|
});
|
|
855
|
-
this.fadeVolume(alias, 0, this._categories.music.volume * this._masterGain, fadeDuration);
|
|
856
858
|
}
|
|
857
|
-
|
|
858
|
-
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
else {
|
|
862
|
-
// No crossfade — instant switch
|
|
863
|
-
if (this._currentMusic) {
|
|
859
|
+
else {
|
|
864
860
|
try {
|
|
865
|
-
sound.stop(
|
|
861
|
+
sound.stop(prevAlias);
|
|
866
862
|
}
|
|
867
863
|
catch { /* ignore */ }
|
|
868
864
|
}
|
|
869
|
-
this._currentMusic = alias;
|
|
870
|
-
if (this._globalMuted || this._categories.music.muted)
|
|
871
|
-
return;
|
|
872
|
-
try {
|
|
873
|
-
sound.play(alias, {
|
|
874
|
-
volume: this._categories.music.volume * this._masterGain,
|
|
875
|
-
loop: true,
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
catch (e) {
|
|
879
|
-
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
880
|
-
}
|
|
881
865
|
}
|
|
866
|
+
this._currentMusic = alias;
|
|
867
|
+
this._musicFadeToken++; // any ramp still running belongs to a track we just replaced
|
|
868
|
+
// Deliberately started even while muted. Global mute is the @pixi/sound CONTEXT mute and a
|
|
869
|
+
// muted music category is a 0 term in `musicGain()` — both already make this inaudible, and
|
|
870
|
+
// both undo themselves the moment the player flips them back. Returning early here instead
|
|
871
|
+
// meant a track begun while muted never existed, so unmuting restored silence until some
|
|
872
|
+
// later mode change happened to switch tracks.
|
|
873
|
+
// The incoming track plays at INSTANCE volume 1 and carries its whole gain on the SOUND layer
|
|
874
|
+
// (`musicGain()`), which is the layer the slider, the duck and this fade all write. Splitting
|
|
875
|
+
// them across layers is what silenced every crossfade: the track was started at instance volume
|
|
876
|
+
// 0 and the ramp then moved the sound layer, whose product with 0 is 0 for the track's life.
|
|
877
|
+
// The gain is written BEFORE play() so the first frame is never at full volume.
|
|
878
|
+
this._musicFade = crossfade ? 0 : 1;
|
|
879
|
+
this.applyMusicGain();
|
|
880
|
+
try {
|
|
881
|
+
sound.play(alias, { volume: 1, loop: true });
|
|
882
|
+
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (crossfade)
|
|
888
|
+
this.rampMusicFade(fadeDuration);
|
|
882
889
|
}
|
|
883
890
|
/**
|
|
884
891
|
* Stop current music.
|
|
@@ -894,6 +901,10 @@ class AudioManager {
|
|
|
894
901
|
// ignore
|
|
895
902
|
}
|
|
896
903
|
this._currentMusic = null;
|
|
904
|
+
// Retire any running ramp and clear the fade term, so the next track does not inherit a
|
|
905
|
+
// half-finished crossfade and start silent.
|
|
906
|
+
this._musicFadeToken++;
|
|
907
|
+
this._musicFade = 1;
|
|
897
908
|
}
|
|
898
909
|
/**
|
|
899
910
|
* Stop all sounds.
|
|
@@ -919,6 +930,9 @@ class AudioManager {
|
|
|
919
930
|
*/
|
|
920
931
|
setVolume(category, volume) {
|
|
921
932
|
this._categories[category].volume = Math.max(0, Math.min(1, volume));
|
|
933
|
+
// applyVolumes() re-pushes the music gain, so moving the Music slider is heard on the track
|
|
934
|
+
// that is ALREADY playing — it used to take effect only at the next playMusic (a mode change).
|
|
935
|
+
// SFX need no push: play() reads the category volume fresh on every call.
|
|
922
936
|
this.applyVolumes();
|
|
923
937
|
this.saveState();
|
|
924
938
|
}
|
|
@@ -991,30 +1005,19 @@ class AudioManager {
|
|
|
991
1005
|
* @param factor - Volume multiplier (0..1), e.g. 0.3 = 30% of normal
|
|
992
1006
|
*/
|
|
993
1007
|
duckMusic(factor) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
// ignore
|
|
1003
|
-
}
|
|
1008
|
+
// Held as a FACTOR rather than written as a finished volume: the duck used to write
|
|
1009
|
+
// `category × factor` onto a track whose instance already carried the category volume, so it
|
|
1010
|
+
// ducked to category², and unducking restored category² instead of category. Keeping it as one
|
|
1011
|
+
// term of `musicGain()` also keeps the slider live while ducked.
|
|
1012
|
+
this._musicDuck = Math.max(0, Math.min(1, factor));
|
|
1013
|
+
this.applyMusicGain();
|
|
1004
1014
|
}
|
|
1005
1015
|
/**
|
|
1006
1016
|
* Restore music to normal volume after ducking.
|
|
1007
1017
|
*/
|
|
1008
1018
|
unduckMusic() {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const { sound } = this._soundModule;
|
|
1012
|
-
try {
|
|
1013
|
-
sound.volume(this._currentMusic, this._categories.music.volume);
|
|
1014
|
-
}
|
|
1015
|
-
catch {
|
|
1016
|
-
// ignore
|
|
1017
|
-
}
|
|
1019
|
+
this._musicDuck = 1;
|
|
1020
|
+
this.applyMusicGain();
|
|
1018
1021
|
}
|
|
1019
1022
|
/**
|
|
1020
1023
|
* Destroy the audio manager and free resources.
|
|
@@ -1053,6 +1056,57 @@ class AudioManager {
|
|
|
1053
1056
|
};
|
|
1054
1057
|
requestAnimationFrame(tick);
|
|
1055
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* The SOUND-layer gain for the running music track.
|
|
1061
|
+
*
|
|
1062
|
+
* @pixi/sound resolves a playing instance as `instance × sound × global` (WebAudioInstance.
|
|
1063
|
+
* refresh). Each of those three has exactly ONE owner here, which is what keeps the mixer honest:
|
|
1064
|
+
* global — the master gain (`applyVolumes`)
|
|
1065
|
+
* sound — music: this function. sfx: untouched, left at 1.
|
|
1066
|
+
* instance — sfx: the per-call volume × the sfx category. music: always 1.
|
|
1067
|
+
* Everything that can move music volume — the player's slider, the category mute, a big-win duck,
|
|
1068
|
+
* a crossfade — is a term below, so they compose instead of overwriting each other.
|
|
1069
|
+
*/
|
|
1070
|
+
musicGain() {
|
|
1071
|
+
const c = this._categories.music;
|
|
1072
|
+
return (c.muted ? 0 : 1) * c.volume * this._musicDuck * this._musicFade;
|
|
1073
|
+
}
|
|
1074
|
+
/** Push `musicGain()` at the current track. Safe before it starts playing and with none playing. */
|
|
1075
|
+
applyMusicGain() {
|
|
1076
|
+
if (!this._soundModule || !this._currentMusic)
|
|
1077
|
+
return;
|
|
1078
|
+
try {
|
|
1079
|
+
this._soundModule.sound.volume(this._currentMusic, this.musicGain());
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
// ignore — alias not registered yet
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/** Current SOUND-layer volume of `alias`, or 0 when it cannot be read. */
|
|
1086
|
+
soundVolumeOf(alias) {
|
|
1087
|
+
try {
|
|
1088
|
+
return Number(this._soundModule.sound.volume(alias)) || 0;
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
return 0;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/** Ramp the crossfade term 0 → 1 over `durationMs`, recomposing the gain each frame so a slider
|
|
1095
|
+
* drag or a duck landing mid-fade is honoured rather than overwritten when the fade ends. */
|
|
1096
|
+
rampMusicFade(durationMs) {
|
|
1097
|
+
const token = this._musicFadeToken;
|
|
1098
|
+
const start = Date.now();
|
|
1099
|
+
const tick = () => {
|
|
1100
|
+
if (token !== this._musicFadeToken)
|
|
1101
|
+
return; // a newer track owns the music now
|
|
1102
|
+
const t = Math.min((Date.now() - start) / durationMs, 1);
|
|
1103
|
+
this._musicFade = t;
|
|
1104
|
+
this.applyMusicGain();
|
|
1105
|
+
if (t < 1)
|
|
1106
|
+
requestAnimationFrame(tick);
|
|
1107
|
+
};
|
|
1108
|
+
requestAnimationFrame(tick);
|
|
1109
|
+
}
|
|
1056
1110
|
applyVolumes() {
|
|
1057
1111
|
if (!this._soundModule)
|
|
1058
1112
|
return;
|
|
@@ -1060,6 +1114,7 @@ class AudioManager {
|
|
|
1060
1114
|
// Global mute is owned by sound.muteAll()/unmuteAll() (context.muted),
|
|
1061
1115
|
// not by volumeAll — mixing both leaves mute un-undoable after reload.
|
|
1062
1116
|
sound.volumeAll = this._masterGain; // master multiplies the global bus
|
|
1117
|
+
this.applyMusicGain(); // category volume/mute reach the RUNNING track
|
|
1063
1118
|
}
|
|
1064
1119
|
setupMobileUnlock() {
|
|
1065
1120
|
if (this._unlocked)
|