-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbattito.html
More file actions
78 lines (78 loc) · 11.6 KB
/
Copy pathbattito.html
File metadata and controls
78 lines (78 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<meta charset="UTF-8"><meta name="viewport" content="width=device-width"><script>onload=()=>{document.title="🎼 Battito Beats";//https://github.com/bacionejs/battito
element("style").textContent=`
*{margin:0;padding:0;box-sizing:border-box;scrollbar-width:none;user-select:none;touch-action:manipulation} body{background:#121212;color:white;display:flex;font:bold .5rem monospace} html{overscroll-behavior:none}
.piano,.sequencer,.sliders,.cell{display:grid} .side{position:relative} .cell{place-items:center;aspect-ratio:1;border:1px solid #2a2a2a;transition: background 0.4s ease-out}
.piano{color:#2a2a2a;line-height:0} .piano>.cell:nth-child(12n+1){color:gray} .fullscreen{position:absolute;bottom:0;right:0;z-index:1;font-size:1rem;color:orange} :fullscreen .fullscreen{display:none}
.sliders{place-items:center;white-space:pre} .slider{height:1px;accent-color:blue} input::-webkit-slider-thumb{opacity:0} label{pointer-events:none}
.sequencer>.cell:nth-child(-n+9){position:sticky;top:0} .sequencer{aspect-ratio:1;overflow-y:scroll} .text{all:inherit;padding:1rem;white-space:pre;overflow:auto} .text,.canvas{height:25%;aspect-ratio:2/1}`;
let SC=8,SR=60,PC=48,PR=32,C3=12*7+12*3+3,SAMPLERATE=44100*60/4,STEPRATE=PR*60/4,cP,pP,pS,cS,cSs,cSi,cVi,S,P,LT=0;
let BS=Battito.toString(),A=new AudioContext(),SONG=instruments().presets,C=Array(SC).fill(0),R=Array(SR).fill(0),RK=[...R.keys()],PA=Array(PC*PR);R[-1]=C[-1]=1;
let piano=element("div",document.body,"piano"),sliders=element("div",document.body,"sliders"),side=element("div",document.body,"side"),sequencer=element("div",side,"sequencer");
let text=element("textarea",side,"text"),X=element("canvas",side,"canvas").getContext("2d"),fullscreen=element("div",side,"fullscreen","⛶");
let widgets=[piano,sliders,sequencer,text], update=()=>widgets.forEach(o=>o.update()), track=()=>{return C[LT]&&SONG.tracks[LT]}, color=i=>["#f55","#fc2","#cf0","#6fd","#4cf","#74f","#f48","#fa4"][i]||""
piano .update=function(){phrase().forEach((v,i)=>this.cells[i].style.background=color(v))}
sequencer .update=function(){this.cells.forEach(e=>state(e,(active,note,body,sequence)=>{e.style.background=!active?"":!note?"gray":color(e.col);body&&(e.textContent=sequence||"")}))}
sliders .update=function(){let t=track();this.cells.forEach(s=>{s.disabled=!t;s.value=t?t[s.key]:0;s.previousSibling.textContent=s.key+label(+s.value,s.max)})}
text .update=function(){if(this==document.activeElement)return;let s=this.selectionStart;this.value=format(SONG);this.setSelectionRange(s,s)}
piano .onclick=function({target:{row,col}}){let n=col+C3,p;preview(n);if(!(p=pattern()))return;p[row]=p[row]==n?0:n;update()}
sequencer .onclick=function({target:{row,col}}){let hr=!~col,hc=!~row;hr&hc?toggleall():hr|hc?((hr?R:C)[row+col+1]^=1,hc&&(LT=col,preview())):incrementid(row,col),update(),start()}
sliders .oninput=function({target:{key,value}}){(track()[key]=+value,update(),preview())}
text .oninput=function(){try{let song=format(text.value);SONG=song;update();preview()}catch{}}
fullscreen .onclick=function(){document.documentElement.requestFullscreen()}
makelongpress(X.canvas,save); oncontextmenu=e=>e.target.tagName=="TEXTAREA"; grid(piano,PC,PR); naturals(piano); grid(sequencer,SC+1,SR+1); instruments().init(sliders); compatibility()
sliders.cells.forEach((v,i)=>v.previousSibling.style.color=color(i<5?0:i<10?1:i<15?2:i<18?3:i<23?4:i<25?5:i<27?6:7)); fetch('tutorial.js').then(r=>r.text()).then(c=>eval(c))
function start(){try{S?.stop?.(); (S=source(range()))&&(S.start(S.startTime=A.currentTime),loop())}catch{}}
function source(s){let n;return s&&(n=Object.assign(A.createBufferSource(),{buffer:Battito(A).createbuffer(s)}),n.connect(A.destination),n)}
function loop(){if(!S||!next())return;if(cS!=pS){pS=cS;piano.update();scroll()}if(cP!=pP){draw();sequencer.update();playhead(cP);pP=cP}requestAnimationFrame(loop)}
function playhead(c){piano.cells.forEach(v=>v.style.transform="");for(let i=PC;i--;){PA[c*PC+i]>=0&&(piano.cells[c*PC+i].style.transform="scale(2)");}}
function scroll(){let t=sequencer.cells[cSi],c=t.parentElement;c.scrollTo({top:t.offsetTop-(c.clientHeight/2)+(t.clientHeight/2),behavior:"smooth"})}
function next(){let s=(A.currentTime-S.startTime)*SONG.bpm/STEPRATE;if(s>=cSs.length)return start();cS=cSs[s|0];cP=(cVi=s*PR|0)%PR;cSi=1+(cS+1)*(SC+1);return 1}
function toggleall(){let c=0,r=0,x;SONG.tracks.forEach((t,i)=>t.s.forEach((v,j)=>v&&(i>c&&(c=i),j>r&&(r=j))));x=R.some((v,i)=>i<=r&!v)|C.some((v,i)=>i<=c&!v);R.fill(x?1:0,0,r+1);C.fill(x?1:0,0,c+1);}
function incrementid(row,col){let t=SONG.tracks[col],np=t.s[row]=((t.s[row]??0)+1)%10;np&&(t.p[np-1]??=Array(PR).fill(0))}
function pattern(){let t,s;return (t=track())&&~(s=R.indexOf(1))&&s==R.lastIndexOf(1)&&t.s[s]&&(t.p[t.s[s]-1]??=Array(PR).fill(0))}
function phrase(){let t,d=i=>C[i]&&(t=SONG.tracks[i]).p[t.s[cS]-1]?.map((n,s)=>n&&(PA[s*PC+n-C3]=i));PA.fill(-1);for(let i=0;i<SC;i++)d(i);d(LT);return PA}
function range(){let c,r;return(c=SONG.tracks.filter((_,t)=>C[t])).length&&(r=cSs=RK.filter(i=>R[i]&&c.some(t=>t.s[i]))).length&&{...SONG,tracks:c.map(t=>({...t,s:r.map(s=>t.s[s])}))}}
function state(e,cb){let r=e.row,c=e.col,b=~c&&~r,t=SONG.tracks[c],s=b&&t.s[r];return cb((~r||~c)&&R[r]&&C[c],r==cS&&t?.p[s-1]?.[cP],b,s)}
function hasnotes(){return range()?.tracks?.some(t=>t.s.some(s=>s&&t.p[s-1]?.some(Boolean)))||alert("Nothing to save. Please select song or range")}
function preview(n){try{let t=track();t&&(P?.stop(),P=source({...SONG,tracks:[{...t,s:[1],p:[{length:PR,0:n||t.p?.[0]?.find(Boolean)||147}]}]}),P.start(),draw(0,P,t.ea+t.es+t.er,0))}catch{}}
function draw(c=1,s=S,l=SAMPLERATE/SONG.bpm|0,o=cVi*l){let d=s?.buffer?.getChannelData(0),{w,m}=candy(c);X.beginPath();for(let x=w;x--;)X.lineTo(x,m+(d[o+(x*l/w|0)])*m);X.stroke()}
function candy(c){let {width:w,height:h}=X.canvas;X.lineWidth=2;X.fillStyle=c?"#12121255":"#121212";X.fillRect(0,0,w,h);X.strokeStyle=c?`hsl(${cP*90},100%,50%)`:"blue";return {w,m:h/2}}
function save(){if(!hasnotes())return;let s="song"+Date.now(),d=(b,z)=>{Object.assign(d.a??=element("a"),{href:URL.createObjectURL(b),download:s+z}).click()};d(wave(),".wav");d(html(),".html")}
function html(){return new File([`<\script>onload=()=>{\ndocument.body.textContent='Click to play';addEventListener('click',music(),{once:true});\n\n${music()}\n\n}</\script>`],"")}
function music(){return `function music(){\nlet A=new AudioContext(),S=source(${format(range())});\n${source}\n${BS}\nreturn ()=>{S.loop=true;S.start()}\n}`}
function wave(){let os=44,d=S.buffer.length*4,o=new ArrayBuffer(d+os),v=new DataView(o);riff(o);v.setUint32(4,d+36,1);v.setUint32(40,d,1);pcm(v,os);return new Blob([o],{type:"audio/wav"})}
function riff(o){new Uint8Array(o).set([82,73,70,70,0,0,0,0,87,65,86,69,102,109,116,32,16,0,0,0,1,0,2,0,68,172,0,0,16,177,2,0,4,0,16,0,100,97,116,97,0,0,0,0])}
function pcm(v,os){for(let j=0,i,s;j<S.buffer.length;j++)for(i=0;i<2;i++,os+=2)v.setInt16(os,(s=S.buffer.getChannelData(i)[j])*32767-(s<0),1)}
function grid(p,c,r){p.style.gridTemplateColumns="repeat("+c+",1fr)";p.cells=Array.from({length:c*r},(_,i)=>{let e=element("div",p,"cell");e.col=i%c;e.row=i/c|0;return c&1?header(e):e})}
function header(c){let r=--c.row,t=--c.col,v;!~r?(~t&&(v=String.fromCharCode(65+t),c.style.color=color(t))):!~t&&(v=r+1);c.textContent=v;return c}
function naturals(p){p.cells.forEach(c=>c.textContent=c.row%4?"":"C,,D,,E,F,,G,,A,,B".split(",")[c.col%12])}
function label(v,m){return String(m==4?["Off","High","Low","Band","Notch"][v]:m==3?["Sine","Square","Saw","Tri"][v]:m==1?["Off","On"][v]:v).padStart(10," ")}
function format(data){return (typeof data=="object")?JSON.stringify(data).replace(/{/g,"\n$&").replace(/\x22/g,""):JSON.parse(data.replace(/(\w+):/g,'"$1":'))}
function compatibility(){setTimeout(()=>{let r=piano.getBoundingClientRect();if(r.width/r.height<1)document.body.innerHTML="Rendering error<br>Requires a modern chromium-based browser"},200)}
function makelongpress(e,c){let t;e.onpointerdown=_=>{clearTimeout(t);t=setTimeout(c,800)};e.onpointerup=e.onpointercancel=_=>clearTimeout(t)}
function element(tag,p=document.body,c="",v=""){let e=p.appendChild(document.createElement(tag));if(c)e.className=c;if(v)e.textContent=v;return e}
function instruments(){
let schema={v1:255,w1:3,o1:16,s1:11,d1:255,v2:255,w2:3,o2:16,s2:11,d2:255,ea:2e5,es:2e5,er:2e5,e1:1,e2:1,ct:4,ca:11025,cr:255,mw:3,ms:16,ma:255,m1:1,mc:1,ds:16,da:248,ps:16,pa:255,nv:255,vm:255};
return {init:(s)=>{s.cells=Object.entries(schema).map(([key,max])=>(element("label",s,"",key),Object.assign(element("input",s,"slider"),{type:"range",key,max,disabled:true})))},presets:{bpm:100,tracks:[
[255,0,9,0,0,255,0,7,0,0,100,0,5970,1,1,2,500,254,0,0,0,0,0,1,31,4,21,0,171],[255,2,6,11,0,255,2,6,11,4,88,2000,7505,0,0,2,3144,51,0,7,179,0,1,6,60,4,64,0,255],[192,2,7,0,0,201,3,7,0,0,100,150,7505,0,0,2,
5839,254,0,6,195,0,1,6,121,6,147,0,191],[255,0,9,0,0,255,0,9,0,12,100,0,14545,0,0,0,0,240,0,0,0,0,0,2,157,3,47,0,70],[255,3,7,0,0,255,0,8,0,0,22,22,2193,0,0,3,4067,176,0,3,96,0,1,4,12,2,84,127,255],[255,2,
7,0,0,154,2,7,0,9,2418,1075,10614,0,0,3,2962,255,0,5,124,0,1,6,117,3,73,0,240],[192,3,7,0,0,201,3,7,0,7,789,1234,13636,0,0,2,5839,254,0,6,195,0,1,6,121,6,147,0,191],
[192,2,7,0,0,192,2,7,0,0,0,0,20000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192]].map(t=>({...Object.keys(schema).reduce((o,k,i)=>(o[k]=t[i],o),{}),s:[],p:[]}))}}}
function Battito(audiocontext){
let {PI,imul,sin,max,ceil,min,floor,abs}=Math,SAMPLERATE=44100,FBASE=1.5*PI/SAMPLERATE,R_S=1,rnd=()=>((R_S=imul(R_S,0x15a4e35))>>>0)/2**32,NOTE=new Float32Array(256);for(let n=256;n--;)NOTE[n]=2**((n-128)/12)/256;
let WS=2048,WMASK=WS-1,W=Array.from({length:4},()=>new Float32Array(WS)),[SIN,SQUARE,SAW,TRI]=W;for(let i=0,p,v;i<WS;i++){SIN[i]=sin((p=i/WS)*2*PI);SQUARE[i]=p<.5?-1:1;SAW[i]=p-.5;TRI[i]=(v=p*4)<2?v-1:3-v}
return(Battito=(o=>()=>o)({createbuffer(song){
let SR=max(...song.tracks.map(t=>t.s.length)),PR=max(...song.tracks.flatMap(t=>t.p.map(p=>p.length)));
let rowlen=ceil(SAMPLERATE*((60/song.bpm)/4)),TR=SR*PR,S=TR*rowlen,[LL,RR,L,R]=Array.from({length:4},()=>new Float32Array(S));
for(let T of song.tracks){
let {v1,w1,o1,s1,d1,v2,w2,o2,s2,d2,ea,es,er,e1,e2,ct,ca,cr,mw,ms,ma,m1,mc,ds,da,ps,pa,nv,vm}=T,envlen=ea+es+er,ai=1/ea,ri=1/er,lfostep=2**(ms-8)/rowlen,panstep=2**(ps-8)/rowlen,MW=W[mw],W1=W[w1],W2=W[w2];
L.fill(0);R.fill(0);ca*=FBASE;vm*=0.006;ma/=512;pa/=512;v1/=255;v2/=255;nv/=255;cr/=255;s1+=(o1-8)*12;d1=1+0.2*d1/255;s2+=(o2-8)*12;d2=1+0.2*d2/255;
for(let row=0;row<TR;row++){
let n=T.p?.[T.s[row/PR|0]-1]?.[row%PR],P=row*rowlen;if(!n)continue; let o1fb=NOTE[n+s1]*d1,o2fb=NOTE[n+s2]*d2,c1=0,c2=0,low=0,band=0,lfo=lfostep*P,pan=panstep*P,end=min(P+envlen,S);
for(let b=P,j=0;b<end;b++,j++){
let lfoR,e=1,f,rs,high,t,t1=o1fb,t2=o2fb; lfo+=lfostep;pan+=panstep;lfoR=MW[((lfo*WS)&(WMASK))>>>0]*ma+0.5;
if(j<ea)e=j*ai;else if(j>=ea+es)e-=(j-ea-es)*ri;if(m1)t1+=lfoR;if(e1)t1*=e*e;c1+=t1;rs=W1[((c1*WS)&(WMASK))>>>0]*v1;if(e2)t2*=e*e;c2+=t2;rs+=W2[((c2*WS)&(WMASK))>>>0]*v2;if(nv)rs+=(rnd()*2-1)*nv*e;rs*=e*vm;
f=ca*(mc?lfoR:1);low+=f*band;high=cr*(rs-band)-low;band+=f*high; rs=ct==1?high:ct==2?low:ct==3?band:ct==4?low+high:rs; t=SIN[((pan*WS)&(WMASK))>>>0]*pa+0.5;L[b]+=rs*(1-t);R[b]+=rs*t;}}
let DS=floor(ds*rowlen/2);if(DS>0){let DA=da/255;for(let b=0;b<S-DS;b++){L[b+DS]+=R[b]*DA;R[b+DS]+=L[b]*DA}} for(let b=0;b<S;b++){LL[b]+=L[b];RR[b]+=R[b]}}
let X=0;for(let i=0;i<S;i++){X=max(X,abs(LL[i]),abs(RR[i]))} if(X>0){let scale=1/X;for(let i=0;i<S;i++){LL[i]*=scale;RR[i]*=scale}}
return[LL,RR].reduce((buffer,data,i)=>(buffer.copyToChannel(data,i),buffer),audiocontext.createBuffer(2,LL.length,SAMPLERATE))}}))()}}</script>