TeaWeb/web/app/audio/sounds.ts

129 lines
4.5 KiB
TypeScript
Raw Normal View History

import {LogCategory, logError, logWarn} from "tc-shared/log";
2020-03-30 13:44:18 +02:00
import {SoundFile} from "tc-shared/sound/Sounds";
import * as aplayer from "./player";
import { tr } from "tc-shared/i18n/localize";
2020-03-30 13:44:18 +02:00
interface SoundEntry {
cached?: AudioBuffer;
node?: HTMLAudioElement;
}
const error_already_handled = "---- error handled ---";
const file_cache: {[key: string]: Promise<SoundEntry> & { timestamp: number }} = {};
let warned = false;
function get_song_entry(file: SoundFile) : Promise<SoundEntry> {
if(typeof file_cache[file.path] === "object") {
return new Promise<SoundEntry>((resolve, reject) => {
if(file_cache[file.path].timestamp + 60 * 1000 > Date.now()) {
file_cache[file.path].then(resolve).catch(reject);
return;
}
2020-03-18 23:00:24 +01:00
2020-03-30 13:44:18 +02:00
const original_timestamp = Date.now();
return file_cache[file.path].catch(error => {
if(file_cache[file.path].timestamp + 60 * 1000 > original_timestamp)
return Promise.reject(error);
delete file_cache[file.path];
return get_song_entry(file);
});
});
}
2020-03-18 23:00:24 +01:00
2020-03-30 13:44:18 +02:00
const context = aplayer.context();
if(!context) throw tr("audio context not initialized");
2020-03-18 23:00:24 +01:00
2020-03-30 13:44:18 +02:00
return (file_cache[file.path] = Object.assign((async () => {
const entry = {} as SoundEntry;
if(context.decodeAudioData) {
const xhr = new XMLHttpRequest();
xhr.open('GET', file.path, true);
xhr.responseType = 'arraybuffer';
2020-03-18 23:00:24 +01:00
2020-03-30 13:44:18 +02:00
try {
const result = new Promise((resolve, reject) => {
xhr.onload = resolve;
xhr.onerror = reject;
2020-03-18 23:00:24 +01:00
});
2020-03-30 13:44:18 +02:00
xhr.send();
await result;
2020-03-18 23:00:24 +01:00
2020-03-30 13:44:18 +02:00
if (xhr.status != 200)
throw "invalid response code (" + xhr.status + ")";
2020-03-18 23:00:24 +01:00
try {
2020-03-30 13:44:18 +02:00
entry.cached = await context.decodeAudioData(xhr.response);
2020-03-18 23:00:24 +01:00
} catch(error) {
logError(LogCategory.AUDIO, error);
2020-03-30 13:44:18 +02:00
throw tr("failed to decode audio data");
2020-03-18 23:00:24 +01:00
}
2020-03-30 13:44:18 +02:00
} catch(error) {
logError(LogCategory.AUDIO, tr("Failed to load audio file %s. Error: %o"), file, error);
2020-03-30 13:44:18 +02:00
throw error_already_handled;
2020-03-18 23:00:24 +01:00
}
2020-03-30 13:44:18 +02:00
} else {
if(!warned) {
warned = true;
logWarn(LogCategory.AUDIO, tr("Your browser does not support decodeAudioData! Using a node to playback! This bypasses the audio output and volume regulation!"));
2020-03-30 13:44:18 +02:00
}
const container = $("#sounds");
const node = $.spawn("audio").attr("src", file.path);
node.appendTo(container);
2020-03-18 23:00:24 +01:00
2020-03-30 13:44:18 +02:00
entry.node = node[0];
2020-03-18 23:00:24 +01:00
}
2020-03-30 13:44:18 +02:00
return entry;
})(), { timestamp: Date.now() }));
}
export async function play_sound(file: SoundFile) : Promise<void> {
const entry = get_song_entry(file);
if(!entry) {
logWarn(LogCategory.AUDIO, tr("Failed to replay sound %s because it could not be resolved."), file.path);
2020-03-30 13:44:18 +02:00
return;
}
2020-03-18 23:00:24 +01:00
2020-03-30 13:44:18 +02:00
try {
const sound = await entry;
if(sound.cached) {
const context = aplayer.context();
if(!context) throw tr("audio context not initialized (this error should never show up!)");
const player = context.createBufferSource();
player.buffer = sound.cached;
player.start(0);
2020-03-18 23:00:24 +01:00
2020-03-30 13:44:18 +02:00
const play_promise = new Promise(resolve => player.onended = resolve);
if(file.volume != 1 && context.createGain) {
const gain = context.createGain();
if(gain.gain.setValueAtTime)
gain.gain.setValueAtTime(file.volume, 0);
else
gain.gain.value = file.volume;
player.connect(gain);
gain.connect(context.destination);
2020-03-18 23:00:24 +01:00
} else {
2020-03-30 13:44:18 +02:00
player.connect(context.destination);
2020-03-18 23:00:24 +01:00
}
2020-03-30 13:44:18 +02:00
await play_promise;
} else if(sound.node) {
sound.node.currentTime = 0;
await sound.node.play();
} else {
throw "missing playback handle";
}
} catch(error) {
if(error === error_already_handled) {
logWarn(LogCategory.AUDIO, tr("Failed to replay sound %s because of an error while loading (see log above)."), file.path);
2020-03-18 23:00:24 +01:00
return;
}
2020-03-30 13:44:18 +02:00
logWarn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), file.path, error);
2020-03-30 13:44:18 +02:00
return;
2020-03-18 23:00:24 +01:00
}
}