window.addEventListener('error',e=>say({kind:'error',msg:String(e.message||e)})); window.addEventListener('message',async e=>{ const m=e.data||{}; if(e.source!==window.parent||!m.rnd) return; if(m.kind==='stop'){ stop=true; return; } if(m.kind!=='go') return; try{ const oc=new OfflineAudioContext(2,1,48000), buf=await CleanRec.decode(oc,m.bytes); ensureAudio(); const sr=buf.sampleRate, ch=Math.min(2,buf.numberOfChannels), pcm=[...Array(ch)].map((_,c)=>buf.getChannelData(c)); const L=pcm[0], Rc=pcm[ch-1], len=L.length, dur=buf.duration, gain=()=>(MASTER&&MASTER.gain)?MASTER.gain.value:1; Object.defineProperty(AC,'currentTime',{get:()=>C.t,configurable:true}); if(ANA) ANA.getByteTimeDomainData=a=>{ const p=Math.round((C.t-RHEAD)*sr), n=a.length, k=gain(); for(let i=0;i=0&&j=dur; }, get paused(){ return C.t=dur; }, pause(){}, play(){ return Promise.resolve(); } }; SONG={name:m.name||'song', dur, url:'', aud}; EVENTS=analyze(buf); anchor=RHEAD; playing=true; takeActive=false; startPump(0); const fps=m.fps, per=Math.max(1,Math.round(RSIM/fps)), size=m.size||RS.size; const steps=Math.ceil((RHEAD+dur+RTAIL)*RSIM); const k0=m.whole||!(m.from>0)?0:Math.round((RHEAD+m.from)*RSIM/per)*per; const kEnd=m.whole?steps:Math.min(steps,k0+Math.round((m.len+(k0?0:RHEAD))*RSIM)); let enc=recCv, ecx=rcx; if(size!==RS.size){ enc=document.createElement('canvas'); enc.width=enc.height=size; ecx=enc.getContext('2d'); } const R=await CleanRec.render({canvas:enc,w:size,h:size,fps,mbps:m.mbps,head:RHEAD-k0/RSIM,audio:{pcm,sr,ch}}); if(!R){ say({kind:'error',msg:'this browser cannot render '+size+'² at '+fps+' fps'}); return; } ecx.imageSmoothingEnabled=true; ecx.imageSmoothingQuality='high'; const frames=Math.ceil((kEnd-k0)/per), w0=Date.now(); let f=0; for(let k=0;k=k0&&k%per===0){ ecx.drawImage(cv,0,0,size,size); if(!await R.frame(f++)){ say({kind:'error',msg:'the encoder stopped at frame '+f}); return; } } if(k%30===0){ say({kind:'progress',done:f/frames,t:C.t,total:kEnd/RSIM,el:(Date.now()-w0)/1000,runup:k{ if(!b) return; const f0=b.onclick; b.onclick=e=>{ if(RJ){ stat.textContent=why; return; } if(f0) return f0.call(b,e); }; }; guard(playBtn,'a render is running: Play waits until it is done'); guard(takeBtn,'a render is running: TAKE waits until it is done'); guard(recBtn,'a render is running: REC waits until it is done'); const place=()=>{ if(!RJ) return; const b=cv.getBoundingClientRect(); Object.assign(RJ.fr.style,{left:b.left+'px',top:b.top+'px',transform:'scale('+(b.width/1080)+')'}); }; window.addEventListener('resize',place); const end=msg=>{ if(!RJ) return; try{ RJ.sleep(); }catch(e){} window.removeEventListener('message',RJ.on); clearTimeout(RJ.wait); try{ RJ.fr.remove(); }catch(e){} RJ=null; renderBtn.classList.remove('on'); renderBtn.innerHTML='⏺ RENDER'; if(msg) stat.textContent=msg; }; renderBtn.onclick=async()=>{ if(RJ){ try{ RJ.fr.contentWindow.postMessage({rnd:1,kind:'stop'},'*'); }catch(e){} end('render stopped · nothing was saved'); return; } if(!SONG||!SONG.url){ stat.textContent='LOAD a song first'; return; } if(recording){ stat.textContent='a take is recording: stop it first'; return; } if(typeof CleanRec==='undefined'||!CleanRec.render||!CleanRec.ok()){ stat.textContent='this browser cannot render (it needs WebCodecs: Chrome or Edge on a computer)'; return; } const P=CleanRec.plan?await CleanRec.plan({dur:SONG.dur,pos:(SONG.aud&&SONG.aud.currentTime)||0,fps:FPS,head:RHEAD,tail:RTAIL,say:t=>{ stat.textContent=t; }}) :{light:false,size:RS.size,mbps:mbps(),from:0,len:SONG.dur,whole:true,slow:''}; if(!P) return; const slow=P.slow||''; pauseAll(''); let bytes=null; try{ bytes=await (await fetch(SONG.url)).arrayBuffer(); }catch(e){ stat.textContent='could not read the song again: '+(e&&e.message||e); return; } const fr=document.createElement('iframe'), fps=FPS, name=SONG.name; fr.style.cssText='position:fixed;left:0;top:0;width:1080px;height:1080px;border:0;transform-origin:0 0;z-index:50;background:#000;pointer-events:none'; fr.src=location.href.replace(/#.*$/,'')+'#render'; RJ={fr,on:null,wait:0,sleep:()=>{}}; try{ if(CleanRec.awake) RJ.sleep=await CleanRec.awake(); }catch(e){} renderBtn.classList.add('on'); renderBtn.innerHTML='■ STOP RENDER'; stat.textContent='⏺ RENDER — opening a fresh copy of the board…'; RJ.on=e=>{ if(!RJ||e.source!==fr.contentWindow) return; const m=e.data||{}; if(!m.rnd) return; if(m.kind==='ready'){ clearTimeout(RJ.wait); fr.contentWindow.postMessage({rnd:1,kind:'go',bytes,fps,mbps:P.mbps,size:P.size,from:P.from,len:P.len,whole:P.whole,name},'*',[bytes]); stat.textContent='⏺ RENDER — the copy is listening to the song…'; } else if(m.kind==='progress') stat.textContent=m.runup?'⏺ RENDER'+(P.light?' LIGHT':'')+' — getting to '+rmmss(m.at)+' (nothing filmed yet) · '+rmmss(m.t)+slow :'⏺ RENDER'+(P.light?' LIGHT':'')+' · '+Math.round(m.done*100)+'% · '+rmmss(m.t)+' of '+rmmss(m.total)+' · '+fps+' fps'+(m.done>0.02?' · about '+rmmss(m.el/m.done*(1-m.done))+' left':'')+slow; else if(m.kind==='done'){ const url=URL.createObjectURL(m.blob), a=document.createElement('a'); a.href=url; a.download=recName().replace(/^[a-z]+-/,'render-')+(P.light?'-light-'+P.size:'')+'.mp4'; a.click(); setTimeout(()=>URL.revokeObjectURL(url),4000); end('rendered · MP4 · '+m.fps+' fps · '+m.frames+' frames · '+rmmss(m.frames/m.fps)+' · in '+rmmss(m.secs)+' · nothing skipped'+(m.aerr?' · ⚠ the sound encoder complained':'')+' · nothing uploaded'); } else if(m.kind==='stopped') end('render stopped · nothing was saved'); else if(m.kind==='error') end('the render stopped: '+m.msg); }; window.addEventListener('message',RJ.on); document.body.appendChild(fr); place(); RJ.wait=setTimeout(()=>end('the fresh copy of the board did not open, so nothing was made'),20000); }; } const songIn=document.getElementById('songIn'), loadBtn=document.getElementById('load'), songLbl=document.getElementById('song'); loadBtn.onclick=()=>songIn.click(); songIn.onchange=async e=>{ const f=e.target.files&&e.target.files[0]; if(!f) return; e.target.value=''; try{ stat.textContent='listening through '+f.name+' …'; pauseAll(''); ensureAudio(); const buf=await ((typeof CleanRec!=='undefined'&&CleanRec.decode)?CleanRec.decode(AC,await f.arrayBuffer()):AC.decodeAudioData(await f.arrayBuffer())); EVENTS=analyze(buf); const url=URL.createObjectURL(f); const aud=new Audio(url); aud.preload='auto'; SONG={name:f.name, dur:buf.duration, url, aud}; hooked=false; songLbl.innerHTML='♪ '+f.name.replace(/\.[^.]+$/,'')+' · '+buf.duration.toFixed(1)+'s · '+EVENTS.length+' onsets heard'; stat.textContent='heard '+EVENTS.length+' onsets in three bands · press Play'; } catch(err){ stat.textContent=(typeof CleanRec!=='undefined'&&CleanRec.whyNot)?CleanRec.whyNot(err):'could not hear that file: '+(err&&err.message||err); } }; })();