for(let size=2;size<=n;size<<=1){ const h=size>>1, st=n/size; for(let s=0;s0?20*Math.log10(sm[k]):-1000; } } }; } const breathe=()=>new Promise(r=>{ const c=new MessageChannel(); c.port1.onmessage=()=>{ c.port1.close(); r(); }; c.port2.postMessage(0); }); const mmss=s=>{ s=Math.max(0,Math.round(s)); return Math.floor(s/60)+':'+String(s%60).padStart(2,'0'); }; let renderStop=false; async function renderVideo(){ if(rendering){ renderStop=true; stat.textContent='stopping the render…'; return; } if(!SONG||!SONG.buf){ 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.buf.duration,pos:(SONG.aud&&SONG.aud.currentTime)||0,fps:FPS,head:HEAD,tail:TAIL,say:t=>{ stat.textContent=t; }}) :{light:false,size:RS.size,mbps:mbps(),from:0,len:SONG.buf.duration,whole:true,slow:''}; if(!P) return; const slow=P.slow||''; pauseAll(''); const fps=FPS, per=Math.max(1,Math.round(SIM/fps)), buf=SONG.buf, len=buf.duration, sr=buf.sampleRate; const ch=Math.min(2,buf.numberOfChannels), pcm=[...Array(ch)].map((_,c)=>buf.getChannelData(c)); rendering=true; renderStop=false; renderBtn.classList.add('on'); renderBtn.textContent='■ STOP RENDER'; stat.textContent='⏺ RENDER — getting the encoder ready…'; const keep={AN,SPEC,TD}; let msg='', R=null, sleep=()=>{}; try{ if(CleanRec.awake) sleep=await CleanRec.awake(); }catch(e){} try{ const STEPS=Math.ceil((HEAD+len+TAIL)*SIM), K0=P.whole||!(P.from>0)?0:Math.round((HEAD+P.from)*SIM/per)*per, KEND=P.whole?STEPS:Math.min(STEPS,K0+Math.round((P.len+(K0?0:HEAD))*SIM)); R=await CleanRec.render({canvas:cv,w:P.size,h:P.size,fps,mbps:P.mbps,head:HEAD-K0/SIM,audio:{pcm,sr,ch}}); if(!R){ msg='this browser cannot render '+P.size+'² at '+fps+' fps'; return; } const ears=makeEars(buf); AN=ears; SPEC=new Float32Array(ears.frequencyBinCount); TD=new Float32Array(ears.fftSize); sizeTo(P.size); freshScene(); PLATE.S.D2=RENDER_D2; playing=false; const steps=Math.ceil((HEAD+len+TAIL)*SIM), hk=Math.round(HEAD*SIM), frames=Math.ceil((KEND-K0)/per), w0=performance.now(); let f=0, ok=true, pure=false; for(let k=0;k=K0){ ok=await R.frame(f++); if(!ok) break; } } if(k%30===0){ const done=f/frames, el=(performance.now()-w0)/1000; stat.textContent=k0.02?' · about '+mmss(el/done*(1-done))+' left':'')+slow; if(playing) readout(t,pure); else read('THE DRUM · at rest'); await breathe(); if(renderStop) break; } } if(renderStop||!ok){ R.cancel(); msg=renderStop?'render stopped · nothing was saved':'the encoder stopped at frame '+f+' · nothing was saved'; return; } stat.textContent='⏺ RENDER — closing the file…'; const res=await R.finish(f/fps); if(!res||!res.blob){ msg='the MP4 could not be closed'+(res&&res.err?': '+res.err:''); return; } const url=URL.createObjectURL(res.blob), a=document.createElement('a'); a.href=url; a.download='render-DRUM-'+new Date().toISOString().replace(/[:.]/g,'-')+(P.light?'-light-'+P.size:'')+'.mp4'; a.click(); setTimeout(()=>URL.revokeObjectURL(url),4000); msg='rendered · MP4 · '+res.fps+' fps · '+res.frames+' frames · '+mmss(res.frames/res.fps)+' · in '+mmss((performance.now()-w0)/1000)+' · nothing skipped'+(res.aerr?' · ⚠ the sound encoder complained':'')+' · nothing uploaded'; }catch(e){ try{ if(R) R.cancel(); }catch(_){} msg='the render stopped: '+(e&&e.message||e); } finally{ sleep(); AN=keep.AN; SPEC=keep.SPEC; TD=keep.TD; delete PLATE.S.D2; playing=false; rendering=false; PLATE.rest(false); renderBtn.classList.remove('on'); renderBtn.textContent='⏺ RENDER'; last=performance.now()/1000; fit(); stat.textContent=msg; } } renderBtn.onclick=renderVideo; window.__PLATE=PLATE; window.__D=D; window.__E=E; window.__VOICE=VOICE; window.__VOICES=VOICES; window.__BES=BES; })();