100 lines
11 KiB
HTML
100 lines
11 KiB
HTML
BLS<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>BLS Patient View</title>
|
|
<style>
|
|
html, body { height: 100%; margin: 0; }
|
|
:root { --bg:#000; }
|
|
.theme-dark { --bg:#000 }
|
|
body { background: var(--bg); display: flex; align-items: center; justify-content: center; }
|
|
#canvas { display: block; }
|
|
#overlay { position: fixed; top: 10px; left: 10px; right: 10px; display: flex; justify-content: space-between; align-items: center; color: #fff; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; opacity: 0.8; }
|
|
#overlay button { padding: 8px 10px; border: none; border-radius: 6px; background: #1f6feb; color: #fff; cursor: pointer; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="canvas"></canvas>
|
|
<div id="overlay" style="display:none">
|
|
<span>Click or tap to enable audio</span>
|
|
</div>
|
|
|
|
<script src="js/bls.js"></script>
|
|
<script src="js/audio.js"></script>
|
|
<script src="js/webrtc.js"></script>
|
|
<script>
|
|
function initPatient(){
|
|
var canvas = document.getElementById('canvas')
|
|
var widthMode = (BLS.loadState().patientWidth||'full');
|
|
function size(){ var w = window.innerWidth, h = window.innerHeight; var fac = (widthMode==='reduced'? 0.7 : 1); canvas.width = Math.floor(w*fac); canvas.height = h }
|
|
function sendViewport(){ try{ var pv = { winW: window.innerWidth, winH: window.innerHeight, canvasW: canvas.width, canvasH: canvas.height }; if(ws && ws.readyState===1){ ws.send(JSON.stringify({type:'patientViewport', payload: pv})) } if(window.NetRTC && NetRTC.send){ NetRTC.send({type:'patientViewport', payload: pv}) } try{ if(window.opener){ window.opener.postMessage({type:'patientViewport', payload: pv}, '*') } }catch(_){ } try{ if(window.BLS){ BLS.createChannel('bls-session').send({type:'patientViewport', payload: pv}) } }catch(_){ } }catch(e){} }
|
|
size(); sendViewport(); window.addEventListener('resize', function(){ size(); sendViewport() })
|
|
var renderer = BLS.Renderer(canvas)
|
|
var chan = BLS.createChannel('bls-session')
|
|
var qs = new URLSearchParams(location.search)
|
|
var hashSid = (location.hash||'').replace('#','')
|
|
var wsSid = qs.get('sid') || hashSid || 'default'
|
|
var wsUrl = qs.get('url') || ('ws://' + (location.hostname||'localhost') + ':8787')
|
|
var ws = null
|
|
var rtcMode = !!qs.get('p2p')
|
|
chan.on(function(msg){ if(!msg || typeof msg !== 'object') return; var apply = Object.assign({}, msg); if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' } if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 } renderer.setState(apply); var st = renderer.getState(); if(st.running){ renderer.start() } else { renderer.stop() } })
|
|
var running = false;
|
|
function wsConnect(){ if(!wsUrl) return; try{ ws = new WebSocket(wsUrl) }catch(e){ return } ws.onopen = function(){ try{ ws.send(JSON.stringify({type:'join', sessionId: wsSid, role:'patient'})) }catch(e){} };
|
|
ws.onmessage = function(ev){
|
|
try{
|
|
var msg = JSON.parse(ev.data);
|
|
if(msg && msg.type==='state'){
|
|
var apply = Object.assign({}, msg.payload||{});
|
|
if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' }
|
|
if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 }
|
|
renderer.setState(apply);
|
|
var nextMode = apply.patientWidth || widthMode;
|
|
if(nextMode !== widthMode){ widthMode = nextMode; size() }
|
|
var st = renderer.getState();
|
|
if(!st.running || st.paused){ stopAudio(); stopProgrammatic(); renderer.stop(); running=false; return }
|
|
if(st.audioEnabled){
|
|
if(st.audioMode==='file'){
|
|
(async function(){ if(!audioFolder){ await ensureFolder() } if(st.audioName){ await loadAudioByName(st.audioName); playBuffer() } })()
|
|
} else {
|
|
stopAudio(); if(!audioUnlocked) bindUnlock(); startProgrammatic(st)
|
|
}
|
|
} else { stopAudio(); stopProgrammatic() }
|
|
if(!running){ renderer.start(); running=true }
|
|
}
|
|
}catch(e){}
|
|
};
|
|
ws.onclose = function(){ ws = null }
|
|
ws.onopen = function(){ try{ sendViewport() }catch(e){} }
|
|
}
|
|
if(!rtcMode && wsUrl){ wsConnect() }
|
|
if(rtcMode){
|
|
var offerStr = prompt('Paste Offer from Admin');
|
|
if(offerStr){ NetRTC.startAnswerer(offerStr).then(function(answer){ alert('Show this Answer to Admin to complete connect'); console.log(answer); try{ navigator.clipboard && navigator.clipboard.writeText(answer) }catch(_){ } }); NetRTC.onMessage(function(msg){ if(msg && msg.type==='state'){ var apply = Object.assign({}, msg.payload||{}); if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' } if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 } renderer.setState(apply); var st = renderer.getState(); if(!st.running || st.paused){ stopAudio(); stopProgrammatic(); renderer.stop(); return } if(st.audioEnabled){ if(st.audioMode==='file'){ (async function(){ if(!audioFolder){ await ensureFolder() } if(st.audioName){ await loadAudioByName(st.audioName); playBuffer() } })() } else { stopAudio(); if(!audioUnlocked) bindUnlock(); startProgrammatic(st) } } else { stopAudio(); stopProgrammatic() } renderer.start() } }); try{ sendViewport() }catch(e){} }
|
|
}
|
|
renderer.setState(BLS.loadState())
|
|
var st = renderer.getState(); if(st.running){ renderer.start() }
|
|
var audioCtx = null, panner = null, gain = null, source = null, audioFolder = null, audioBuffer = null
|
|
var audioUnlocked = false
|
|
var pendingProgramState = null
|
|
function bindUnlock(){ var ov = document.getElementById('overlay'); if(ov){ ov.style.display='flex' } ['pointerdown','keydown','touchstart','mousemove'].forEach(function(ev){ window.addEventListener(ev, function once(){ try{ AudioEngine.ensureCtx(); audioUnlocked = true; AudioEngine.playPing(0) }catch(e){} if(ov){ ov.style.display='none' } if(pendingProgramState){ var st = pendingProgramState; pendingProgramState = null; startProgrammatic(st) } try{ window.removeEventListener(ev, once) }catch(_){ } }, { once:true }) }) }
|
|
bindUnlock()
|
|
var progRunning = false
|
|
function stopProgrammatic(){ AudioEngine.stopNoise(); progRunning=false }
|
|
function startProgrammatic(st){ if(!audioUnlocked){ pendingProgramState = st; bindUnlock(); return } AudioEngine.setVolume(st.volume||0.6); if(st.programPreset==='pink'){ if(st.syncPan){ renderer.setOnUpdate(function(info){ var pan=(info.x/info.w)*2-1; }); AudioEngine.startPinkNoisePan(Math.max(0.1, st.panRate||0.5)) } else { AudioEngine.startPinkNoisePan(Math.max(0.1, st.panRate||0.5)) } progRunning=true } else if(st.programPreset==='hybrid'){ AudioEngine.startHybrid(Math.max(0.1, st.cueRate||1.2)); progRunning=true } else { renderer.setOnUpdate(function(info){ var r = renderer.getState().size; var hitL = info.x<=r+0.1; var hitR = info.x>=info.w-r-0.1; var hitT = info.y<=r+0.1; var hitB = info.y>=info.h-r-0.1; if(hitL||hitR||hitT||hitB){ var pan = hitL? -1 : hitR? 1 : 0; var pre = st.programPreset; if(pre==='tone'){ AudioEngine.playTone({ freq: st.toneFreq||440, durationMs: st.toneDurationMs||70, pan: pan, adsr:{ attackMs: st.adsrAttackMs||8, decayMs: st.adsrDecayMs||80, sustain: st.adsrSustainLevel||0.3, releaseMs: st.adsrReleaseMs||10 } }) } else if(pre==='click'){ AudioEngine.playClick(pan) } else if(pre==='ping'){ AudioEngine.playPing(pan) } else if(pre==='woodblock'){ AudioEngine.playWoodblock(pan) } else if(pre==='bell'){ AudioEngine.playBell(pan) } else if(pre==='bass'){ AudioEngine.playBass(pan) } else if(pre==='kick'){ AudioEngine.playKick(pan) } else if(pre==='snare'){ AudioEngine.playSnare(pan) } else if(pre==='hihat'){ AudioEngine.playHiHat(pan) } else if(pre==='sweep'){ AudioEngine.playSweep(pan) } else if(pre==='zap'){ AudioEngine.playZap(pan) } else if(pre==='bubble'){ AudioEngine.playBubble(pan) } else { AudioEngine.playWoodblock(pan) } } }); progRunning=true } }
|
|
function ensureAudio(){ if(!audioCtx){ audioCtx = new (window.AudioContext||window.webkitAudioContext)(); panner = audioCtx.createStereoPanner ? audioCtx.createStereoPanner() : null; gain = audioCtx.createGain(); if(panner){ panner.connect(gain) } gain.connect(audioCtx.destination) } if(audioCtx.state==='suspended'){ audioCtx.resume() } }
|
|
async function chooseAudioFolder(){ ensureAudio(); if(window.showDirectoryPicker){ try{ audioFolder = await window.showDirectoryPicker(); var st0 = BLS.loadState(); if(st0.audioEnabled && st0.audioName){ await loadAudioByName(st0.audioName); if(st0.running) playBuffer() } }catch(e){ return } } else { var input = document.createElement('input'); input.type='file'; input.webkitdirectory=true; input.multiple=true; input.addEventListener('change', function(){ var st0 = BLS.loadState(); var files = Array.from(input.files); var match = files.find(function(f){ return f.name === st0.audioName }); if(match){ var fr = new FileReader(); fr.onload = function(){ audioCtx.decodeAudioData(fr.result).then(function(buf){ audioBuffer = buf; if(st0.running && st0.audioEnabled) playBuffer() }) }; fr.readAsArrayBuffer(match) } }); input.click(); }
|
|
}
|
|
async function loadAudioByName(name){ if(!name) return; ensureAudio(); if(!audioFolder) return; try{ let handle = null; for await (const [n,h] of audioFolder.entries()){ if(h.kind==='file' && n===name){ handle=h; break } } if(!handle) return; var file = await handle.getFile(); var buf = await file.arrayBuffer(); audioBuffer = await audioCtx.decodeAudioData(buf); }catch(e){} }
|
|
function playBuffer(){ if(!audioBuffer || !audioCtx) return; if(source){ try{ source.stop() }catch(e){} source.disconnect() } source = audioCtx.createBufferSource(); source.buffer = audioBuffer; source.loop = true; var nodeChainStart = source; if(panner){ nodeChainStart.connect(panner) } else { nodeChainStart.connect(gain) } source.start() }
|
|
function stopAudio(){ if(source){ try{ source.stop() }catch(e){} source.disconnect(); source=null } }
|
|
async function ensureFolder(){ if(!audioFolder){ try{ await chooseAudioFolder() }catch(e){} } }
|
|
renderer.setOnUpdate(function(info){ if(panner){ var pan = (info.x / info.w) * 2 - 1; if(pan < -1) pan=-1; if(pan>1) pan=1; try{ panner.pan.value = pan }catch(e){} } })
|
|
chan.on(async function(msg){ if(!msg || typeof msg!=='object') return; if(!msg.running || msg.paused){ stopAudio(); stopProgrammatic(); renderer.stop(); return } if(msg.audioEnabled){ if(msg.audioMode==='file'){ if(!audioFolder){ await ensureFolder() } if(msg.audioName){ await loadAudioByName(msg.audioName); playBuffer() } } else { stopAudio(); if(!audioUnlocked) bindUnlock(); startProgrammatic(msg) } } else { stopAudio(); stopProgrammatic() } })
|
|
}
|
|
function wait(){ if(window.BLS){ initPatient() } else { setTimeout(wait, 50) } }
|
|
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', wait) } else { wait() }
|
|
</script>
|
|
</body>
|
|
</html>
|