@osobh/reactar
Version:
AR Library for React Native and Expo
208 lines (185 loc) • 5.47 kB
text/typescript
// 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');
}
}