177 lines
6.0 KiB
JavaScript
Executable File
177 lines
6.0 KiB
JavaScript
Executable File
// Web Audio API Synthesizer for magical escape sounds
|
|
class SoundFX {
|
|
constructor() {
|
|
this.ctx = null;
|
|
this.muted = localStorage.getItem('escape_sound_muted') === 'true';
|
|
}
|
|
|
|
init() {
|
|
if (!this.ctx) {
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
this.ctx = new AudioContext();
|
|
}
|
|
if (this.ctx && this.ctx.state === 'suspended') {
|
|
this.ctx.resume();
|
|
}
|
|
}
|
|
|
|
toggleMute() {
|
|
this.muted = !this.muted;
|
|
localStorage.setItem('escape_sound_muted', this.muted);
|
|
return !this.muted;
|
|
}
|
|
|
|
isMuted() {
|
|
return this.muted;
|
|
}
|
|
|
|
// Whoosh of flying broomstick
|
|
playWhoosh() {
|
|
if (this.muted) return;
|
|
this.init();
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
const filter = this.ctx.createBiquadFilter();
|
|
|
|
osc.type = 'sawtooth';
|
|
filter.type = 'lowpass';
|
|
filter.frequency.setValueAtTime(300, this.ctx.currentTime);
|
|
filter.frequency.exponentialRampToValueAtTime(800, this.ctx.currentTime + 0.15);
|
|
filter.frequency.exponentialRampToValueAtTime(200, this.ctx.currentTime + 0.35);
|
|
|
|
gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.35);
|
|
|
|
osc.connect(filter);
|
|
filter.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.36);
|
|
}
|
|
|
|
// Key caught / fluttering chime
|
|
playKeyChime() {
|
|
if (this.muted) return;
|
|
this.init();
|
|
const freqs = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
|
|
freqs.forEach((freq, idx) => {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = 'sine';
|
|
osc.frequency.value = freq;
|
|
const startTime = this.ctx.currentTime + idx * 0.05;
|
|
gain.gain.setValueAtTime(0.1, startTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.4);
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.start(startTime);
|
|
osc.stop(startTime + 0.41);
|
|
});
|
|
}
|
|
|
|
// Clink of glass bottle
|
|
playBottleClink() {
|
|
if (this.muted) return;
|
|
this.init();
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = 'triangle';
|
|
osc.frequency.setValueAtTime(1200, this.ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(800, this.ctx.currentTime + 0.12);
|
|
gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.15);
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.16);
|
|
}
|
|
|
|
// Success magical harmony
|
|
playMagicSuccess() {
|
|
if (this.muted) return;
|
|
this.init();
|
|
const chords = [440, 554.37, 659.25, 880, 1108.73]; // A Major triumph
|
|
chords.forEach((freq, i) => {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.06);
|
|
gain.gain.setValueAtTime(0.12, this.ctx.currentTime + i * 0.06);
|
|
gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + i * 0.06 + 1.2);
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.start(this.ctx.currentTime + i * 0.06);
|
|
osc.stop(this.ctx.currentTime + i * 0.06 + 1.25);
|
|
});
|
|
}
|
|
|
|
// Poison fizzle
|
|
playPoisonSizzle() {
|
|
if (this.muted) return;
|
|
this.init();
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.setValueAtTime(180, this.ctx.currentTime);
|
|
osc.frequency.linearRampToValueAtTime(90, this.ctx.currentTime + 0.5);
|
|
gain.gain.setValueAtTime(0.25, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.51);
|
|
}
|
|
|
|
// Stone slide
|
|
playStoneSlide() {
|
|
if (this.muted) return;
|
|
this.init();
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = 'square';
|
|
osc.frequency.setValueAtTime(80, this.ctx.currentTime);
|
|
gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.21);
|
|
}
|
|
}
|
|
|
|
window.soundFX = new SoundFX();
|
|
|
|
function updateAudioButtonUI() {
|
|
const btn = document.getElementById('audioToggleBtn');
|
|
const bgm = document.getElementById('bgm');
|
|
if (!btn) return;
|
|
const isMuted = window.soundFX.isMuted();
|
|
btn.innerHTML = isMuted ? '🔇 <span class="audio-lbl">Musik & Ton: Aus</span>' : '🔊 <span class="audio-lbl">Musik & Ton: An</span>';
|
|
if (bgm) {
|
|
bgm.volume = 0.45;
|
|
if (isMuted && !bgm.paused) {
|
|
bgm.pause();
|
|
} else if (!isMuted && bgm.paused) {
|
|
bgm.play().catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleSound() {
|
|
window.soundFX.toggleMute();
|
|
updateAudioButtonUI();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
updateAudioButtonUI();
|
|
document.body.addEventListener('click', () => {
|
|
window.soundFX.init();
|
|
const bgm = document.getElementById('bgm');
|
|
if (bgm && !window.soundFX.isMuted() && bgm.paused) {
|
|
bgm.volume = 0.45;
|
|
bgm.play().catch(() => {});
|
|
}
|
|
}, { once: true });
|
|
});
|