import {LogCategory, logError, logWarn} from "tc-shared/log"; import * as log from "tc-shared/log"; import {SoundFile} from "tc-shared/sound/Sounds"; import * as aplayer from "./player"; import { tr } from "tc-shared/i18n/localize"; interface SoundEntry { cached?: AudioBuffer; node?: HTMLAudioElement; } const error_already_handled = "---- error handled ---"; const file_cache: {[key: string]: Promise & { timestamp: number }} = {}; let warned = false; function get_song_entry(file: SoundFile) : Promise { if(typeof file_cache[file.path] === "object") { return new Promise((resolve, reject) => { if(file_cache[file.path].timestamp + 60 * 1000 > Date.now()) { file_cache[file.path].then(resolve).catch(reject); return; } 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); }); }); } const context = aplayer.context(); if(!context) throw tr("audio context not initialized"); 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'; try { const result = new Promise((resolve, reject) => { xhr.onload = resolve; xhr.onerror = reject; }); xhr.send(); await result; if (xhr.status != 200) throw "invalid response code (" + xhr.status + ")"; try { entry.cached = await context.decodeAudioData(xhr.response); } catch(error) { logError(LogCategory.AUDIO, error); throw tr("failed to decode audio data"); } } catch(error) { logError(LogCategory.AUDIO, tr("Failed to load audio file %s. Error: %o"), file, error); throw error_already_handled; } } 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!")); } const container = $("#sounds"); const node = $.spawn("audio").attr("src", file.path); node.appendTo(container); entry.node = node[0]; } return entry; })(), { timestamp: Date.now() })); } export async function play_sound(file: SoundFile) : Promise { 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); return; } 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); 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); } else { player.connect(context.destination); } 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); return; } logWarn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), file.path, error); return; } }