// Sound service for chess UI // Define sound types type SoundType = 'move' | 'capture' | 'check' | 'castle' | 'game-end' | 'promote'; // Map of sound files const SOUND_FILES: Record = { 'move': '/assets/sounds/move.mp3', 'capture': '/assets/sounds/capture.mp3', 'check': '/assets/sounds/check.mp3', 'castle': '/assets/sounds/castle.mp3', 'game-end': '/assets/sounds/game-end.mp3', 'promote': '/assets/sounds/move.mp3' // Reusing move sound for promote }; // Audio objects for preloading const audioCache: Record = {} as Record; // Sound enabled flag (can be toggled by user) let soundEnabled = true; // Create fallback sounds using the Web Audio API if sound files are not available const audioContext = typeof AudioContext !== 'undefined' ? new AudioContext() : null; // Function to create a simple beep sound const createBeepSound = (frequency: number, duration: number): AudioBuffer | null => { if (!audioContext) return null; const sampleRate = audioContext.sampleRate; const buffer = audioContext.createBuffer(1, sampleRate * duration, sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < buffer.length; i++) { data[i] = Math.sin(2 * Math.PI * frequency * i / sampleRate) * (i < buffer.length * 0.1 ? i / (buffer.length * 0.1) : i > buffer.length * 0.9 ? (buffer.length - i) / (buffer.length * 0.1) : 1); } return buffer; }; // Create fallback sounds const fallbackSounds: Record = { 'move': audioContext ? createBeepSound(440, 0.1) : null, // A4 note 'capture': audioContext ? createBeepSound(330, 0.2) : null, // E4 note 'check': audioContext ? createBeepSound(587, 0.3) : null, // D5 note 'castle': audioContext ? createBeepSound(261, 0.3) : null, // C4 note 'game-end': audioContext ? createBeepSound(196, 0.5) : null, // G3 note 'promote': audioContext ? createBeepSound(523, 0.2) : null // C5 note }; /** * Preload all sound files */ export const preloadSounds = (): void => { Object.entries(SOUND_FILES).forEach(([key, file]) => { try { const audio = new Audio(); audio.src = file; audio.preload = 'auto'; audioCache[key as SoundType] = audio; } catch (error) { console.error(`Failed to preload sound: ${file}`, error); } }); }; /** * Play a sound effect * @param type The type of sound to play */ export const playSound = (type: SoundType): void => { if (!soundEnabled) return; try { // Try to play the audio file const audio = audioCache[type]; if (audio) { audio.currentTime = 0; audio.play().catch(err => { console.warn('Error playing sound file, using fallback:', err); playFallbackSound(type); }); } else { // Try to create a new audio element const newAudio = new Audio(SOUND_FILES[type]); newAudio.play().catch(err => { console.warn('Error playing sound file, using fallback:', err); playFallbackSound(type); }); } } catch (error) { console.error(`Failed to play sound: ${type}`, error); playFallbackSound(type); } }; /** * Play a fallback sound using Web Audio API * @param type The type of sound to play */ const playFallbackSound = (type: SoundType): void => { if (!audioContext) return; try { const buffer = fallbackSounds[type]; if (buffer) { const source = audioContext.createBufferSource(); source.buffer = buffer; source.connect(audioContext.destination); source.start(); } else { // Create a simple beep as last resort const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); // Set properties based on sound type switch (type) { case 'move': oscillator.frequency.value = 440; // A4 gainNode.gain.value = 0.1; break; case 'capture': oscillator.frequency.value = 330; // E4 gainNode.gain.value = 0.2; break; case 'check': oscillator.frequency.value = 587; // D5 gainNode.gain.value = 0.3; break; case 'castle': oscillator.frequency.value = 261; // C4 gainNode.gain.value = 0.2; break; case 'game-end': oscillator.frequency.value = 196; // G3 gainNode.gain.value = 0.3; break; case 'promote': oscillator.frequency.value = 523; // C5 gainNode.gain.value = 0.2; break; } // Start and stop the oscillator oscillator.start(); oscillator.stop(audioContext.currentTime + 0.2); } } catch (error) { console.error('Failed to play fallback sound:', error); } }; /** * Play a sound if sound is enabled * @param type The type of sound to play */ export const playSoundIfEnabled = (type: SoundType): void => { if (soundEnabled) { playSound(type); } }; /** * Toggle sound on/off * @param enabled Whether sound should be enabled */ export const toggleSound = (enabled?: boolean): boolean => { if (enabled !== undefined) { soundEnabled = enabled; } else { soundEnabled = !soundEnabled; } return soundEnabled; }; /** * Set sound enabled state * @param enabled Whether sound should be enabled */ export const setSoundEnabled = (enabled: boolean): void => { soundEnabled = enabled; }; /** * Check if sound is enabled */ export const isSoundEnabled = (): boolean => { return soundEnabled; };