2021-04-25 11:26:23 +02:00

160 lines
4.2 KiB
TypeScript

export class AudioController {
private audio = new Audio()
private handle = -1
private volume = 1.0
private fadeDuration = 200
private buffer = new Audio()
ontimeupdate: (value: number) => void = () => { /* do nothing */ }
ondurationchange: (value: number) => void = () => { /* do nothing */ }
onended: () => void = () => { /* do nothing */ }
onerror: (err: MediaError | null) => void = () => { /* do nothing */ }
currentTime() {
return this.audio.currentTime
}
duration() {
return this.audio.duration
}
setBuffer(url: string) {
this.buffer.src = url
}
setVolume(value: number) {
this.cancelFade()
this.volume = value
this.audio.volume = value
}
async pause() {
await this.fadeOut()
this.audio.pause()
}
async resume() {
this.audio.volume = 0.0
await this.audio.play()
this.fadeIn()
}
async seek(value: number) {
await this.fadeOut(this.fadeDuration / 2.0)
this.audio.volume = 0.0
this.audio.currentTime = value
await this.fadeIn(this.fadeDuration / 2.0)
}
async changeTrack(url: string, options: { paused?: boolean } = {}) {
if (this.audio) {
this.cancelFade()
endPlayback(this.audio, this.fadeDuration)
}
this.audio = new Audio(url)
this.audio.onerror = () => {
this.onerror(this.audio.error)
}
this.audio.onended = () => {
this.onended()
}
this.audio.ontimeupdate = () => {
this.ontimeupdate(this.audio.currentTime)
}
this.audio.ondurationchange = () => {
this.ondurationchange(this.audio.duration)
}
this.ondurationchange(this.audio.duration)
this.ontimeupdate(this.audio.currentTime)
this.audio.volume = 0.0
if (options.paused !== true) {
try {
await this.audio.play()
} catch (error) {
if (error.name === 'AbortError') {
console.warn(error)
return
}
throw error
}
this.fadeIn()
}
}
private cancelFade() {
clearTimeout(this.handle)
}
private fadeIn(duration: number = this.fadeDuration) {
this.fadeFromTo(0.0, this.volume, duration).then()
}
private fadeOut(duration: number = this.fadeDuration) {
return this.fadeFromTo(this.volume, 0.0, duration)
}
private fadeFromTo(from: number, to: number, duration: number) {
console.info(`AudioController: start fade (${from}, ${to}, ${duration})`)
const startTime = Date.now()
const step = (to - from) / duration
if (duration <= 0.0) {
this.audio.volume = to
}
clearTimeout(this.handle)
return new Promise<void>((resolve) => {
const run = () => {
if (this.audio.volume === to) {
console.info(
'AudioController: fade result. ' +
`duration: ${duration}ms, actual: ${Date.now() - startTime}ms, ` +
`volume: ${this.audio.volume}`)
resolve()
return
}
const elapsed = Date.now() - startTime
this.audio.volume = clamp(0.0, this.volume, from + (elapsed * step))
this.handle = setTimeout(run, 10)
}
run()
})
}
}
function endPlayback(audio: HTMLAudioElement, duration: number) {
async function fade(audio: HTMLAudioElement, from: number, to: number, duration: number) {
if (duration <= 0.0) {
audio.volume = to
return audio
}
const startTime = Date.now()
const step = (to - from) / duration
while (audio.volume !== to) {
const elapsed = Date.now() - startTime
audio.volume = clamp(0.0, 1.0, from + (elapsed * step))
await sleep(10)
}
return audio
}
console.info(`AudioController: ending payback for ${audio}`)
audio.ontimeupdate = null
audio.ondurationchange = null
audio.onerror = null
audio.onended = null
const startTime = Date.now()
fade(audio, audio.volume, 0.0, duration)
.catch((err) => console.warn('Error during fade out: ' + err.stack))
.finally(() => {
audio.pause()
console.info(`AudioController: ending payback done. actual ${Date.now() - startTime}ms`)
})
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
function clamp(min: number, max: number, value: number) {
return Math.max(min, Math.min(value, max))
}