Skip to content

Instantly share code, notes, and snippets.

@toricls
Created July 11, 2026 04:59
Show Gist options
  • Select an option

  • Save toricls/b8b890e82aa5a836e6359e06ed311261 to your computer and use it in GitHub Desktop.

Select an option

Save toricls/b8b890e82aa5a836e6359e06ed311261 to your computer and use it in GitHub Desktop.
Flappy Kiro's SFX
// Audio Manager - handles programmatic sound generation using Web Audio API
// Factory function for creating testable audio manager instances
/**
* Creates a new audio manager instance
* @param {Object} options - Configuration options
* @param {AudioContext} options.audioContext - Optional AudioContext for testing
* @returns {Object} Audio manager with init, playJump, playPass, playCollision, playHighScore methods
*/
export function createAudioManager(options = {}) {
// Load cached SFX preference from localStorage
let cachedSfxEnabled = true;
try {
const stored = localStorage.getItem('flappyKiro_sfxEnabled');
if (stored !== null) {
cachedSfxEnabled = stored === 'true';
}
} catch (e) {
// localStorage unavailable, use default
}
return {
audioContext: options.audioContext || null,
enabled: true,
sfxEnabled: cachedSfxEnabled, // Sound effects toggle state (cached)
/**
* Initialize the AudioContext
* Handles Web Audio API unavailability gracefully
*/
init() {
try {
// Use provided context (for testing) or create new one
if (!this.audioContext) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) {
this.enabled = false;
console.warn('Web Audio API not available, audio disabled');
return;
}
this.audioContext = new AudioContextClass();
}
this.enabled = true;
// Resume context on user interaction if suspended
if (this.audioContext.state === 'suspended') {
const resumeAudio = () => {
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
};
document.addEventListener('click', resumeAudio, { once: true });
document.addEventListener('keydown', resumeAudio, { once: true });
}
// Set up tab visibility listener to resume AudioContext when tab becomes active
this.setupVisibilityListener();
} catch (e) {
this.enabled = false;
console.warn('Failed to initialize AudioContext:', e.message);
}
},
/**
* Set up document visibility change listener
* Resumes AudioContext when tab becomes visible again
*/
setupVisibilityListener() {
document.addEventListener('visibilitychange', () => {
this.handleVisibilityChange();
});
},
/**
* Handle tab visibility changes
* Resumes AudioContext when tab becomes visible (browser suspends it when hidden)
*/
handleVisibilityChange() {
if (!document.hidden && this.audioContext && this.audioContext.state === 'suspended') {
// Tab is now visible - resume AudioContext if suspended
this.audioContext.resume().catch(e => {
console.warn('Failed to resume AudioContext on visibility:', e.message);
});
}
},
/**
* Helper to create oscillator-based tones
* @param {number} frequency - Frequency in Hz
* @param {number} duration - Duration in seconds
* @param {string} type - Waveform type ('sine', 'triangle', 'square', 'sawtooth')
* @param {number} volume - Volume level (0.0 - 1.0)
* @returns {Object|null} Object with oscillator and gainNode, or null if audio unavailable
*/
createTone(frequency, duration, type = 'sine', volume = 0.3) {
if (!this.enabled || !this.audioContext) {
return null;
}
try {
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime);
// Connect oscillator -> gain -> destination
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
// Set initial volume
gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
return { oscillator, gainNode, frequency, duration, type, volume };
} catch (e) {
console.warn('Failed to create tone:', e.message);
return null;
}
},
/**
* Play jump/flap sound
* 400-600 Hz sine wave, 100-150ms duration
* Quick attack and decay envelope
*/
playJump() {
if (!this.enabled || !this.audioContext || !this.sfxEnabled) return;
try {
const frequency = 400 + Math.random() * 200; // 400-600 Hz
const duration = 0.1 + Math.random() * 0.05; // 100-150ms
const tone = this.createTone(frequency, duration, 'sine', 0.3);
if (!tone) return;
const { oscillator, gainNode } = tone;
const now = this.audioContext.currentTime;
// Quick attack and decay envelope
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.3, now + 0.01); // Quick attack
gainNode.gain.linearRampToValueAtTime(0, now + duration); // Decay
oscillator.start(now);
oscillator.stop(now + duration);
} catch (e) {
// Graceful degradation - continue silently
}
},
/**
* Play obstacle pass chime
* 800-1000 Hz sine wave, ~150ms duration
*/
playPass() {
if (!this.enabled || !this.audioContext || !this.sfxEnabled) return;
try {
const frequency = 800 + Math.random() * 200; // 800-1000 Hz
const duration = 0.15;
const tone = this.createTone(frequency, duration, 'sine', 0.2);
if (!tone) return;
const { oscillator, gainNode } = tone;
const now = this.audioContext.currentTime;
// Pleasant envelope
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.2, now + 0.02);
gainNode.gain.linearRampToValueAtTime(0, now + duration);
oscillator.start(now);
oscillator.stop(now + duration);
} catch (e) {
// Graceful degradation - continue silently
}
},
/**
* Play collision thud sound
* 150-200 Hz triangle wave, ~200ms duration
*/
playCollision() {
if (!this.enabled || !this.audioContext || !this.sfxEnabled) return;
try {
const frequency = 150 + Math.random() * 50; // 150-200 Hz
const duration = 0.2;
const tone = this.createTone(frequency, duration, 'triangle', 0.4);
if (!tone) return;
const { oscillator, gainNode } = tone;
const now = this.audioContext.currentTime;
// Impact envelope - quick attack, slower decay
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.4, now + 0.01);
gainNode.gain.linearRampToValueAtTime(0, now + duration);
oscillator.start(now);
oscillator.stop(now + duration);
} catch (e) {
// Graceful degradation - continue silently
}
},
/**
* Play high score celebration fanfare
* Ascending tone sequence: C5 (523Hz), E5 (659Hz), G5 (784Hz), C6 (1047Hz)
* Each note ~100ms with slight overlap
*/
playHighScore() {
if (!this.enabled || !this.audioContext || !this.sfxEnabled) return;
try {
const notes = [523, 659, 784, 1047]; // C5, E5, G5, C6
const noteDuration = 0.1; // 100ms each
const now = this.audioContext.currentTime;
notes.forEach((freq, index) => {
const startTime = now + index * 0.08; // Slight overlap
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(freq, startTime);
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
// Envelope for each note
gainNode.gain.setValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(0.3, startTime + 0.01);
gainNode.gain.linearRampToValueAtTime(0, startTime + noteDuration);
oscillator.start(startTime);
oscillator.stop(startTime + noteDuration);
});
} catch (e) {
// Graceful degradation - continue silently
}
},
/**
* Get sound frequency configurations for testing
* @returns {Object} Sound frequency ranges
*/
getSoundFrequencies() {
return {
jump: { min: 400, max: 600 },
pass: { min: 800, max: 1000 },
collision: { min: 150, max: 200 },
highScore: [523, 659, 784, 1047]
};
},
/**
* Toggle all sound effects on/off
* Flips the sfxEnabled state and caches to localStorage
*/
toggleSFX() {
this.sfxEnabled = !this.sfxEnabled;
try {
localStorage.setItem('flappyKiro_sfxEnabled', this.sfxEnabled.toString());
} catch (e) {
// localStorage unavailable, continue without caching
}
},
/**
* Get current SFX enabled state
* @returns {boolean} Whether sound effects are enabled
*/
isSFXEnabled() {
return this.sfxEnabled;
}
};
}
// Default instance for game.js usage
const audioManager = createAudioManager();
export default audioManager;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment