UNPKG

advanced-games-library

Version:

Advanced Gaming Library for React Native - Four Complete Games with iOS Compatibility Fixes

676 lines (580 loc) 20.1 kB
import * as React from 'react'; /** * Advanced Audio System for Games * Comprehensive audio management with 3D spatial audio, dynamic music, and adaptive sound effects */ export interface AudioTrack { id: string; name: string; url: string; type: 'sfx' | 'music' | 'voice' | 'ambient'; volume: number; loop: boolean; fadeIn?: number; fadeOut?: number; spatial?: SpatialAudioConfig; adaptive?: AdaptiveAudioConfig; } export interface SpatialAudioConfig { enabled: boolean; position: { x: number; y: number; z: number }; maxDistance: number; rolloffFactor: number; dopplerFactor: number; } export interface AdaptiveAudioConfig { enabled: boolean; triggers: AudioTrigger[]; crossfadeDuration: number; } export interface AudioTrigger { condition: 'performance' | 'game_state' | 'player_action' | 'time' | 'location'; value: any; operator: '==' | '!=' | '>' | '<' | '>=' | '<='; targetTrack: string; volumeModifier?: number; } export interface AudioProfile { name: string; masterVolume: number; musicVolume: number; sfxVolume: number; voiceVolume: number; ambientVolume: number; spatialAudioEnabled: boolean; adaptiveAudioEnabled: boolean; qualityLevel: 'low' | 'medium' | 'high' | 'ultra'; } export interface AudioAnalytics { trackId: string; playCount: number; totalDuration: number; avgVolume: number; skipRate: number; performanceImpact: number; userRating?: number; } /** * Advanced Audio Service with spatial audio and adaptive music */ export class AdvancedAudioService { private static instance: AdvancedAudioService; private audioContext?: AudioContext; private tracks = new Map<string, AudioTrack>(); private loadedAudio = new Map<string, AudioBuffer>(); private playingSources = new Map<string, AudioBufferSourceNode>(); private masterGain?: GainNode; private musicGain?: GainNode; private sfxGain?: GainNode; private voiceGain?: GainNode; private ambientGain?: GainNode; private spatialPanner = new Map<string, PannerNode>(); private profile: AudioProfile; private isEnabled = true; private analytics = new Map<string, AudioAnalytics>(); private adaptiveAudioEngine?: AdaptiveAudioEngine; private constructor() { this.profile = this.getDefaultProfile(); this.initializeAudioContext(); this.setupPerformanceMonitoring(); this.adaptiveAudioEngine = new AdaptiveAudioEngine(this); } static getInstance(): AdvancedAudioService { if (!AdvancedAudioService.instance) { AdvancedAudioService.instance = new AdvancedAudioService(); } return AdvancedAudioService.instance; } /** * Initialize Web Audio API context */ private initializeAudioContext(): void { try { // @ts-ignore - Web Audio API might not be available in all environments this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Create gain nodes for different audio categories this.masterGain = this.audioContext.createGain(); this.musicGain = this.audioContext.createGain(); this.sfxGain = this.audioContext.createGain(); this.voiceGain = this.audioContext.createGain(); this.ambientGain = this.audioContext.createGain(); // Connect gain nodes this.musicGain.connect(this.masterGain); this.sfxGain.connect(this.masterGain); this.voiceGain.connect(this.masterGain); this.ambientGain.connect(this.masterGain); this.masterGain.connect(this.audioContext.destination); // Apply initial volume settings this.updateVolumeSettings(); } catch (error) { console.warn('Advanced audio features not available:', error); this.isEnabled = false; } } /** * Load audio track */ async loadTrack(track: AudioTrack): Promise<void> { if (!this.isEnabled || !this.audioContext) return; try { const response = await fetch(track.url); const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer); this.loadedAudio.set(track.id, audioBuffer); this.tracks.set(track.id, track); // Initialize analytics this.analytics.set(track.id, { trackId: track.id, playCount: 0, totalDuration: 0, avgVolume: track.volume, skipRate: 0, performanceImpact: 0 }); } catch (error) { console.error(`Failed to load audio track ${track.id}:`, error); } } /** * Play audio with advanced features */ async play( trackId: string, options?: { volume?: number; loop?: boolean; fadeIn?: number; spatialPosition?: { x: number; y: number; z: number }; playbackRate?: number; startTime?: number; endTime?: number; } ): Promise<string> { if (!this.isEnabled || !this.audioContext) return ''; const track = this.tracks.get(trackId); const audioBuffer = this.loadedAudio.get(trackId); if (!track || !audioBuffer) { console.warn(`Audio track ${trackId} not found or not loaded`); return ''; } // Create audio source const source = this.audioContext.createBufferSource(); source.buffer = audioBuffer; // Configure playback source.loop = options?.loop ?? track.loop; source.playbackRate.value = options?.playbackRate ?? 1.0; // Create gain node for this instance const gainNode = this.audioContext.createGain(); const finalVolume = (options?.volume ?? track.volume) * this.getCategoryVolume(track.type); gainNode.gain.value = finalVolume; // Setup spatial audio if enabled let finalDestination: AudioNode = gainNode; if (track.spatial?.enabled || options?.spatialPosition) { const panner = this.createSpatialPanner(trackId, options?.spatialPosition || track.spatial!.position); gainNode.connect(panner); finalDestination = panner; } // Connect to appropriate category gain finalDestination.connect(this.getCategoryGainNode(track.type)); source.connect(gainNode); // Setup fade in if (options?.fadeIn || track.fadeIn) { const fadeTime = options?.fadeIn || track.fadeIn!; gainNode.gain.setValueAtTime(0, this.audioContext.currentTime); gainNode.gain.linearRampToValueAtTime(finalVolume, this.audioContext.currentTime + fadeTime / 1000); } // Generate unique play ID const playId = `${trackId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Start playback const startTime = options?.startTime || 0; const duration = options?.endTime ? options.endTime - startTime : audioBuffer.duration - startTime; source.start(this.audioContext.currentTime, startTime, duration); // Store playing source this.playingSources.set(playId, source); // Setup cleanup on end source.onended = () => { this.playingSources.delete(playId); this.updateAnalytics(trackId, duration); }; // Update analytics const analytics = this.analytics.get(trackId); if (analytics) { analytics.playCount++; } return playId; } /** * Stop playing audio with fade out */ async stop(playId: string, fadeOut?: number): Promise<void> { if (!this.isEnabled || !this.audioContext) return; const source = this.playingSources.get(playId); if (!source) return; if (fadeOut) { // Create fade out effect const gainNode = this.audioContext.createGain(); gainNode.gain.setValueAtTime(1, this.audioContext.currentTime); gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + fadeOut / 1000); setTimeout(() => { source.stop(); this.playingSources.delete(playId); }, fadeOut); } else { source.stop(); this.playingSources.delete(playId); } } /** * Create spatial audio panner */ private createSpatialPanner(trackId: string, position: { x: number; y: number; z: number }): PannerNode { if (!this.audioContext) throw new Error('Audio context not available'); const panner = this.audioContext.createPanner(); // Configure spatial audio properties panner.panningModel = 'HRTF'; panner.distanceModel = 'inverse'; panner.refDistance = 1; panner.maxDistance = 100; panner.rolloffFactor = 1; panner.coneInnerAngle = 360; panner.coneOuterAngle = 0; panner.coneOuterGain = 0; // Set position panner.positionX.value = position.x; panner.positionY.value = position.y; panner.positionZ.value = position.z; this.spatialPanner.set(trackId, panner); return panner; } /** * Update spatial audio listener position */ updateListenerPosition(position: { x: number; y: number; z: number }, orientation?: { x: number; y: number; z: number }): void { if (!this.audioContext || !this.audioContext.listener) return; // Update listener position this.audioContext.listener.positionX.value = position.x; this.audioContext.listener.positionY.value = position.y; this.audioContext.listener.positionZ.value = position.z; // Update orientation if provided if (orientation) { this.audioContext.listener.forwardX.value = orientation.x; this.audioContext.listener.forwardY.value = orientation.y; this.audioContext.listener.forwardZ.value = orientation.z; } } /** * Create dynamic music playlist that adapts to gameplay */ createAdaptivePlaylist(tracks: string[], adaptationRules: AudioTrigger[]): string { return this.adaptiveAudioEngine!.createPlaylist(tracks, adaptationRules); } /** * Update game state for adaptive audio */ updateGameState(gameState: any): void { this.adaptiveAudioEngine?.updateGameState(gameState); } /** * Get category gain node */ private getCategoryGainNode(type: AudioTrack['type']): GainNode { switch (type) { case 'music': return this.musicGain!; case 'sfx': return this.sfxGain!; case 'voice': return this.voiceGain!; case 'ambient': return this.ambientGain!; default: return this.sfxGain!; } } /** * Get category volume */ private getCategoryVolume(type: AudioTrack['type']): number { switch (type) { case 'music': return this.profile.musicVolume; case 'sfx': return this.profile.sfxVolume; case 'voice': return this.profile.voiceVolume; case 'ambient': return this.profile.ambientVolume; default: return this.profile.sfxVolume; } } /** * Update volume settings */ private updateVolumeSettings(): void { if (!this.audioContext) return; this.masterGain!.gain.value = this.profile.masterVolume; this.musicGain!.gain.value = this.profile.musicVolume; this.sfxGain!.gain.value = this.profile.sfxVolume; this.voiceGain!.gain.value = this.profile.voiceVolume; this.ambientGain!.gain.value = this.profile.ambientVolume; } /** * Set audio profile */ setProfile(profile: Partial<AudioProfile>): void { this.profile = { ...this.profile, ...profile }; this.updateVolumeSettings(); } /** * Get default audio profile */ private getDefaultProfile(): AudioProfile { return { name: 'default', masterVolume: 1.0, musicVolume: 0.7, sfxVolume: 0.8, voiceVolume: 1.0, ambientVolume: 0.5, spatialAudioEnabled: true, adaptiveAudioEnabled: true, qualityLevel: 'high' }; } /** * Setup performance monitoring */ private setupPerformanceMonitoring(): void { // Monitor audio performance impact setInterval(() => { if (this.audioContext) { const playingCount = this.playingSources.size; const performanceImpact = this.calculatePerformanceImpact(playingCount); // Adjust quality if performance is impacted if (performanceImpact > 0.8 && this.profile.qualityLevel !== 'low') { this.adaptQualityLevel(); } } }, 5000); } /** * Calculate performance impact */ private calculatePerformanceImpact(playingCount: number): number { // Simplified performance impact calculation const baseImpact = playingCount * 0.1; const spatialImpact = this.spatialPanner.size * 0.05; return Math.min(1.0, baseImpact + spatialImpact); } /** * Adapt quality level based on performance */ private adaptQualityLevel(): void { const currentQuality = this.profile.qualityLevel; switch (currentQuality) { case 'ultra': this.profile.qualityLevel = 'high'; break; case 'high': this.profile.qualityLevel = 'medium'; break; case 'medium': this.profile.qualityLevel = 'low'; break; } console.log(`Audio quality adapted to: ${this.profile.qualityLevel}`); } /** * Update analytics */ private updateAnalytics(trackId: string, duration: number): void { const analytics = this.analytics.get(trackId); if (analytics) { analytics.totalDuration += duration; } } /** * Get audio analytics */ getAnalytics(): Map<string, AudioAnalytics> { return new Map(this.analytics); } /** * Enable/disable audio system */ setEnabled(enabled: boolean): void { this.isEnabled = enabled; if (!enabled) { // Stop all playing audio this.playingSources.forEach((source, playId) => { source.stop(); }); this.playingSources.clear(); } } /** * Cleanup resources */ cleanup(): void { this.playingSources.forEach(source => source.stop()); this.playingSources.clear(); this.spatialPanner.clear(); if (this.audioContext) { this.audioContext.close(); } } } /** * Adaptive Audio Engine for dynamic music and sound adaptation */ class AdaptiveAudioEngine { private audioService: AdvancedAudioService; private playlists = new Map<string, AdaptivePlaylist>(); private gameState: any = {}; private currentTrack?: string; constructor(audioService: AdvancedAudioService) { this.audioService = audioService; } /** * Create adaptive playlist */ createPlaylist(tracks: string[], adaptationRules: AudioTrigger[]): string { const playlistId = `playlist_${Date.now()}`; const playlist: AdaptivePlaylist = { id: playlistId, tracks, rules: adaptationRules, currentTrackIndex: 0, isPlaying: false }; this.playlists.set(playlistId, playlist); return playlistId; } /** * Update game state and trigger audio adaptations */ updateGameState(newGameState: any): void { this.gameState = { ...this.gameState, ...newGameState }; // Check all playlists for trigger conditions this.playlists.forEach(playlist => { this.checkAdaptationTriggers(playlist); }); } /** * Check adaptation triggers for a playlist */ private checkAdaptationTriggers(playlist: AdaptivePlaylist): void { for (const rule of playlist.rules) { if (this.evaluateTrigger(rule)) { this.adaptPlaylist(playlist, rule); break; // Only apply first matching rule } } } /** * Evaluate trigger condition */ private evaluateTrigger(trigger: AudioTrigger): boolean { const gameValue = this.getGameStateValue(trigger.condition); switch (trigger.operator) { case '==': return gameValue === trigger.value; case '!=': return gameValue !== trigger.value; case '>': return gameValue > trigger.value; case '<': return gameValue < trigger.value; case '>=': return gameValue >= trigger.value; case '<=': return gameValue <= trigger.value; default: return false; } } /** * Get game state value for trigger evaluation */ private getGameStateValue(condition: AudioTrigger['condition']): any { switch (condition) { case 'performance': // Get current performance metrics return this.gameState.fps || 60; case 'game_state': return this.gameState.currentState; case 'player_action': return this.gameState.lastAction; case 'time': return Date.now(); case 'location': return this.gameState.playerPosition; default: return null; } } /** * Adapt playlist based on trigger */ private adaptPlaylist(playlist: AdaptivePlaylist, trigger: AudioTrigger): void { // Find target track const targetIndex = playlist.tracks.findIndex(track => track === trigger.targetTrack); if (targetIndex !== -1 && targetIndex !== playlist.currentTrackIndex) { // Crossfade to new track this.crossfadeToTrack(playlist, targetIndex, trigger.volumeModifier); } } /** * Crossfade between tracks */ private async crossfadeToTrack(playlist: AdaptivePlaylist, targetIndex: number, volumeModifier?: number): Promise<void> { const currentTrack = playlist.tracks[playlist.currentTrackIndex]; const targetTrack = playlist.tracks[targetIndex]; // Start new track with fade in const newPlayId = await this.audioService.play(targetTrack, { fadeIn: 2000, // 2 second fade in volume: volumeModifier || 1.0 }); // Fade out current track if playing if (this.currentTrack) { await this.audioService.stop(this.currentTrack, 2000); } playlist.currentTrackIndex = targetIndex; this.currentTrack = newPlayId; } } interface AdaptivePlaylist { id: string; tracks: string[]; rules: AudioTrigger[]; currentTrackIndex: number; isPlaying: boolean; } // Export singleton instance export const advancedAudioService = AdvancedAudioService.getInstance(); /** * React Hook for advanced audio management */ export function useAdvancedAudio() { const [isEnabled, setIsEnabled] = React.useState(true); const [currentProfile, setCurrentProfile] = React.useState<AudioProfile>(); const [analytics, setAnalytics] = React.useState<Map<string, AudioAnalytics>>(new Map()); React.useEffect(() => { // Update analytics periodically const interval = setInterval(() => { setAnalytics(advancedAudioService.getAnalytics()); }, 5000); return () => clearInterval(interval); }, []); const playSound = React.useCallback(async (trackId: string, options?: any) => { return await advancedAudioService.play(trackId, options); }, []); const stopSound = React.useCallback(async (playId: string, fadeOut?: number) => { return await advancedAudioService.stop(playId, fadeOut); }, []); const updateProfile = React.useCallback((profile: Partial<AudioProfile>) => { advancedAudioService.setProfile(profile); setCurrentProfile(prev => ({ ...prev!, ...profile })); }, []); const enable = React.useCallback((enabled: boolean) => { advancedAudioService.setEnabled(enabled); setIsEnabled(enabled); }, []); return { isEnabled, currentProfile, analytics, playSound, stopSound, updateProfile, enable, loadTrack: advancedAudioService.loadTrack.bind(advancedAudioService), createAdaptivePlaylist: advancedAudioService.createAdaptivePlaylist.bind(advancedAudioService), updateGameState: advancedAudioService.updateGameState.bind(advancedAudioService), updateListenerPosition: advancedAudioService.updateListenerPosition.bind(advancedAudioService) }; }