Files
AetherForge/server/web/src/audio/ambientMusic.ts
AetherForge 1551bd5dad feat: Emberwake, Crucible phases, Linux agent, musical dashboard, e2e
Emberwake spread/waterhole UI, campaign DB, spread handler, spread-kit web publisher, and SPREAD_TECHNIQUES doc. Crucible Phase A-C: expanded ops, port-forward matrix, remote dir browser, crucible help/tests.

Linux agent hardening: credential vault, persistence audit, firewall/defender deploy, SMB spread status, CPU stats, screenshots/crypt/file-ops split. Docker compose and agent/server images with e2e validation script and docs.

Musical dashboard: ambient music player, hover SFX, SoundContext/AmbientMusicContext, steampunk polish. Public builds API, dropper handler updates, SessionGate and fleet UX. README and PROBLEMS.md refresh.
2026-06-04 21:53:31 -07:00

153 lines
3.6 KiB
TypeScript

/**
* Looping background music — drop your MP3 at public/audio/ambient.mp3
* (MP3 preferred; OGG/WAV also work if you update AMBIENT_MUSIC_SRC).
*/
export const BGM_STORAGE_KEY = 'aetherforge-bgm';
export const BGM_VOLUME_KEY = 'aetherforge-bgm-volume';
/** Served from Vite public/ — place ambient.mp3 here before enabling in Settings. */
export const AMBIENT_MUSIC_SRC = '/audio/ambient.mp3';
export function loadBgmEnabled(): boolean {
try {
const v = localStorage.getItem(BGM_STORAGE_KEY);
return v === '1';
} catch {
return false;
}
}
export function loadBgmVolume(): number {
try {
const v = localStorage.getItem(BGM_VOLUME_KEY);
if (v === null) return 0.22;
const n = parseFloat(v);
return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0.22;
} catch {
return 0.22;
}
}
function persistBgmEnabled(enabled: boolean) {
try {
localStorage.setItem(BGM_STORAGE_KEY, enabled ? '1' : '0');
} catch {
/* ignore */
}
}
function persistBgmVolume(volume: number) {
try {
localStorage.setItem(BGM_VOLUME_KEY, String(volume));
} catch {
/* ignore */
}
}
export class AmbientMusicPlayer {
private audio: HTMLAudioElement | null = null;
private enabled = loadBgmEnabled();
private volume = loadBgmVolume();
private unlocked = false;
private playing = false;
private listeners = new Set<(playing: boolean) => void>();
isEnabled() {
return this.enabled;
}
isPlaying() {
return this.playing;
}
getVolume() {
return this.volume;
}
subscribe(fn: (playing: boolean) => void) {
this.listeners.add(fn);
return () => { this.listeners.delete(fn); };
}
private setPlaying(v: boolean) {
if (this.playing === v) return;
this.playing = v;
for (const fn of this.listeners) fn(v);
}
setEnabled(enabled: boolean) {
this.enabled = enabled;
persistBgmEnabled(enabled);
if (enabled) {
this.ensureAudio();
void this.tryPlay();
} else {
this.pause();
}
}
setVolume(volume: number) {
this.volume = Math.min(1, Math.max(0, volume));
persistBgmVolume(this.volume);
if (this.audio) this.audio.volume = this.volume;
}
/** Browsers block autoplay until a user gesture unlocks audio. */
unlock() {
if (this.unlocked) return;
this.unlocked = true;
this.ensureAudio();
if (this.enabled) void this.tryPlay();
}
togglePlay() {
if (this.playing) {
this.pause();
return false;
}
if (!this.enabled) {
this.setEnabled(true);
}
void this.tryPlay();
return true;
}
private ensureAudio() {
if (this.audio || typeof document === 'undefined') return;
const el = new Audio(AMBIENT_MUSIC_SRC);
el.loop = true;
el.preload = 'auto';
el.volume = this.volume;
el.addEventListener('play', () => this.setPlaying(true));
el.addEventListener('pause', () => this.setPlaying(false));
el.addEventListener('ended', () => this.setPlaying(false));
el.addEventListener('error', () => {
this.setPlaying(false);
});
this.audio = el;
}
private pause() {
if (!this.audio) return;
this.audio.pause();
this.setPlaying(false);
}
async tryPlay(): Promise<boolean> {
if (!this.enabled) return false;
this.ensureAudio();
if (!this.audio) return false;
this.audio.volume = this.volume;
try {
await this.audio.play();
this.setPlaying(true);
return true;
} catch {
this.setPlaying(false);
/* Autoplay policy — wait for Settings toggle or first click */
return false;
}
}
}
export const ambientMusicPlayer = new AmbientMusicPlayer();