advanced-games-library
Version:
Advanced Gaming Library for React Native - Four Complete Games with iOS Compatibility Fixes
676 lines (580 loc) • 20.1 kB
text/typescript
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)
};
}