slavic_game_jam/src/audio.js

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