49 lines
3.0 KiB
JavaScript
49 lines
3.0 KiB
JavaScript
/* Grady is the locked system announcer. One serialized speaker, never overlapping cues. */
|
|
(() => {
|
|
'use strict';
|
|
function create({clips,canPlay,isPaused,onStart=()=>{},onActivity=()=>{}}){
|
|
const bank={},failed=new Set();let queue=[],active=null,serial=0,playToken=0,starts=0,lastCue='',error='',volume=.45,muted=false;
|
|
function mix(){for(const media of Object.values(bank)){media.volume=Math.max(0,Math.min(1,volume*1.25));media.muted=muted;}onActivity();}
|
|
function clear(){serial++;playToken++;queue=[];if(active){active.media.pause();try{active.media.currentTime=0;}catch(_){}}active=null;onActivity();}
|
|
function finish(){if(active)active.media.pause();active=null;playToken++;next();}
|
|
function next(){
|
|
if(active)return;
|
|
const key=queue.shift();if(!key){onActivity();return;}
|
|
if(failed.has(key)){next();return;}
|
|
const media=bank[key];active={key,media,elapsed:0,serial:++serial};lastCue=key;starts++;error='';try{media.currentTime=0;}catch(_){}
|
|
onStart();mix();sync();
|
|
}
|
|
function sync(){
|
|
if(!active)return;
|
|
const entry=active;
|
|
if(!canPlay()||isPaused()){playToken++;entry.media.pause();onActivity();return;}
|
|
if(!entry.media.paused)return;
|
|
const token=++playToken;
|
|
try{const p=entry.media.play();p?.catch(e=>{if(active!==entry||token!==playToken)return;error=e?.name||'audio-playback-failed';finish();});}
|
|
catch(e){if(active===entry&&token===playToken){error=e?.name||'audio-playback-failed';finish();}}
|
|
}
|
|
for(const [key,clip] of Object.entries(clips)){
|
|
try{
|
|
const media=new Audio();bank[key]=media;media.preload='auto';
|
|
media.addEventListener('ended',()=>{if(active?.media===media)finish();});
|
|
media.addEventListener('error',()=>{failed.add(key);if(active?.media===media){error='audio-load-failed';finish();}});
|
|
media.addEventListener('playing',()=>{if(active?.media!==media||!canPlay()||isPaused()){media.pause();return;}mix();});
|
|
media.src=clip.file;media.load();
|
|
}catch(_){failed.add(key);}
|
|
}
|
|
return {
|
|
play(keys,{replace=true}={}){
|
|
if(!canPlay())return false;
|
|
const valid=(Array.isArray(keys)?keys:[keys]).filter(k=>Object.hasOwn(bank,k)&&!failed.has(k));
|
|
if(!valid.length)return false;if(replace)clear();queue.push(...valid);queue=queue.slice(0,4);next();return true;
|
|
},
|
|
clear,sync,setMix(v,m){volume=v;muted=m;for(const media of Object.values(bank)){media.volume=Math.max(0,Math.min(1,v*1.25));media.muted=m;}},
|
|
tick(dt){if(!active||!canPlay()||isPaused())return;active.elapsed+=dt;const duration=clips[active.key].duration||3;if(active.elapsed>duration+.4)finish();},
|
|
get busy(){return !!active||queue.length>0;},
|
|
get speaking(){return !!active&&!active.media.paused;},
|
|
snapshot(){return {voice:'Grady',cue:active?.key||'',lastCue,active:!!active,paused:active?.media.paused??true,time:active?.media.currentTime||0,readyState:active?.media.readyState||0,starts,error,queue:[...queue]};}
|
|
};
|
|
}
|
|
window.SystemAnnouncer={create};
|
|
})();
|