Spaces:
Sleeping
Sleeping
| // 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<SoundType, string> = { | |
| '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<SoundType, HTMLAudioElement> = {} as Record<SoundType, HTMLAudioElement>; | |
| // 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<SoundType, AudioBuffer | null> = { | |
| '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; | |
| }; |