UNPKG

@osobh/reactar

Version:

AR Library for React Native and Expo

208 lines (185 loc) 5.47 kB
// src/media/AudioManager.ts import { Audio } from 'expo-av'; import { EventEmitter } from 'events'; import { Vector3 } from 'three'; export interface AudioSource { id: string; sound: Audio.Sound; position: Vector3; maxDistance?: number; refDistance?: number; rolloffFactor?: number; } export interface AudioOptions { maxDistance?: number; // Distance at which the sound is completely inaudible refDistance?: number; // Distance at which the sound starts to attenuate rolloffFactor?: number; // How quickly the sound attenuates with distance loop?: boolean; volume?: number; } export class AudioManager extends EventEmitter { private audioSources: Map<string, AudioSource> = new Map(); private listenerPosition: Vector3 = new Vector3(); private isInitialized: boolean = false; constructor() { super(); } /** * Initializes the audio system */ public async initialize(): Promise<void> { try { await Audio.setAudioModeAsync({ playsInSilentModeIOS: true, staysActiveInBackground: true, shouldDuckAndroid: true }); this.isInitialized = true; this.emit('initialized'); } catch (error) { this.emit('error', error); throw error; } } /** * Loads and creates a spatial audio source * @param id Unique identifier for the audio source * @param uri URI of the audio file to load * @param position Initial 3D position of the audio source * @param options Audio options */ public async createAudioSource( id: string, uri: string, position: Vector3, options: AudioOptions = {} ): Promise<void> { if (!this.isInitialized) { throw new Error('AudioManager must be initialized first'); } try { const { sound } = await Audio.Sound.createAsync( { uri }, { shouldPlay: false, volume: options.volume ?? 1.0, isLooping: options.loop ?? false, }, undefined, true ); const audioSource: AudioSource = { id, sound, position: position.clone(), maxDistance: options.maxDistance ?? 50, refDistance: options.refDistance ?? 1, rolloffFactor: options.rolloffFactor ?? 1 }; this.audioSources.set(id, audioSource); this.updateAudioSourceVolume(audioSource); this.emit('audio-source-created', id); } catch (error) { this.emit('error', error); throw error; } } /** * Updates the position of an audio source * @param id Audio source identifier * @param position New position */ public updateSourcePosition(id: string, position: Vector3): void { const source = this.audioSources.get(id); if (!source) { throw new Error(`Audio source ${id} not found`); } source.position.copy(position); this.updateAudioSourceVolume(source); } /** * Updates the listener's position (usually the camera/user position) * @param position New listener position */ public updateListenerPosition(position: Vector3): void { this.listenerPosition.copy(position); this.audioSources.forEach(source => this.updateAudioSourceVolume(source)); } /** * Plays an audio source * @param id Audio source identifier */ public async playSource(id: string): Promise<void> { const source = this.audioSources.get(id); if (!source) { throw new Error(`Audio source ${id} not found`); } try { await source.sound.playAsync(); } catch (error) { this.emit('error', error); throw error; } } /** * Stops an audio source * @param id Audio source identifier */ public async stopSource(id: string): Promise<void> { const source = this.audioSources.get(id); if (!source) { throw new Error(`Audio source ${id} not found`); } try { await source.sound.stopAsync(); } catch (error) { this.emit('error', error); throw error; } } /** * Removes an audio source and cleans up its resources * @param id Audio source identifier */ public async removeSource(id: string): Promise<void> { const source = this.audioSources.get(id); if (!source) { return; } try { await source.sound.unloadAsync(); this.audioSources.delete(id); this.emit('audio-source-removed', id); } catch (error) { this.emit('error', error); throw error; } } /** * Updates the volume of an audio source based on its distance from the listener * @param source Audio source to update */ private updateAudioSourceVolume(source: AudioSource): void { const distance = this.listenerPosition.distanceTo(source.position); const refDistance = source.refDistance ?? 1; const maxDistance = source.maxDistance ?? 50; const rolloffFactor = source.rolloffFactor ?? 1; // Calculate volume using inverse distance model with rolloff let volume = 1.0; if (distance > refDistance) { volume = refDistance / (refDistance + rolloffFactor * (distance - refDistance)); } if (distance >= maxDistance) { volume = 0; } source.sound.setVolumeAsync(volume); } /** * Cleans up all audio resources */ public async cleanup(): Promise<void> { const promises = Array.from(this.audioSources.keys()).map(id => this.removeSource(id)); await Promise.all(promises); this.emit('cleanup-complete'); } }