Spaces:
Sleeping
Sleeping
| import { Chess, Square, Move, PieceSymbol, Color } from 'chess.js' | |
| import { PositionEvaluator } from './PositionEvaluator' | |
| export class AudioEngine { | |
| private audioContext: AudioContext | null = null | |
| private masterGain: GainNode | null = null | |
| private isActive: boolean = false | |
| private volume: number = 0.7 | |
| private ambientVolume: number = 0.8 | |
| private gameVolume: number = 0.048 | |
| private userInterval: NodeJS.Timeout | null = null | |
| private boardFlipped: boolean = false | |
| private noteFrequencies = { | |
| 'A': 27.50, // A0 | |
| 'B': 30.87, // B0 | |
| 'Cb': 32.70, // C1♭ | |
| 'C': 32.70, // C1 | |
| 'D': 36.71, // D1 | |
| 'Db': 34.65, // D1♭ | |
| 'E': 41.20, // E1 | |
| 'F': 43.65 // F1 | |
| } | |
| private fileNotes = ['A', 'B', 'Cb', 'C', 'D', 'Db', 'E', 'F'] | |
| private fileFrequencies = [ | |
| this.noteFrequencies.A, | |
| this.noteFrequencies.B, | |
| this.noteFrequencies.Cb, | |
| this.noteFrequencies.C, | |
| this.noteFrequencies.D, | |
| this.noteFrequencies.Db, | |
| this.noteFrequencies.E, | |
| this.noteFrequencies.F | |
| ] | |
| private userInitiativeFreq = 110.0 // A2 | |
| constructor() { | |
| this.initialize() | |
| } | |
| private async initialize() { | |
| try { | |
| this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() | |
| this.masterGain = this.audioContext.createGain() | |
| this.masterGain.connect(this.audioContext.destination) | |
| this.masterGain.gain.value = this.volume | |
| this.isActive = true | |
| console.log('Audio engine initialized') | |
| } catch (error) { | |
| console.error('Failed to initialize audio engine:', error) | |
| this.isActive = false | |
| } | |
| } | |
| setBoardFlipped(flipped: boolean) { | |
| this.boardFlipped = flipped | |
| } | |
| private getSquareFrequency(square: Square): number { | |
| const file = square.charCodeAt(0) - 97 | |
| const rank = parseInt(square[1]) - 1 | |
| const actualFile = this.boardFlipped ? 7 - file : file | |
| const actualRank = this.boardFlipped ? 7 - rank : rank | |
| const baseFreq = this.fileFrequencies[actualFile] | |
| const octaveMultiplier = Math.pow(2, actualRank) | |
| return baseFreq * octaveMultiplier | |
| } | |
| getFileNoteName(fileIndex: number): string { | |
| const actualFile = this.boardFlipped ? 7 - fileIndex : fileIndex | |
| return this.fileNotes[actualFile] | |
| } | |
| setVolume(volume: number) { | |
| this.volume = Math.max(0, Math.min(1, volume)) | |
| if (this.masterGain) { | |
| this.masterGain.gain.value = this.volume | |
| } | |
| if (this.volume === 0) { | |
| this.stopAllAudio() | |
| } | |
| } | |
| setAmbientVolume(volume: number) { | |
| this.ambientVolume = Math.max(0, Math.min(1, volume)) | |
| } | |
| setGameVolume(volume: number) { | |
| this.gameVolume = Math.max(0, Math.min(0.1, volume * 0.1)) | |
| } | |
| getAmbientVolume(): number { | |
| return this.ambientVolume | |
| } | |
| getGameVolume(): number { | |
| return this.gameVolume / 0.1 | |
| } | |
| async ensureAudioContext() { | |
| if (!this.audioContext) { | |
| await this.initialize() | |
| } | |
| if (this.audioContext?.state === 'suspended') { | |
| await this.audioContext.resume() | |
| } | |
| } | |
| private createTone(frequency: number, duration: number, fadeOut: boolean = true, volume: number = 0.4): Promise<void> { | |
| return new Promise((resolve) => { | |
| if (!this.audioContext || !this.masterGain || this.volume === 0) { | |
| resolve() | |
| return | |
| } | |
| const oscillator = this.audioContext.createOscillator() | |
| const gainNode = this.audioContext.createGain() | |
| oscillator.connect(gainNode) | |
| gainNode.connect(this.masterGain) | |
| oscillator.frequency.value = frequency | |
| oscillator.type = 'sine' | |
| const now = this.audioContext.currentTime | |
| gainNode.gain.setValueAtTime(volume, now) | |
| if (fadeOut) { | |
| gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration) | |
| } else { | |
| gainNode.gain.setValueAtTime(volume, now + duration - 0.01) | |
| gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration) | |
| } | |
| oscillator.start(now) | |
| oscillator.stop(now + duration) | |
| oscillator.onended = () => resolve() | |
| }) | |
| } | |
| async playMoveSound(move: Move, board: Chess, capturedPiece?: any) { | |
| if (!this.isActive || this.volume === 0) return | |
| try { | |
| await this.ensureAudioContext() | |
| const originFreq = this.getSquareFrequency(move.from as Square) | |
| const destFreq = this.getSquareFrequency(move.to as Square) | |
| const beatDuration = 0.2 | |
| const pauseDuration = 0.08 | |
| await this.createTone(originFreq, beatDuration, false, this.gameVolume) | |
| await new Promise(resolve => setTimeout(resolve, pauseDuration * 1000)) | |
| await this.createTone(destFreq, beatDuration, false, this.gameVolume) | |
| if (capturedPiece) { | |
| await new Promise(resolve => setTimeout(resolve, pauseDuration * 1000)) | |
| await this.createCaptureSound() | |
| } else { | |
| const canCapture = this.checkForDanger(move.to as Square, board) | |
| if (canCapture) { | |
| await new Promise(resolve => setTimeout(resolve, pauseDuration * 1000)) | |
| await this.createDangerSound() | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Error playing move sound:', error) | |
| } | |
| } | |
| private async createCaptureSound() { | |
| if (!this.audioContext || !this.masterGain) return | |
| const duration = 0.2 | |
| const now = this.audioContext.currentTime | |
| const frequencies = [400, 800, 1200] | |
| const oscillators: OscillatorNode[] = [] | |
| const gainNodes: GainNode[] = [] | |
| frequencies.forEach((freq, index) => { | |
| const oscillator = this.audioContext!.createOscillator() | |
| const gainNode = this.audioContext!.createGain() | |
| oscillator.connect(gainNode) | |
| gainNode.connect(this.masterGain!) | |
| oscillator.frequency.value = freq | |
| oscillator.type = 'sine' | |
| const harmonic_volume = this.gameVolume * (1 / (index + 1)) | |
| gainNode.gain.setValueAtTime(harmonic_volume, now) | |
| gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration) | |
| oscillator.start(now) | |
| oscillator.stop(now + duration) | |
| oscillators.push(oscillator) | |
| gainNodes.push(gainNode) | |
| }) | |
| return new Promise<void>(resolve => { | |
| oscillators[oscillators.length - 1].onended = () => resolve() | |
| }) | |
| } | |
| private async createDangerSound() { | |
| if (!this.audioContext || !this.masterGain) return | |
| const duration = 0.15 | |
| const now = this.audioContext.currentTime | |
| const oscillator = this.audioContext.createOscillator() | |
| const gainNode = this.audioContext.createGain() | |
| oscillator.connect(gainNode) | |
| gainNode.connect(this.masterGain) | |
| oscillator.frequency.value = 1000 | |
| oscillator.type = 'sawtooth' | |
| gainNode.gain.setValueAtTime(this.gameVolume, now) | |
| gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration) | |
| oscillator.start(now) | |
| oscillator.stop(now + duration) | |
| return new Promise<void>(resolve => { | |
| oscillator.onended = () => resolve() | |
| }) | |
| } | |
| private checkForDanger(square: Square, board: Chess): boolean { | |
| try { | |
| const moves = board.moves({ square, verbose: true }) | |
| return moves.some((move: any) => move.captured) | |
| } catch { | |
| return false | |
| } | |
| } | |
| async updatePositionAudio(board: Chess, userColor: Color = 'w') { | |
| if (!this.isActive || this.volume === 0) return | |
| try { | |
| await this.ensureAudioContext() | |
| this.startContinuousInitiativeAudio(board, userColor) | |
| } catch (error) { | |
| console.error('Error updating position audio:', error) | |
| } | |
| } | |
| private startContinuousInitiativeAudio(board: Chess, userColor: Color = 'w') { | |
| if (!this.audioContext || !this.masterGain) return | |
| this.stopPositionAudio() | |
| const userInitiative = PositionEvaluator.getInitiative(board, userColor) | |
| const userBeatsPerMinute = userInitiative * 120 | |
| if (userBeatsPerMinute > 0) { | |
| const userInterval = (60 / userBeatsPerMinute) * 1000 | |
| this.userInterval = setInterval(() => { | |
| this.playInitiativeBeat(this.userInitiativeFreq) | |
| }, userInterval) | |
| } | |
| } | |
| private async playInitiativeBeat(frequency: number) { | |
| if (!this.audioContext || !this.masterGain || this.volume === 0) return | |
| const oscillator = this.audioContext.createOscillator() | |
| const gainNode = this.audioContext.createGain() | |
| oscillator.connect(gainNode) | |
| gainNode.connect(this.masterGain) | |
| oscillator.frequency.value = frequency | |
| oscillator.type = 'sine' | |
| const now = this.audioContext.currentTime | |
| const duration = 0.1 | |
| const volume = this.ambientVolume | |
| gainNode.gain.setValueAtTime(volume, now) | |
| gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration) | |
| oscillator.start(now) | |
| oscillator.stop(now + duration) | |
| } | |
| updateInitiativeVolumes(board: Chess, userColor: Color = 'w') { | |
| this.startContinuousInitiativeAudio(board, userColor) | |
| } | |
| stopPositionAudio() { | |
| if (this.userInterval) { | |
| clearInterval(this.userInterval) | |
| this.userInterval = null | |
| } | |
| } | |
| stopAllAudio() { | |
| this.stopPositionAudio() | |
| if (this.audioContext) { | |
| try { | |
| if (this.masterGain) { | |
| this.masterGain.disconnect() | |
| this.masterGain = this.audioContext.createGain() | |
| this.masterGain.connect(this.audioContext.destination) | |
| this.masterGain.gain.value = this.volume | |
| } | |
| } catch (error) { | |
| console.error('Error stopping audio:', error) | |
| } | |
| } | |
| } | |
| isPlaying(): boolean { | |
| return this.userInterval !== null | |
| } | |
| cleanup() { | |
| this.stopAllAudio() | |
| this.isActive = false | |
| if (this.audioContext) { | |
| this.audioContext.close() | |
| this.audioContext = null | |
| } | |
| } | |
| } |