blob: d309ca203b8a9c406ed7213e817baa6be5393924 [file] [log] [blame]
import { Howl, Howler } from 'howler';
// seek tolerance in seconds, keeps the Howl player from seeking unnecessarily
// if the number is too small, Howl.seek() is called too often and creates a popping noise
// too large and audio layers may be skipped over
const kTolerance = 0.75;
/**
* AudioPlayers wrap a howl and control playback through seek calls
*
* @param source - URL or base64 data URI pointing to audio data
* @param format - only needed if extension is not provided by source (inline URI)
*
*/
export class AudioPlayer {
private playing: boolean = false;
private howl: Howl;
constructor(source: string) {
this.howl = new Howl({
src: [source],
preload: true,
});
}
pause(): void {
if (this.playing) {
this.howl.pause();
this.playing = false;
}
}
seek(t: number): void {
if (!this.playing && t >= 0) {
// Sometimes browsers will prevent the audio from playing.
// We need to resume the AudioContext or it will never play.
if (Howler.ctx.state === 'suspended') {
Howler.ctx.resume().then(() => this.howl.play());
} else {
this.howl.play();
}
this.playing = true;
}
if (this.playing) {
if (t < 0) {
this.howl.stop();
this.playing = false;
} else {
const playerPos = this.howl.seek() as number;
if (Math.abs(playerPos - t) > kTolerance) {
this.howl.seek(t);
}
}
}
}
volume(v: number): void {
this.howl.volume(v);
}
}
export class SoundMap {
map: Map<string, AudioPlayer> = new Map()
setPlayer(name: string, player: AudioPlayer): void {
this.map.set(name, player);
}
getPlayer(name: string): AudioPlayer {
return this.map.get(name)!;
}
pause(): void {
for (const player of this.map.values()) {
player.pause();
}
}
stop(): void {
for (const player of this.map.values()) {
player.seek(-1);
}
}
setVolume(v: number): void {
Howler.volume(v);
}
}