import { EmitterSubscription, NativeEventEmitter, NativeModules, Platform, } from "react-native"; const LINKING_ERROR = `The package 'react-native-sound' doesn't seem to be linked. Make sure: \n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: "" }) + "- You rebuilt the app after installing the package\n" + "- You are not using Expo Go\n"; const SoundModule = (() => { if (global.__turboModuleProxy) { try { return require('./NativeSound').default; } catch (e) { console.warn('TurboModule not found, falling back to NativeModules'); } } return NativeModules.RNSound; })(); const RNSound = SoundModule || new Proxy( {}, { get() { throw new Error(LINKING_ERROR); }, } ) interface SoundProps { duration?: number; numberOfChannels?: number; } interface SoundOptionTypes { speed: number; loadSync: boolean; } const isRelativePath = (path: string) => { return !/^(\/|http(s?)|asset|file)/.test(path); }; let nextKey = 0; const eventEmitter = new NativeEventEmitter(RNSound); class Sound { private _filename: string; private _key: number; private _loaded: boolean; private _playing: boolean; private _duration: number; private _numberOfChannels: number; private _volume: number; private _pan: number; private _numberOfLoops: number; private _speed: number; private _pitch: number; private onPlaySubscription: EmitterSubscription; constructor( filename: string, basePath?: string | ((error: string, props: SoundProps) => void), onError?: (error: string, props: SoundProps) => void, options?: SoundOptionTypes ) { if (filename.startsWith("http")) { this._filename = filename; } else { this._filename = basePath ? `${basePath}/${filename}` : filename; } if (typeof basePath === "function") { onError = basePath; } this._key = nextKey++; this._loaded = false; this._playing = false; this._duration = -1; this._numberOfChannels = -1; this._volume = 1; this._pan = 0; this._numberOfLoops = 0; this._speed = 1; this._pitch = 1; RNSound.prepare( this._filename, this._key, options ?? {}, (error: string, props: SoundProps) => { if (props) { if (typeof props.duration === "number") { this._duration = props.duration; } if (typeof props.numberOfChannels === "number") { this._numberOfChannels = props.numberOfChannels; } } if (error === null) { this._loaded = true; this.registerOnPlay(); } onError && onError(error, props); } ); } private registerOnPlay() { if (this.onPlaySubscription) { return; } this.onPlaySubscription = eventEmitter.addListener( "onPlayChange", (param: { isPlaying: boolean; playerKey: number }) => { const { isPlaying, playerKey } = param; if (playerKey === this._key) { this._playing = isPlaying; } } ); } private calculateRelativeVolume(volume: number, pan: number): number { const relativeVolume = volume * (1 - Math.abs(pan)); return Number(relativeVolume.toFixed(1)); } private setAndroidVolumes() { if (this._pan) { const relativeVolume = this.calculateRelativeVolume( this._volume, this._pan ); if (this._pan < 0) { RNSound.setVolume(this._key, this._volume, relativeVolume); } else { RNSound.setVolume(this._key, relativeVolume, this._volume); } } else { RNSound.setVolume(this._key, this._volume, this._volume); } } public isLoaded(): boolean { return this._loaded; } public play(onEnd?: (successfully: boolean) => void): Sound { if (this._loaded) { RNSound.play( this._key, (successfully: boolean) => onEnd && onEnd(successfully) ); } else { onEnd && onEnd(false); } return this; } public pause(callback?: () => void): Sound { if (this._loaded) { RNSound.pause(this._key, () => { this._playing = false; callback && callback(); }); } return this; } public stop(callback?: () => void): Sound { if (this._loaded) { RNSound.stop(this._key, () => { this._playing = false; callback && callback(); }); } return this; } public reset(): Sound { if (this._loaded) { RNSound.reset(this._key); this._playing = false; } return this; } public release(): Sound { if (this._loaded) { RNSound.release(this._key); this._loaded = false; if (this.onPlaySubscription) { this.onPlaySubscription.remove(); } } return this; } public getFilename(): string { return this._filename; } public getDuration(): number { return this._duration; } public getNumberOfChannels(): number { return this._numberOfChannels; } public getVolume(): number { return this._volume; } public getSpeed(): number { return this._speed; } public getPitch(): number { return this._pitch; } public setVolume(value: number): Sound { this._volume = value; if (this._loaded) { RNSound.setVolume(this._key, value); } return this; } public setPan(value: number): Sound { this._pan = value; if (this._loaded) { RNSound.setPan(this._key, value); } return this; } public getPan(): number { return this._pan; } public getNumberOfLoops(): number { return this._numberOfLoops; } public setNumberOfLoops(value: number): Sound { this._numberOfLoops = value; if (this._loaded) { RNSound.setNumberOfLoops(this._key, value); } return this; } public setSpeed(value: number): Sound { this._speed = value; if (this._loaded) { RNSound.setSpeed(this._key, value); } return this; } public getCurrentTime(callback: (time: number) => void): void { if (this._loaded) { RNSound.getCurrentTime(this._key, callback); } } public setCurrentTime(value: number): Sound { if (this._loaded) { RNSound.setCurrentTime(this._key, value); } return this; } public isPlaying(): boolean { return this._playing; } } export default Sound;