Checkpoint before line-grid scrolling refactor
This commit is contained in:
@@ -22,6 +22,7 @@ class AudioManagerModule extends BaseModule {
|
||||
this.activeTtsPlaybackCount = 0;
|
||||
this.ttsQueueEmpty = true;
|
||||
this.pendingMusicPlayback = null;
|
||||
this.currentMusicState = null;
|
||||
this.assetRoots = {
|
||||
images: '/images/',
|
||||
music: '/music/',
|
||||
@@ -203,14 +204,14 @@ class AudioManagerModule extends BaseModule {
|
||||
this.currentLoop = audio;
|
||||
} else {
|
||||
this.currentAudio = audio.cloneNode(true);
|
||||
this.currentAudio.volume = this.getSfxVolume();
|
||||
this.setMediaVolume(this.currentAudio, this.getSfxVolume());
|
||||
this.currentAudio.play().catch(error => {
|
||||
console.error('Error playing audio:', error);
|
||||
});
|
||||
return this.currentAudio;
|
||||
}
|
||||
|
||||
audio.volume = this.getMusicVolume();
|
||||
this.setMediaVolume(audio, this.getMusicVolume());
|
||||
audio.play().catch(error => {
|
||||
console.error('Error playing audio:', error);
|
||||
});
|
||||
@@ -233,14 +234,14 @@ class AudioManagerModule extends BaseModule {
|
||||
}
|
||||
this.currentLoop = new Audio(url);
|
||||
this.currentLoop.loop = true;
|
||||
this.currentLoop.volume = this.getMusicVolume();
|
||||
this.setMediaVolume(this.currentLoop, this.getMusicVolume());
|
||||
this.currentLoop.play().catch(error => {
|
||||
console.error('Error playing audio loop:', error);
|
||||
});
|
||||
return this.currentLoop;
|
||||
} else {
|
||||
this.currentAudio = new Audio(url);
|
||||
this.currentAudio.volume = this.getSfxVolume();
|
||||
this.setMediaVolume(this.currentAudio, this.getSfxVolume());
|
||||
this.currentAudio.play().catch(error => {
|
||||
console.error('Error playing audio:', error);
|
||||
});
|
||||
@@ -331,19 +332,19 @@ class AudioManagerModule extends BaseModule {
|
||||
updateVolumes() {
|
||||
this.sounds.forEach(audio => {
|
||||
const isMusic = audio.loop;
|
||||
audio.volume = this.masterVolume * (isMusic ? this.musicVolume : this.sfxVolume);
|
||||
this.setMediaVolume(audio, this.masterVolume * (isMusic ? this.musicVolume : this.sfxVolume));
|
||||
});
|
||||
|
||||
if (this.currentAudio) {
|
||||
this.currentAudio.volume = this.masterVolume * this.sfxVolume;
|
||||
this.setMediaVolume(this.currentAudio, this.masterVolume * this.sfxVolume);
|
||||
}
|
||||
|
||||
if (this.currentLoop) {
|
||||
this.currentLoop.volume = this.getMusicVolume();
|
||||
this.setMediaVolume(this.currentLoop, this.getMusicVolume());
|
||||
}
|
||||
|
||||
if (this.currentMusic) {
|
||||
this.currentMusic.volume = this.getMusicVolume();
|
||||
this.setMediaVolume(this.currentMusic, this.getMusicVolume());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +352,11 @@ class AudioManagerModule extends BaseModule {
|
||||
return Math.max(0, Math.min(1, Number.isFinite(Number(volume)) ? Number(volume) : 1));
|
||||
}
|
||||
|
||||
setMediaVolume(audio, volume) {
|
||||
if (!audio) return;
|
||||
audio.volume = this.clampVolume(volume);
|
||||
}
|
||||
|
||||
getSfxVolume() {
|
||||
return this.masterVolume * this.sfxVolume;
|
||||
}
|
||||
@@ -387,8 +393,8 @@ class AudioManagerModule extends BaseModule {
|
||||
|
||||
const audio = this.currentMusic;
|
||||
const token = ++this.musicFadeToken;
|
||||
const startVolume = audio.volume;
|
||||
const targetVolume = this.getUnduckedMusicVolume() * this.musicDuckingFactor;
|
||||
const startVolume = this.clampVolume(audio.volume);
|
||||
const targetVolume = this.clampVolume(this.getUnduckedMusicVolume() * this.musicDuckingFactor);
|
||||
const start = performance.now();
|
||||
|
||||
const tick = () => {
|
||||
@@ -396,7 +402,7 @@ class AudioManagerModule extends BaseModule {
|
||||
return;
|
||||
}
|
||||
const progress = Math.min(1, (performance.now() - start) / duration);
|
||||
audio.volume = startVolume + ((targetVolume - startVolume) * progress);
|
||||
this.setMediaVolume(audio, startVolume + ((targetVolume - startVolume) * progress));
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
@@ -428,7 +434,7 @@ class AudioManagerModule extends BaseModule {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const audio = new Audio(url);
|
||||
audio.preload = 'auto';
|
||||
audio.volume = this.getSfxVolume();
|
||||
this.setMediaVolume(audio, this.getSfxVolume());
|
||||
audio.addEventListener('canplaythrough', () => resolve(audio), { once: true });
|
||||
audio.addEventListener('error', () => reject(new Error(`Failed to preload sound effect: ${url}`)), { once: true });
|
||||
audio.load();
|
||||
@@ -556,7 +562,7 @@ class AudioManagerModule extends BaseModule {
|
||||
try {
|
||||
const template = await this.preloadSfx(filename);
|
||||
const audio = template.cloneNode(true);
|
||||
audio.volume = this.getSfxVolume();
|
||||
this.setMediaVolume(audio, this.getSfxVolume());
|
||||
this.currentAudio = audio;
|
||||
const maxDuration = Math.max(0, Number(options.maxDurationSeconds || options.maxDuration || 0)) * 1000;
|
||||
const endMode = String(options.endMode || options.mode || 'stop').toLowerCase().startsWith('fade') ? 'fade' : 'stop';
|
||||
@@ -595,12 +601,12 @@ class AudioManagerModule extends BaseModule {
|
||||
|
||||
fadeOutAudio(audio, duration = 1000) {
|
||||
if (!audio) return Promise.resolve(false);
|
||||
const startVolume = audio.volume;
|
||||
const startVolume = this.clampVolume(audio.volume);
|
||||
const startedAt = performance.now();
|
||||
return new Promise(resolve => {
|
||||
const step = () => {
|
||||
const progress = Math.min(1, (performance.now() - startedAt) / duration);
|
||||
audio.volume = startVolume * (1 - progress);
|
||||
this.setMediaVolume(audio, startVolume * (1 - progress));
|
||||
if (progress < 1 && !audio.paused && !audio.ended) {
|
||||
requestAnimationFrame(step);
|
||||
return;
|
||||
@@ -617,6 +623,8 @@ class AudioManagerModule extends BaseModule {
|
||||
async playMusic(filename, mode = 'crossfade', options = {}) {
|
||||
const url = this.getAssetUrl('music', filename);
|
||||
const shouldLoop = options.loop !== false;
|
||||
const startAt = Math.max(0, Number(options.startAt ?? options.currentTime ?? 0) || 0);
|
||||
const fadeInSeconds = Math.max(0, Number(options.fadeInSeconds ?? options.fadeIn ?? 0) || 0);
|
||||
|
||||
if (mode === 'queue' && this.currentMusic && !this.currentMusic.paused) {
|
||||
this.queuedMusic = { filename, mode: 'cut', options: { loop: shouldLoop } };
|
||||
@@ -631,22 +639,44 @@ class AudioManagerModule extends BaseModule {
|
||||
|
||||
const next = new Audio(url);
|
||||
next.loop = shouldLoop;
|
||||
next.volume = mode === 'crossfade' && this.currentMusic ? 0 : this.getMusicVolume();
|
||||
this.setMediaVolume(next, (mode === 'crossfade' && this.currentMusic) || fadeInSeconds > 0 ? 0 : this.getMusicVolume());
|
||||
if (startAt > 0) {
|
||||
try {
|
||||
next.currentTime = startAt;
|
||||
} catch {
|
||||
next.addEventListener('loadedmetadata', () => {
|
||||
next.currentTime = Math.min(startAt, Number.isFinite(next.duration) ? next.duration : startAt);
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
next.addEventListener('ended', () => {
|
||||
if (this.currentMusic === next) {
|
||||
this.currentMusic = null;
|
||||
this.currentMusicState = null;
|
||||
}
|
||||
});
|
||||
const nextState = {
|
||||
filename,
|
||||
url,
|
||||
loop: shouldLoop,
|
||||
mode,
|
||||
startedAt: Date.now()
|
||||
};
|
||||
|
||||
if (mode === 'cut' || !this.currentMusic) {
|
||||
this.stopCurrentMusic();
|
||||
this.currentMusic = next;
|
||||
this.currentMusicState = nextState;
|
||||
await this.startMusicAudio(next, filename);
|
||||
if (fadeInSeconds > 0) {
|
||||
this.fadeAudioTo(next, this.getMusicVolume(), fadeInSeconds * 1000);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
const previous = this.currentMusic;
|
||||
this.currentMusic = next;
|
||||
this.currentMusicState = nextState;
|
||||
await this.startMusicAudio(next, filename);
|
||||
this.crossfade(previous, next, 1500);
|
||||
console.log(`AudioManager: Crossfading music to ${filename}`);
|
||||
@@ -678,7 +708,7 @@ class AudioManagerModule extends BaseModule {
|
||||
|
||||
const pending = this.pendingMusicPlayback;
|
||||
this.pendingMusicPlayback = null;
|
||||
pending.audio.volume = this.getMusicVolume();
|
||||
this.setMediaVolume(pending.audio, this.getMusicVolume());
|
||||
|
||||
try {
|
||||
await pending.audio.play();
|
||||
@@ -697,17 +727,61 @@ class AudioManagerModule extends BaseModule {
|
||||
this.currentMusic.pause();
|
||||
this.currentMusic.currentTime = 0;
|
||||
this.currentMusic = null;
|
||||
this.currentMusicState = null;
|
||||
}
|
||||
|
||||
getMusicState() {
|
||||
if (!this.currentMusic || this.currentMusic.paused || this.currentMusic.ended || !this.currentMusicState?.filename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
filename: this.currentMusicState.filename,
|
||||
currentTime: Math.max(0, Number(this.currentMusic.currentTime || 0)),
|
||||
loop: Boolean(this.currentMusic.loop),
|
||||
mode: this.currentMusicState.mode || 'cut',
|
||||
volume: this.currentMusic.volume,
|
||||
duckingFactor: this.musicDuckingFactor
|
||||
};
|
||||
}
|
||||
|
||||
async restoreMusicState(state = null) {
|
||||
if (!state?.filename) return null;
|
||||
return this.playMusic(state.filename, 'cut', {
|
||||
loop: state.loop !== false,
|
||||
startAt: Number(state.currentTime || 0),
|
||||
fadeInSeconds: 1.5
|
||||
});
|
||||
}
|
||||
|
||||
fadeAudioTo(audio, targetVolume, duration = 1000) {
|
||||
if (!audio) return Promise.resolve(false);
|
||||
const startVolume = this.clampVolume(audio.volume);
|
||||
const target = this.clampVolume(targetVolume);
|
||||
const startedAt = performance.now();
|
||||
return new Promise(resolve => {
|
||||
const step = (now) => {
|
||||
const progress = duration <= 0 ? 1 : Math.min(1, (now - startedAt) / duration);
|
||||
this.setMediaVolume(audio, startVolume + ((target - startVolume) * progress));
|
||||
if (progress < 1 && !audio.paused && !audio.ended) {
|
||||
requestAnimationFrame(step);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
});
|
||||
}
|
||||
|
||||
crossfade(previous, next, duration = 1500) {
|
||||
const start = performance.now();
|
||||
const previousStart = previous ? previous.volume : 0;
|
||||
const target = this.getMusicVolume();
|
||||
const previousStart = previous ? this.clampVolume(previous.volume) : 0;
|
||||
const target = this.clampVolume(this.getMusicVolume());
|
||||
|
||||
const tick = () => {
|
||||
const progress = Math.min(1, (performance.now() - start) / duration);
|
||||
if (previous) previous.volume = previousStart * (1 - progress);
|
||||
next.volume = target * progress;
|
||||
if (previous) this.setMediaVolume(previous, previousStart * (1 - progress));
|
||||
this.setMediaVolume(next, target * progress);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(tick);
|
||||
@@ -718,7 +792,7 @@ class AudioManagerModule extends BaseModule {
|
||||
previous.pause();
|
||||
previous.currentTime = 0;
|
||||
}
|
||||
next.volume = this.getMusicVolume();
|
||||
this.setMediaVolume(next, this.getMusicVolume());
|
||||
};
|
||||
|
||||
tick();
|
||||
@@ -747,7 +821,7 @@ class AudioManagerModule extends BaseModule {
|
||||
}
|
||||
|
||||
const audio = this.currentAudio;
|
||||
const initialVolume = audio.volume;
|
||||
const initialVolume = this.clampVolume(audio.volume);
|
||||
const volumeStep = initialVolume / (duration / 50);
|
||||
let currentVolume = initialVolume;
|
||||
|
||||
@@ -757,10 +831,10 @@ class AudioManagerModule extends BaseModule {
|
||||
clearInterval(fadeInterval);
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
audio.volume = initialVolume; // Reset volume for future use
|
||||
this.setMediaVolume(audio, initialVolume); // Reset volume for future use
|
||||
resolve();
|
||||
} else {
|
||||
audio.volume = currentVolume;
|
||||
this.setMediaVolume(audio, currentVolume);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
@@ -806,7 +880,7 @@ class AudioManagerModule extends BaseModule {
|
||||
}
|
||||
|
||||
// Apply master volume and speech volume
|
||||
audio.volume = this.masterVolume * speechVolume * this._ttsVolume;
|
||||
this.setMediaVolume(audio, this.masterVolume * speechVolume * this.ttsVolume);
|
||||
|
||||
// Set up cleanup
|
||||
audio.onended = () => {
|
||||
|
||||
Reference in New Issue
Block a user