315 lines
10 KiB
JavaScript
315 lines
10 KiB
JavaScript
// 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; |