startRec(()=>{ anchor=AC.currentTime+0.5; startPump(0); setTimeout(()=>{ if(takeActive&&SONG) SONG.aud.play(); }, Math.max(0,(anchor-AC.currentTime)*1000)); stat.textContent='TAKE — head 0.500s · song '+SONG.dur.toFixed(3)+'s · tail 0.500s'; }); } function finishTake(){ takeActive=false; playing=false; if(pump){ clearInterval(pump); pump=null; } if(SONG&&!SONG.aud.paused) SONG.aud.pause(); if(recorder&&recorder.state==='recording') recorder.stop(); playBtn.classList.remove('on'); takeBtn.classList.remove('on'); } playBtn.onclick=play; pauseBtn.onclick=pause; takeBtn.onclick=()=>{ takeActive?finishTake():take(); }; const RS={"size":1080,"fps":{"computer":60,"phone":30},"video_mbps":{"computer_60":16,"computer_30":10,"phone_60":12,"phone_30":8},"light":{"size":720,"video_mbps":{"60":6,"30":4},"parts":[30,60]},"sound":"aac","audio_kbps":192,"opus_kbps":192,"tail_seconds":0.3,"keyframe_every_seconds":2,"clean_take":true,"keyway_mbps":{"computer_60":40,"computer_30":20,"phone_60":16,"phone_30":8}}; const recCv=document.createElement('canvas'); recCv.width=RS.size; recCv.height=RS.size; const rcx=recCv.getContext('2d'); let recorder=null, chunks=[], recording=false, REQ=null, NEXT=0, WC=null; const RECMIME=['video/mp4;codecs=avc1.640028,mp4a.40.2','video/mp4;codecs=avc1.42E01E,mp4a.40.2','video/mp4', 'video/webm;codecs=h264,opus','video/x-matroska;codecs=avc1,opus', 'video/webm;codecs=vp9,opus','video/webm;codecs=vp8,opus','video/webm']; const smallDev=/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent||'')||(navigator.maxTouchPoints>1&&/Mac/i.test(navigator.platform||'')); let FPS=smallDev?RS.fps.phone:RS.fps.computer; const fpsBtn=document.getElementById('fps'); const mbps=()=>RS.video_mbps[(smallDev?'phone':'computer')+'_'+FPS]||(FPS>30?40:20); const showFps=()=>{ if(fpsBtn) fpsBtn.textContent=FPS+' FPS'; }; showFps(); if(fpsBtn) fpsBtn.onclick=()=>{ if(recording){ stat.textContent='the frame rate changes between takes, not during one'; return; } FPS=FPS===60?30:60; showFps(); stat.textContent=FPS===60?'takes at 60 frames a second: as smooth as the screen, about twice the size':'takes at 30 frames a second: lighter, for a phone or a tablet'; }; const recName=()=>'hear-L-'+new Date().toISOString().replace(/[:.]/g,'-'); async function startWC(onRoll,mode){ recording=true; REQ=null; stat.textContent=mode==='song'?'TAKE — reading the song for a clean file…':'getting a clean recording ready…'; let audio=null; if(mode==='song'){ let pcm=null; try{ pcm=await CleanRec.decode(AC,await (await fetch(SONG.url)).arrayBuffer()); }catch(e){} if(pcm){ const ch=Math.min(2,pcm.numberOfChannels); audio={pcm:[...Array(ch)].map((_,c)=>pcm.getChannelData(c)),sr:pcm.sampleRate,ch}; } } else audio={track:recDest.stream.getAudioTracks()[0],sr:AC.sampleRate,ch:2,ctx:AC}; rcx.imageSmoothingEnabled=true; rcx.imageSmoothingQuality='high'; rcx.drawImage(cv,0,0,recCv.width,recCv.height); const R=audio?await CleanRec.start({canvas:recCv,w:RS.size,h:RS.size,fps:FPS,mbps:mbps(),clock:()=>AC.currentTime,audio}):null; if(!R){ recording=false; startRecMR(onRoll); return; } WC=R; R.mode=mode; recorder={state:'recording',mimeType:'video/mp4',stop:()=>{ wcFinish(); }}; recBtn.classList.add('on'); recBtn.textContent='\u23F9 STOP'; if(mode!=='song') stat.textContent='recording a clean take · '+FPS+' fps… click STOP to save'; if(onRoll) onRoll(); if(mode==='song') R.head=Math.max(0,anchor-R.t0); R.frame(true); } async function wcFinish(){ const R=WC; if(!R||R.finishing) return; R.finishing=true; if(recorder) recorder.state='inactive'; let end=AC.currentTime-R.t0; if(R.mode==='song'&&typeof SONG!=='undefined'&&SONG&&SONG.aud&&SONG.aud.ended) end=Math.max(end,R.head+R.len+RS.tail_seconds); stat.textContent='finishing the file…'; const res=await R.finish(end); WC=null; recording=false; recBtn.classList.remove('on'); recBtn.textContent='\u23FA REC'; if(!res||!res.blob){ stat.textContent=res&&res.err?'the MP4 could not be closed: '+res.err:'no picture arrived, so nothing was made · keep the tab in front and try again'; return; } const url=URL.createObjectURL(res.blob), link=document.createElement('a'); link.href=url; link.download=recName()+'.mp4'; link.click(); setTimeout(()=>URL.revokeObjectURL(url),4000); stat.textContent='saved a clean take · MP4 · '+res.fps+' fps · '+res.frames+' frames · colours for every player'+(res.aerr?' · ⚠ the sound encoder complained':'')+(res.skipped?' · '+res.skipped+' frame'+(res.skipped>1?'s':'')+' held (the device was busy · ⏺ RENDER never holds one)':'')+' · nothing uploaded'; } function startRecMR(onRoll){ ensureAudio(); rcx.imageSmoothingEnabled=true; rcx.imageSmoothingQuality='high'; rcx.drawImage(cv,0,0,recCv.width,recCv.height); recording=true; const cs0=recCv.captureStream(0), vt0=cs0.getVideoTracks(); REQ=(vt0[0]&&typeof vt0[0].requestFrame==='function')?vt0[0]:null; NEXT=0; const vtr=REQ?vt0:recCv.captureStream(FPS).getVideoTracks(), atr=recDest?recDest.stream.getAudioTracks():[]; const stream=new MediaStream([...vtr, ...atr]); const mime=RECMIME.find(m=>window.MediaRecorder&&MediaRecorder.isTypeSupported(m))||''; chunks=[]; try{ recorder=new MediaRecorder(stream, mime?{mimeType:mime, videoBitsPerSecond:Math.round(mbps()*1e6), audioBitsPerSecond:RS.audio_kbps*1000}:undefined); } catch(e){ recording=false; stat.textContent='recording is not supported in this browser'; return; } recorder.ondataavailable=e=>{ if(e.data&&e.data.size) chunks.push(e.data); }; recorder.onstop=()=>{ recording=false; REQ=null; const mt=recorder.mimeType||mime||'video/webm'; const blob=new Blob(chunks,{type:mt}); const url=URL.createObjectURL(blob), link=document.createElement('a'); link.href=url; link.download=recName()+(mt.indexOf('mp4')>=0?'.mp4':'.webm'); link.click(); setTimeout(()=>URL.revokeObjectURL(url),2000); recBtn.classList.remove('on'); recBtn.textContent='⏺ REC'; stat.textContent='saved a HD take '+(mt.indexOf('mp4')>=0?'(.mp4)':'(.webm)')+' · nothing uploaded'; }; recorder.onstart=()=>{ if(onRoll) onRoll(); }; recorder.start(1000); recBtn.classList.add('on'); recBtn.textContent='⏹ STOP'; stat.textContent='recording HD '+RS.size+'² · '+FPS+' fps… click STOP to save'; } function startRec(onRoll){ ensureAudio(); if(typeof CleanRec!=='undefined'&&CleanRec.ok()&&AC){ if(onRoll&&typeof SONG!=='undefined'&&SONG&&SONG.url) return startWC(onRoll,'song'); if(recDest&&CleanRec.okLive()) return startWC(onRoll,'live'); } startRecMR(onRoll); } function recTick(){ if(WC) WC.frame(false); else if(REQ){ const n=performance.now(); if(n>=NEXT-4){ REQ.requestFrame(); NEXT+=1000/FPS; if(NEXT{ if(recorder && recorder.state==='recording') recorder.stop(); else startRec(); }; const RHEAD=0.5, RTAIL=0.5, RSIM=60, renderBtn=document.getElementById('render'); const rbreathe=()=>new Promise(r=>{ const c=new MessageChannel(); c.port1.onmessage=()=>{ c.port1.close(); r(); }; c.port2.postMessage(0); }); const rmmss=s=>{ s=Math.max(0,Math.round(s)); return Math.floor(s/60)+':'+String(s%60).padStart(2,'0'); }; if(typeof RenderChild!=='undefined'&&RenderChild){ const C=RenderChild; let stop=false; try{ document.getElementById('bar').style.display='none'; document.body.style.background='#000'; }catch(e){} const say=m=>{ try{ window.parent.postMessage(Object.assign({rnd:1},m),'*'); }catch(e){} }; 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); } }; })();