// Audio Manager using Web Audio API const audioManager = { audioContext: null, muted: false, sounds: {}, lowTimeThreshold: 10, // Seconds threshold for low time warning lastTickTime: 0, // Track when we started continuous ticking tickFadeoutTime: 3, // Seconds after which tick sound fades out // Initialize the audio context init() { try { // Create AudioContext (with fallback for older browsers) this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Check for saved mute preference const savedMute = localStorage.getItem('chessTimerMuted'); this.muted = savedMute === 'true'; // Create all the sounds this.createSounds(); console.log('Web Audio API initialized successfully'); return true; } catch (error) { console.error('Web Audio API initialization failed:', error); return false; } }, // Create all the sound generators createSounds() { // Game sounds this.sounds.tick = this.createTickSound(); this.sounds.lowTime = this.createLowTimeSound(); this.sounds.timeUp = this.createTimeUpSound(); this.sounds.gameStart = this.createGameStartSound(); this.sounds.gamePause = this.createGamePauseSound(); this.sounds.gameResume = this.createGameResumeSound(); this.sounds.gameOver = this.createGameOverSound(); this.sounds.playerSwitch = this.createPlayerSwitchSound(); // UI sounds this.sounds.buttonClick = this.createButtonClickSound(); this.sounds.modalOpen = this.createModalOpenSound(); this.sounds.modalClose = this.createModalCloseSound(); this.sounds.playerAdded = this.createPlayerAddedSound(); this.sounds.playerEdited = this.createPlayerEditedSound(); this.sounds.playerDeleted = this.createPlayerDeletedSound(); }, // Helper function to create an oscillator createOscillator(type, frequency, startTime, duration, gain = 1.0, ramp = false) { if (this.audioContext === null) this.init(); const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); oscillator.type = type; oscillator.frequency.value = frequency; gainNode.gain.value = gain; oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); oscillator.start(startTime); if (ramp) { gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + duration); } oscillator.stop(startTime + duration); return { oscillator, gainNode }; }, // Sound creators createTickSound() { return () => { const now = this.audioContext.currentTime; const currentTime = Date.now() / 1000; // Initialize lastTickTime if it's not set if (this.lastTickTime === 0) { this.lastTickTime = currentTime; } // Calculate how long we've been ticking continuously const tickDuration = currentTime - this.lastTickTime; // Determine volume based on duration let volume = 0.1; // Default/initial volume if (tickDuration <= this.tickFadeoutTime) { // Linear fade from 0.1 to 0 over tickFadeoutTime seconds volume = 0.1 * (1 - (tickDuration / this.tickFadeoutTime)); } else { // After tickFadeoutTime, don't play any sound return; // Exit without playing sound } // Only play if volume is significant if (volume > 0.001) { this.createOscillator('sine', 800, now, 0.03, volume); } }; }, createLowTimeSound() { return () => { const now = this.audioContext.currentTime; // Low time warning is always audible this.createOscillator('triangle', 660, now, 0.1, 0.2); // Reset tick fade timer on low time warning this.lastTickTime = 0; }; }, createTimeUpSound() { return () => { const now = this.audioContext.currentTime; // First note this.createOscillator('sawtooth', 440, now, 0.2, 0.3); // Second note (lower) this.createOscillator('sawtooth', 220, now + 0.25, 0.3, 0.4); // Reset tick fade timer this.lastTickTime = 0; }; }, createGameStartSound() { return () => { const now = this.audioContext.currentTime; // Rising sequence this.createOscillator('sine', 440, now, 0.1, 0.3); this.createOscillator('sine', 554, now + 0.1, 0.1, 0.3); this.createOscillator('sine', 659, now + 0.2, 0.3, 0.3, true); // Reset tick fade timer this.lastTickTime = 0; }; }, createGamePauseSound() { return () => { const now = this.audioContext.currentTime; // Two notes pause sound this.createOscillator('sine', 659, now, 0.1, 0.3); this.createOscillator('sine', 523, now + 0.15, 0.2, 0.3, true); // Reset tick fade timer this.lastTickTime = 0; }; }, createGameResumeSound() { return () => { const now = this.audioContext.currentTime; // Rising sequence (opposite of pause) this.createOscillator('sine', 523, now, 0.1, 0.3); this.createOscillator('sine', 659, now + 0.15, 0.2, 0.3, true); // Reset tick fade timer this.lastTickTime = 0; }; }, createGameOverSound() { return () => { const now = this.audioContext.currentTime; // Fanfare this.createOscillator('square', 440, now, 0.1, 0.3); this.createOscillator('square', 554, now + 0.1, 0.1, 0.3); this.createOscillator('square', 659, now + 0.2, 0.1, 0.3); this.createOscillator('square', 880, now + 0.3, 0.4, 0.3, true); // Reset tick fade timer this.lastTickTime = 0; }; }, createPlayerSwitchSound() { return () => { const now = this.audioContext.currentTime; this.createOscillator('sine', 1200, now, 0.05, 0.2); // Reset tick fade timer on player switch this.lastTickTime = 0; }; }, createButtonClickSound() { return () => { const now = this.audioContext.currentTime; this.createOscillator('sine', 700, now, 0.04, 0.1); }; }, createModalOpenSound() { return () => { const now = this.audioContext.currentTime; // Ascending sound this.createOscillator('sine', 400, now, 0.1, 0.2); this.createOscillator('sine', 600, now + 0.1, 0.1, 0.2); }; }, createModalCloseSound() { return () => { const now = this.audioContext.currentTime; // Descending sound this.createOscillator('sine', 600, now, 0.1, 0.2); this.createOscillator('sine', 400, now + 0.1, 0.1, 0.2); }; }, createPlayerAddedSound() { return () => { const now = this.audioContext.currentTime; // Positive ascending notes this.createOscillator('sine', 440, now, 0.1, 0.2); this.createOscillator('sine', 523, now + 0.1, 0.1, 0.2); this.createOscillator('sine', 659, now + 0.2, 0.2, 0.2, true); }; }, createPlayerEditedSound() { return () => { const now = this.audioContext.currentTime; // Two note confirmation this.createOscillator('sine', 440, now, 0.1, 0.2); this.createOscillator('sine', 523, now + 0.15, 0.15, 0.2); }; }, createPlayerDeletedSound() { return () => { const now = this.audioContext.currentTime; // Descending notes this.createOscillator('sine', 659, now, 0.1, 0.2); this.createOscillator('sine', 523, now + 0.1, 0.1, 0.2); this.createOscillator('sine', 392, now + 0.2, 0.2, 0.2, true); }; }, // Play a sound if not muted play(soundName) { if (this.muted || !this.sounds[soundName]) return; // Resume audio context if it's suspended (needed for newer browsers) if (this.audioContext.state === 'suspended') { this.audioContext.resume(); } this.sounds[soundName](); }, // Toggle mute state toggleMute() { this.muted = !this.muted; localStorage.setItem('chessTimerMuted', this.muted); return this.muted; }, // Play timer sounds based on remaining time playTimerSound(remainingSeconds) { if (remainingSeconds <= 0) { // Reset tick fade timer when timer stops this.lastTickTime = 0; return; // Don't play sounds for zero time } if (remainingSeconds <= this.lowTimeThreshold) { // Play low time warning sound (this resets the tick fade timer) this.play('lowTime'); } else if (remainingSeconds % 1 === 0) { // Normal tick sound on every second this.play('tick'); } }, // Play timer expired sound playTimerExpired() { this.play('timeUp'); }, // Stop all sounds and reset the tick fading stopAllSounds() { // Reset tick fade timer when stopping sounds this.lastTickTime = 0; }, // Reset the tick fading (call this when timer is paused or player changes) resetTickFade() { this.lastTickTime = 0; } }; // Initialize audio on module load audioManager.init(); // Export the audio manager export default audioManager;