mirror of
https://github.com/kuhyx/slavic_game_jam.git
synced 2026-07-04 13:23:08 +02:00
367 lines
11 KiB
JavaScript
367 lines
11 KiB
JavaScript
export class AudioSystem {
|
|
constructor() {
|
|
this.audioContext = null;
|
|
this.gainNode = null;
|
|
this.activeSounds = new Set();
|
|
this.initialized = false;
|
|
this.lastSpokenDirection = '';
|
|
this.speechEnabled = true;
|
|
|
|
this.initializeAudio();
|
|
}
|
|
|
|
async initializeAudio() {
|
|
try {
|
|
// Create audio context (requires user interaction)
|
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
this.gainNode = this.audioContext.createGain();
|
|
this.gainNode.connect(this.audioContext.destination);
|
|
this.gainNode.gain.value = 0.3; // Set volume to 30%
|
|
|
|
this.initialized = true;
|
|
} catch (error) {
|
|
console.warn('Web Audio API not supported:', error);
|
|
}
|
|
}
|
|
|
|
async ensureAudioContext() {
|
|
if (!this.initialized) {
|
|
await this.initializeAudio();
|
|
}
|
|
|
|
if (this.audioContext && this.audioContext.state === 'suspended') {
|
|
await this.audioContext.resume();
|
|
}
|
|
}
|
|
|
|
createOscillator(frequency, type = 'sine', duration = 0.2, panValue = 0, pitchBend = 0) {
|
|
if (!this.audioContext || !this.initialized) return null;
|
|
|
|
const oscillator = this.audioContext.createOscillator();
|
|
const envelope = this.audioContext.createGain();
|
|
const panner = this.audioContext.createStereoPanner();
|
|
|
|
oscillator.connect(envelope);
|
|
envelope.connect(panner);
|
|
panner.connect(this.gainNode);
|
|
|
|
oscillator.frequency.value = frequency + pitchBend;
|
|
oscillator.type = type;
|
|
|
|
// Set stereo panning (-1 = left, 0 = center, 1 = right)
|
|
panner.pan.value = Math.max(-1, Math.min(1, panValue));
|
|
|
|
// Create ADSR envelope
|
|
const now = this.audioContext.currentTime;
|
|
envelope.gain.setValueAtTime(0, now);
|
|
envelope.gain.linearRampToValueAtTime(1, now + 0.01); // Attack
|
|
envelope.gain.exponentialRampToValueAtTime(0.01, now + duration); // Decay
|
|
|
|
oscillator.start(now);
|
|
oscillator.stop(now + duration);
|
|
|
|
this.activeSounds.add(oscillator);
|
|
oscillator.onended = () => {
|
|
this.activeSounds.delete(oscillator);
|
|
};
|
|
|
|
return oscillator;
|
|
}
|
|
|
|
createNoise(duration = 0.1) {
|
|
if (!this.audioContext || !this.initialized) return null;
|
|
|
|
const bufferSize = this.audioContext.sampleRate * duration;
|
|
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
|
|
const output = buffer.getChannelData(0);
|
|
|
|
// Generate white noise
|
|
for (let i = 0; i < bufferSize; i++) {
|
|
output[i] = Math.random() * 2 - 1;
|
|
}
|
|
|
|
const source = this.audioContext.createBufferSource();
|
|
const filter = this.audioContext.createBiquadFilter();
|
|
const envelope = this.audioContext.createGain();
|
|
|
|
source.buffer = buffer;
|
|
source.connect(filter);
|
|
filter.connect(envelope);
|
|
envelope.connect(this.gainNode);
|
|
|
|
filter.type = 'lowpass';
|
|
filter.frequency.value = 300;
|
|
|
|
const now = this.audioContext.currentTime;
|
|
envelope.gain.setValueAtTime(0.5, now);
|
|
envelope.gain.linearRampToValueAtTime(0, now + duration);
|
|
|
|
source.start(now);
|
|
|
|
this.activeSounds.add(source);
|
|
source.onended = () => {
|
|
this.activeSounds.delete(source);
|
|
};
|
|
|
|
return source;
|
|
}
|
|
|
|
async playDangerSound() {
|
|
await this.ensureAudioContext();
|
|
|
|
// Create a danger sound with multiple frequencies and noise
|
|
this.createOscillator(150, 'sawtooth', 0.3);
|
|
this.createOscillator(200, 'square', 0.2);
|
|
this.createNoise(0.15);
|
|
|
|
// Add a low rumble
|
|
setTimeout(() => {
|
|
this.createOscillator(80, 'sine', 0.4);
|
|
}, 100);
|
|
}
|
|
|
|
async playProximitySound() {
|
|
await this.ensureAudioContext();
|
|
|
|
// Create a subtle warning sound for proximity to audio danger
|
|
this.createOscillator(300, 'sine', 0.1);
|
|
this.createOscillator(250, 'sine', 0.1);
|
|
}
|
|
|
|
async playDirectionalProximitySound(directions) {
|
|
// Use text-to-speech instead of sounds
|
|
this.speakDirections(directions);
|
|
}
|
|
|
|
speakDirections(directions) {
|
|
console.log('speakDirections called with:', directions);
|
|
|
|
if (!this.speechEnabled) {
|
|
console.log('Speech is disabled');
|
|
return;
|
|
}
|
|
|
|
if (!('speechSynthesis' in window)) {
|
|
console.error('Speech synthesis not supported in this browser');
|
|
return;
|
|
}
|
|
|
|
// Determine which directions to announce
|
|
const activeDirections = [];
|
|
|
|
if (directions.up) activeDirections.push('up');
|
|
if (directions.down) activeDirections.push('down');
|
|
if (directions.left) activeDirections.push('left');
|
|
if (directions.right) activeDirections.push('right');
|
|
|
|
console.log('Active directions:', activeDirections);
|
|
|
|
if (activeDirections.length === 0) {
|
|
console.log('No active directions to announce');
|
|
return;
|
|
}
|
|
|
|
// Create the text to speak
|
|
let textToSpeak;
|
|
if (activeDirections.length === 1) {
|
|
textToSpeak = activeDirections[0];
|
|
} else if (activeDirections.length === 2) {
|
|
textToSpeak = activeDirections.join(' and ');
|
|
} else {
|
|
textToSpeak = activeDirections.slice(0, -1).join(', ') + ', and ' + activeDirections[activeDirections.length - 1];
|
|
}
|
|
|
|
console.log('Text to speak:', textToSpeak);
|
|
console.log('Last spoken direction:', this.lastSpokenDirection);
|
|
|
|
// Avoid repeating the same direction announcement
|
|
if (textToSpeak === this.lastSpokenDirection) {
|
|
console.log('Skipping duplicate direction announcement');
|
|
return;
|
|
}
|
|
this.lastSpokenDirection = textToSpeak;
|
|
|
|
// Stop any currently playing speech
|
|
speechSynthesis.cancel();
|
|
|
|
try {
|
|
// Check if speechSynthesis is ready
|
|
if (speechSynthesis.speaking) {
|
|
console.log('Speech synthesis is currently speaking, canceling...');
|
|
speechSynthesis.cancel();
|
|
}
|
|
|
|
// Wait a bit for cancellation to complete
|
|
setTimeout(() => {
|
|
try {
|
|
// Create and configure the speech utterance
|
|
const utterance = new SpeechSynthesisUtterance(textToSpeak);
|
|
utterance.rate = 1.0; // Normal speech rate to avoid issues
|
|
utterance.pitch = 1.0;
|
|
utterance.volume = 0.8;
|
|
utterance.lang = 'en-US'; // Explicitly set language
|
|
|
|
// Enhanced event handlers
|
|
utterance.onstart = () => {
|
|
console.log('Speech started for:', textToSpeak);
|
|
};
|
|
|
|
utterance.onend = () => {
|
|
console.log('Speech ended for:', textToSpeak);
|
|
setTimeout(() => {
|
|
this.lastSpokenDirection = '';
|
|
}, 500);
|
|
};
|
|
|
|
utterance.onerror = (event) => {
|
|
console.error('Speech synthesis error:', event.error, 'for text:', textToSpeak);
|
|
console.error('Error details:', event);
|
|
|
|
// Try to recover by clearing the last spoken direction
|
|
setTimeout(() => {
|
|
this.lastSpokenDirection = '';
|
|
}, 100);
|
|
|
|
// Fallback: try a simpler approach
|
|
this.fallbackSpeech(textToSpeak);
|
|
};
|
|
|
|
utterance.onpause = () => {
|
|
console.log('Speech paused');
|
|
};
|
|
|
|
utterance.onresume = () => {
|
|
console.log('Speech resumed');
|
|
};
|
|
|
|
// Check if voices are available
|
|
const voices = speechSynthesis.getVoices();
|
|
console.log('Available voices:', voices.length, voices.map(v => v.name));
|
|
|
|
if (voices.length > 0) {
|
|
// Try to use a default English voice
|
|
const englishVoice = voices.find(voice =>
|
|
voice.lang.startsWith('en') && voice.default
|
|
) || voices.find(voice =>
|
|
voice.lang.startsWith('en')
|
|
) || voices[0];
|
|
|
|
if (englishVoice) {
|
|
utterance.voice = englishVoice;
|
|
console.log('Using voice:', englishVoice.name, englishVoice.lang);
|
|
}
|
|
}
|
|
|
|
console.log('Starting speech synthesis for:', textToSpeak);
|
|
|
|
// Speak the directions
|
|
speechSynthesis.speak(utterance);
|
|
|
|
} catch (innerError) {
|
|
console.error('Inner error in speech synthesis:', innerError);
|
|
this.fallbackSpeech(textToSpeak);
|
|
}
|
|
}, 100); // Small delay to ensure cancellation completes
|
|
|
|
} catch (error) {
|
|
console.error('Outer error in speech synthesis:', error);
|
|
this.fallbackSpeech(textToSpeak);
|
|
}
|
|
}
|
|
|
|
setSpeechEnabled(enabled) {
|
|
this.speechEnabled = enabled;
|
|
if (!enabled) {
|
|
speechSynthesis.cancel();
|
|
this.lastSpokenDirection = '';
|
|
}
|
|
}
|
|
|
|
// Debug methods for testing directions
|
|
debugTestDirection(direction) {
|
|
console.log('Debug: Testing direction:', direction);
|
|
const testDirections = {
|
|
up: direction === 'up',
|
|
down: direction === 'down',
|
|
left: direction === 'left',
|
|
right: direction === 'right',
|
|
squares: [{ x: 0, y: 0, dx: 0, dy: 0 }] // Mock square data
|
|
};
|
|
this.speakDirections(testDirections);
|
|
}
|
|
|
|
debugTestMultipleDirections(directionsArray) {
|
|
console.log('Debug: Testing multiple directions:', directionsArray);
|
|
const testDirections = {
|
|
up: directionsArray.includes('up'),
|
|
down: directionsArray.includes('down'),
|
|
left: directionsArray.includes('left'),
|
|
right: directionsArray.includes('right'),
|
|
squares: [{ x: 0, y: 0, dx: 0, dy: 0 }] // Mock square data
|
|
};
|
|
this.speakDirections(testDirections);
|
|
}
|
|
|
|
async playWinSound() {
|
|
await this.ensureAudioContext();
|
|
|
|
// Create a victory melody
|
|
const notes = [262, 330, 392, 523]; // C, E, G, C (one octave higher)
|
|
|
|
notes.forEach((frequency, index) => {
|
|
setTimeout(() => {
|
|
this.createOscillator(frequency, 'sine', 0.4);
|
|
}, index * 100);
|
|
});
|
|
|
|
// Add harmony
|
|
setTimeout(() => {
|
|
this.createOscillator(659, 'sine', 0.6); // E
|
|
}, 200);
|
|
}
|
|
|
|
async playGameOverSound() {
|
|
await this.ensureAudioContext();
|
|
|
|
// Create a descending game over sound
|
|
const notes = [330, 294, 262, 220]; // E, D, C, A (descending)
|
|
|
|
notes.forEach((frequency, index) => {
|
|
setTimeout(() => {
|
|
this.createOscillator(frequency, 'sawtooth', 0.5);
|
|
}, index * 150);
|
|
});
|
|
|
|
// Add a final low note
|
|
setTimeout(() => {
|
|
this.createOscillator(147, 'square', 1.0); // Low D
|
|
}, 600);
|
|
}
|
|
|
|
playMoveSound() {
|
|
if (!this.initialized) return;
|
|
|
|
// Subtle movement sound
|
|
this.createOscillator(440, 'sine', 0.05);
|
|
}
|
|
|
|
stopAll() {
|
|
this.activeSounds.forEach(sound => {
|
|
try {
|
|
if (sound.stop) {
|
|
sound.stop();
|
|
}
|
|
} catch (error) {
|
|
// Sound may have already stopped
|
|
}
|
|
});
|
|
this.activeSounds.clear();
|
|
}
|
|
|
|
setVolume(volume) {
|
|
if (this.gainNode) {
|
|
this.gainNode.gain.value = Math.max(0, Math.min(1, volume));
|
|
}
|
|
}
|
|
}
|