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 | |
} | |
} | |
} |