Skip to content

Commit a549395

Browse files
add screen recorder and improve audio engine
1 parent 0458a26 commit a549395

File tree

11 files changed

+470
-191
lines changed

11 files changed

+470
-191
lines changed

js/LaserParticle.js

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,44 @@
11
class AlienLaser {
2-
constructor(x, y) {
2+
constructor(x, y, audioManager) {
33
this.x = x;
44
this.y = y;
55
this.width = 4;
66
this.height = 16;
77
this.vy = 420; // Reduced from 600 by 30%
88
this.life = 2.0;
99
this.maxLife = this.life;
10+
this.audioManager = audioManager;
1011

11-
// Set up audio if it hasn't been initialized yet
12-
if (!AlienLaser.audioContext) {
13-
AlienLaser.setupAudio();
14-
}
12+
// Play sound when laser is created
13+
AlienLaser.playShootSound(x, window.innerWidth, audioManager);
1514
}
1615

17-
static async setupAudio() {
18-
AlienLaser.audioContext = new (window.AudioContext || window.webkitAudioContext)();
19-
try {
20-
const response = await fetch('./audio/alien-shoot.mp3');
21-
const arrayBuffer = await response.arrayBuffer();
22-
AlienLaser.shootBuffer = await AlienLaser.audioContext.decodeAudioData(arrayBuffer);
23-
} catch (error) {
24-
console.error('Error loading alien shoot sound:', error);
16+
static playShootSound(x, virtualWidth, audioManager) {
17+
if (!audioManager) {
18+
console.warn('AlienLaser: No audioManager provided');
19+
return;
2520
}
26-
}
27-
28-
static playShootSound(x, virtualWidth) {
29-
if (!AlienLaser.shootBuffer || !AlienLaser.audioContext) return;
30-
31-
const source = AlienLaser.audioContext.createBufferSource();
32-
const gainNode = AlienLaser.audioContext.createGain();
33-
const pannerNode = AlienLaser.audioContext.createStereoPanner();
34-
35-
source.buffer = AlienLaser.shootBuffer;
36-
source.connect(pannerNode);
37-
pannerNode.connect(gainNode);
38-
gainNode.connect(AlienLaser.audioContext.destination);
39-
40-
// Pan based on position
41-
const normalizedX = (x / virtualWidth) * 2 - 1;
42-
pannerNode.pan.value = normalizedX;
21+
22+
try {
23+
if (!audioManager.sounds.has('alien-laser')) {
24+
console.error('alien-laser sound not loaded!');
25+
return;
26+
}
4327

44-
// Slightly randomize pitch
45-
const pitchVariation = 1 + (Math.random() * 0.1 - 0.05); // ±5% variation
46-
source.playbackRate.value = pitchVariation;
28+
const normalizedX = (x / virtualWidth) * 2 - 1;
29+
30+
const soundConfig = {
31+
pitch: 0.5 + Math.random(), // Random pitch between 0.5 and 1.5
32+
pan: normalizedX, // Pan based on position (-1 to 1)
33+
volume: 0.5 + Math.random() * 0.25, // Random volume between 0.5 and 0.75
34+
decay: 0.5 + Math.random() * 0.5 // Random decay between 0.5 and 1.0 seconds
35+
};
4736

48-
// Quick fade out
49-
const fadeDuration = 0.2;
50-
gainNode.gain.setValueAtTime(0.3, AlienLaser.audioContext.currentTime);
51-
gainNode.gain.linearRampToValueAtTime(0, AlienLaser.audioContext.currentTime + fadeDuration);
37+
audioManager.playSound('alien-laser', soundConfig);
5238

53-
source.start();
54-
source.stop(AlienLaser.audioContext.currentTime + fadeDuration);
39+
} catch (error) {
40+
console.error('AlienLaser: Error playing sound:', error);
41+
}
5542
}
5643

5744
update(delta) {

js/PatternFormation.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class PatternFormation {
1010
this.ctx = ctx;
1111
this.virtualWidth = options.virtualWidth || 1080;
1212
this.virtualHeight = options.virtualHeight || 1080;
13+
this.audioManager = options.audioManager; // Add this
1314

1415
// Initialize config with new starting values
1516
this.config = {
@@ -90,7 +91,7 @@ class PatternFormation {
9091
this.initialAlienCount = this.config.alienCount; // Store initial count
9192
this.pointsBase = 100; // Base points per alien
9293

93-
this.explosionEffect = new ExplosionEffect(ctx);
94+
this.explosionEffect = new ExplosionEffect(ctx, this.audioManager);
9495

9596
// Enhanced rotation parameters
9697
this.baseRotationSpeed = 1.0; // Increased from 0.5
@@ -376,16 +377,14 @@ class PatternFormation {
376377
const shooter = this.aliens[Math.floor(Math.random() * this.aliens.length)];
377378
const laser = new AlienLaser(
378379
shooter.x + shooter.width/2,
379-
shooter.y + shooter.height
380+
shooter.y + shooter.height,
381+
this.audioManager // Pass the audioManager instance
380382
);
381383
this.lasers.push(laser);
382-
383-
// Play shoot sound with position
384-
AlienLaser.playShootSound(shooter.x + shooter.width/2, this.virtualWidth);
385384
}
386385

387386
getPatternPosition(angle, centerX, centerY, radiusX, radiusY) {
388-
switch(this.pattern.type || 'circle') {
387+
switch (this.pattern.type || 'circle') {
389388
case 'figure8':
390389
return {
391390
x: centerX + Math.sin(angle * 2) * radiusX,

js/audio/AudioManager.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
class AudioManager {
2+
static instance = null;
3+
4+
static getInstance() {
5+
if (!AudioManager.instance) {
6+
AudioManager.instance = new AudioManager();
7+
}
8+
return AudioManager.instance;
9+
}
10+
11+
constructor() {
12+
if (AudioManager.instance) {
13+
return AudioManager.instance;
14+
}
15+
AudioManager.instance = this;
16+
17+
this.context = new (window.AudioContext || window.webkitAudioContext)();
18+
this.masterGain = this.context.createGain();
19+
this.musicGain = this.context.createGain();
20+
this.fxGain = this.context.createGain();
21+
22+
// Connect gains to master
23+
this.musicGain.connect(this.masterGain);
24+
this.fxGain.connect(this.masterGain);
25+
26+
// Connect master to speakers by default
27+
this.masterGain.connect(this.context.destination);
28+
29+
this.sounds = new Map();
30+
this.music = new Map();
31+
32+
// Set default volumes
33+
this.masterGain.gain.value = 1.0;
34+
this.musicGain.gain.value = 0.5; // Lower music volume
35+
this.fxGain.gain.value = 0.7; // Slightly lower FX volume
36+
37+
// Add initialization state
38+
this.isInitialized = false;
39+
}
40+
41+
createAudioNodes(source, config = {}) {
42+
const gainNode = this.context.createGain();
43+
const panNode = this.context.createStereoPanner();
44+
45+
// Configure nodes with provided values or defaults
46+
gainNode.gain.value = config.volume ?? 1;
47+
panNode.pan.value = config.pan ?? 0;
48+
49+
// Connect nodes
50+
source.connect(panNode);
51+
panNode.connect(gainNode);
52+
gainNode.connect(this.fxGain);
53+
54+
// Apply pitch if specified
55+
if (config.pitch !== undefined) {
56+
source.playbackRate.value = config.pitch;
57+
}
58+
59+
// Apply decay if specified
60+
if (config.decay > 0) {
61+
const startTime = this.context.currentTime;
62+
gainNode.gain.setValueAtTime(config.volume ?? 1, startTime);
63+
gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + config.decay);
64+
}
65+
66+
return { gainNode, panNode };
67+
}
68+
69+
async loadSound(key, url) {
70+
const response = await fetch(url);
71+
const arrayBuffer = await response.arrayBuffer();
72+
const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
73+
this.sounds.set(key, audioBuffer);
74+
}
75+
76+
async loadMusic(key, url) {
77+
const response = await fetch(url);
78+
const arrayBuffer = await response.arrayBuffer();
79+
const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
80+
this.music.set(key, audioBuffer);
81+
}
82+
83+
async preloadGameSounds(musicTracks = []) {
84+
try {
85+
console.log('Loading game sounds...');
86+
87+
// Load standard game sounds
88+
const standardSounds = [
89+
this.loadSound('explosion', './audio/explosion.mp3'),
90+
this.loadSound('laser', './audio/player-shoot.wav'),
91+
this.loadSound('alien-laser', './audio/alien-shoot.mp3')
92+
];
93+
94+
// Load music tracks
95+
const musicLoading = musicTracks.map((track, index) => {
96+
return this.loadMusic(`track${index}`, track)
97+
.then(() => console.log(`Loaded music track: ${track.split('/').pop()}`));
98+
});
99+
100+
await Promise.all([...standardSounds, ...musicLoading]);
101+
102+
this.isInitialized = true;
103+
console.log('All sounds loaded successfully');
104+
this.checkAudioState();
105+
106+
} catch (error) {
107+
console.error('Failed to load sounds:', error);
108+
throw error;
109+
}
110+
}
111+
112+
playSound(key, config = {}) {
113+
if (!this.isInitialized) {
114+
console.warn('Attempted to play sound before initialization:', key);
115+
return null;
116+
}
117+
118+
const buffer = this.sounds.get(key);
119+
if (!buffer) {
120+
console.warn(`Sound not found: ${key}`);
121+
return null;
122+
}
123+
124+
try {
125+
const source = this.context.createBufferSource();
126+
source.buffer = buffer;
127+
128+
// Create and connect audio processing nodes
129+
const nodes = this.createAudioNodes(source, config);
130+
131+
source.start(0);
132+
console.log(`Playing sound: ${key} with config:`, config);
133+
134+
return { source, ...nodes };
135+
} catch (error) {
136+
console.error(`Error playing sound ${key}:`, error);
137+
return null;
138+
}
139+
}
140+
141+
playMusic(key, loop = true) {
142+
const buffer = this.music.get(key);
143+
if (buffer) {
144+
const source = this.context.createBufferSource();
145+
source.buffer = buffer;
146+
source.loop = loop;
147+
source.connect(this.musicGain);
148+
source.start(0);
149+
return source;
150+
}
151+
}
152+
153+
connectToDestination(destination) {
154+
// Disconnect from speakers
155+
this.masterGain.disconnect();
156+
// Connect to new destination
157+
this.masterGain.connect(destination);
158+
}
159+
160+
reconnectToSpeakers() {
161+
// Reconnect to speakers
162+
this.masterGain.disconnect();
163+
this.masterGain.connect(this.context.destination);
164+
}
165+
166+
// Add debug method
167+
checkAudioState() {
168+
console.log({
169+
contextState: this.context.state,
170+
masterVolume: this.masterGain.gain.value,
171+
fxVolume: this.fxGain.gain.value,
172+
musicVolume: this.musicGain.gain.value,
173+
loadedSounds: Array.from(this.sounds.keys()),
174+
loadedMusic: Array.from(this.music.keys())
175+
});
176+
}
177+
}
178+
179+
export default AudioManager;

0 commit comments

Comments
 (0)