/*
Piano soundfont and demo. Created by Tonny Espeset
This is my shadertoy demo ported to dittytoy:
https://www.shadertoy.com/view/sfcXRn
My Quest for a realistic sounding piano soundfont. The basic idea is to use a high frequency CPU
generated sound with a low frequency control signal sampled from a real piano.
*/
ditty.bpm = 120;
const KEY =
'B{4WXKsY@>I6Xi(JwPubI6Xi(JwP};KsY@>Eg?BQKsh}?9x6FLLODJ{EI1%MLO4A@AUG^NKQ=r+EHN}CI6XK$KsY@>L^wS_L^v!&'
+ 'BrrchI6p!-KSD$}KSD%AI58zXKsY@>IYk~SJwiD>LPR(tFFQauJ3vAsIWIduM>#t`KsYEgJ3u%)KsY-<I6FWzHz+$mH#<N#J3vD>'
+ 'J3vE2HzqPWK{q==H#<Q?H#<Q?LohcaJU}-iFFZglA~!uiLN+}>LN+fWJ3cl$K0_QgC_6nsD>NTFJwi4fDmy+lJ3c}lDmFSjHaa~!'
+ 'FC#ueJ3c}?KSD=4KSD@5KtnVrJ3vD#H#<N>Mkq8pKto46BQHNfJ3m4@KSD=4KSD@5KtnVrJ3vD_H!DCxMl?GpKto3_BRfArJ3m4o'
+ 'EIUU&K|4M{M;$3UJw!V_L>@&dIXyx-JwhWdH$yu<H#<H;HZUYPKQ=i(JuxLVIWaalF+C+VI59mnI5sLCB{@AgJwiP~MM*tEMM+61'
+ 'F*Q9zM?FMGJw!)IJw!)INj)kyJwio2LPba?Gdw~?F-avoLPaDjOE5n|L_b1AKSD$>OFtwmLPShJF(oWSNkBwNOCBmeLPb4EA1gpc'
+ 'AS_5cKSVq~MLa)3JU<{TJU=)*KRG-<HatHxAS^&cNkBwQKtxGEL?A3QI6X8tKr}c#G&mqEGdVUqIX^r?KSe@FG$1TJI6yr_KuJVQ'
+ 'Nkkw$EJHv>LrF$VNl#2tEJ7eiMN3FdOHoc#QCCz>QA<upOGQXRML$A3KRG-$IWsmfHaRdXAT~HTI5smjIWaalFf1T8I5{{rGB!Ce'
+ 'HU';
const VEL =
'0|W&J1_=oV2L}ZP2L%WU2nPiQ2L%QP1_TBL2M7fT2?h!a5DN(m3k(hn4+{$p3keDd2MGrW2nq)X2LuTO2M7rV1q1~J5DEka4+#bh'
+ '1_cNO1qB5J1p^KS2Luid2n7cQ2nYxX2@nSa3k3xT2oMMf1PBTW2n!7d1q2EV5d#Sh4h0AV2nh%X1P28N1qB8N1quZQ0|EsF0|g8P'
+ '0t5{R2L}WM0|o^K2L=ie2MG)i5(f(i3k?bj1_}uU2L}ZR2LuHL3I_xS2L%ZU2M`De3knDn2?+@c3JnVd1PKWV2L}uX1qcNO2LcEP'
+ '1O){H0t5#K2nP}b1_~Am2M7%g2Mh}f3kL}V2?+@Z2n7rW1q28M1_uKK1_c)c2@4en2?-Dn2MY)Y4+{wi4h;(p2?rGk4G9eg2n-7j'
+ '3k((w3=9qo77hyt3keAe4GRwn3k(Pe3knSj3K0nm3keJk3<e4c2nY!a4+sbg2?+@W1`P%Y1_cZZ3JwMb2L}fT3kL=R1rrJf1{D+v'
+ '2M!4a3<w7Z2nGle2?z=j77z*w0SE~Q2nrPq4iE_p5Ec#)4G#?s777sv2?huX3kwbn77+^!3Jn(!4Gs(p3k?hp6b%Rt3JVGl2?z=a'
+ '1qTcX3J?ei4G0Md2L}lY3JDJg3l0bg5C{qn1qcoc6AB9y2nY%U2nh-c3J(hj4G9Ja1qKBN1_uic2@ehn4hjeg4G0Jh4igg*6A%y%'
+ '4Gj$q3=I$v6ciE@6B-s44iOF%6&4p57Z@2G8xt545e^Ry4Fw4X1_=lW3kwYm3=Rto3J47e1_}rX2@4Ml3lR(n4+;nk2nPoV4Gahj'
+ '3I';
const DUR =
'fq{X4eS3L!ba8EIWL<xMe|>v+c5`rTXk%Scb#--eZ);{?aBx>jXJT7VaAj~`RAO3Ab76B>NoHSEgM)*Efq#8_cy)4aX=q_tPl17f'
+ 'fq{R0dU$qoZ);^`UQ|tRaBy&JXJT7WaBy&MY-nR#QgCl?Z)|8{T2FFvadB>HWMf@Xetv#_e0q3wa&K&BVp~svfq{R1e0g_tacya6'
+ 'Vp&gsfPjF0e0g_tac*j3V_Z>6dShc_T~cCUVOdIZb7WmoWMf-Ta&mHVZfj;=R%2shV_HyQVPRldOmJ{-Z)|5_T1|3ta&m5JWnEKp'
+ 'hlg!xV_Z{jY-nO(T2XgxX=GntUsg$BVPRTMVPIfLS4w?-g@t>0cXV=YYi40!T2FjzXk%SnT~tSHZEa~}UtV5bRY-1bZf<I3U|CLW'
+ 'ZEbC8WnWfFZ*OmJYi3|sPH}N@acya2T~u#xZ*Oa7VOdRba&mHSYiD6uZ*OmGXJT7WWMqn5QHF+uf`EX4fPH&;cXV=YX=7qrQAdA&'
+ 'e}8{{dwF(qaBXN}Vp~y6a652tZft2|T~%XZVq#oUVq#)qTvC31etv#?d3SViZfa#<U|?WiSx#<lZf<UBWnWiIU|(NISWIGKVq#iN'
+ 'hJ|ixXJT7YU|COhb#i-qYi43xQfp^oTv9}fi;Iehh=+!SgMxv7eSCa+cy@GgZfa&=SxbF=eS3L!baHQOXk=YgdU|?zc64%YY-nU%'
+ 'R7QJydwF+ta&T>FWno%RlaZ5=kdKazjf{(miHL`Wg@b~De|~&=d3SVjaBXR2U|LaOT2XF#dTM52TT)+GPF+<;XnlQBL{vvkVOdUK'
+ 'S^';
const FRQ =
'=ouP9Sq7Ry%0uTu!CD$a_6I>j1rumPeV2zrSmCZiqNKt@gNfEde~JD>uvQ5~=5`ZA&bA#y{H#$#4mMmwLtu$ShB}l)Ne{?GIOkeI'
+ 's}I{k_ID&h*OFU9;5CUuF1VyaF&V%^0z~&i8;S`;(r_0<Z&@Wo+fg$_rDQ=wNPAC2NI!Z+#7=}no{Eb^M#qvw4xP<J^@^H8S>_r;'
+ 'F&Jq>%37O4+xo#n2_or3`vM3=WF#*{j=n-fYI#;f1wd*<-35L`$L)wj9OIHiE8Uz#nEkLs!x6hgA3DWEony~Lq(kmRcP-mO3MgDd'
+ '#0jNC5E0x%#F7a_*cBy2&YD0(OC5Mbe)ow)E$o*>Z0xf{8T`RS!3xksovZppp=cXJ+cKL&jWFp$D}Eb9<#a+sKVxY`vRH^j+>Eb8'
+ 'kyyh-8za|51_bLwtr}cIu+!T^I3FcMB|Th3>eGot2$H2lCV#<1D~|a@B(Ix8AdwqHBd2LZYV4atA40=KYv$@i!$b*1C<oj_LPT6d'
+ '0D7fFLK@vflB)_u^1>TLPZOL(9oFhZlqVfUj9x`Wy?R_kS83fuV525QT6<qbc5R$Q2^$?n7l&;{';
const ESC =
'b0$hMUo%@UL^NY7c`(-|3MyDAwJE_c5-flx11N_o4KAN2J}N>b0yKOxnk}y}=_(T}D=0N8qAV>cGAWlSnI-`$J12fEB`M4)NHk?O'
+ 'L@>235iLC`b1SGRKq=EGMkoj=j4ClKVJIRcIV4ji@F?>uku+vARx$4_CoPdMaV!!mx-GCR{wK#TIV!y>k~cs#jx}yAwkl67UMwUn'
+ 'G%PzP-z_yZ%r;Xnn=p+p%PCVVpe<-NAU7Z_l`Xg}`YPHj>N#FCD=^qAE+||&IWHnG-7ZWqt~rY{p)i3iS~yHFX(|';
const ENV =
'7AniS6)DQO5+=v73MQ^k6_^kj`}hDSyT0PWVz)|5Q&+;USXs24WL~<XV4tY4MzcIYMYcFULcB3LKFK38MdAo2==1=r=;9j6<K_~('
+ '<KP;>-`pO*)zT@r!NNGFyVfG1z1bh4!QC09%;pcU-|_&#?CJ;7=Hv{@<>U^`-`y3)*V7@v#lbbTsist)<2={M<~!NT>p0%g^)%<*'
+ '{xk0H`6%!8@D=9p?H1wb=^5bP-XPl0&@a!uyhFa8FiE?sElIttC`ZAs9zx2s4n5Vu05j3l04H3)4K6{TB0NW$L0DmtVrFY`l#+UF'
+ '+cP6d-Z3FY<1HRO>LnE}@fsE$<q$R$?+hXa?G7UV>KQ5o-zGT`$2vnPt58u?xY<j!y4*>&z~V)<%IrU~+WRz7-2N~#+4(X#)$%++'
+ '%;!QzywXckqOx9-YMOMN-9Htf;5-tY<u?v)@Gb{U{w@P8`8Nm^^R5XP_u&p1@!Az5<I*HC#l1B{QFu*QU)b3;VA<R>WZmH~isbAl'
+ 'gzEVmMCbk$Gwc2mDeC$YChGJVCgkcOG}YTKUbn?OC@DNjCMG&cAR#qO78os12Mi&n023L-01*?$0}v0z2@epm5*HP+86zjUDnFaT'
+ 'o<%;{okBd^ojf_>lP)#wTpTg_Rwgz6NgXowJ{mjoK_58gNh(6zPeM)2Sg4e^OfX<3N-ba{MJZw*JRoTnDiEm)A_372IRM)hs4T@l'
+ 'T*09&QLL*gTd1QnZB3y;Gu0~HGT0~IFWn&HDd!dGB<=+7G3){AKJNhUVB7=lx5W|V$73MY#Z5u1tE`<|*B8RG*%iUH+z`IF;|Q(6'
+ '<^V3q;s6-C<N+eO=LadH;|e!W+!RVf%Opl%rb9-xuC+fVueUxUv%NbVyvQ~Z$J#Uo%HKH#$mBu*!s=%M!sxgG)}+`4-%meVRjocV'
+ 'w&2pyw&T>(xair}yYb@Qz5edyzWwXtu=MNeL+$16HPzYfL1wkp!ppl*u+he~SfrILTc(*UWu~DmsGqGc$1=L8-T*J*!3Q7A)BtZT'
+ '${b!Twk@qdl2BiXWTc&zD<>a3DJ2{@Bp??t92gBH4G9Ae2LK5e0SyWe3J?qm7$OG{DJ~ZqH#9yyP*tU=HQiXlGv8OmFXdOtDehI#'
+ 'Bm7z1CH_F+C-^(*GVVC*MB6*-ue?s#v|dlSUvpcg6E5iB5H0HA3o7pA0w3(-B9s2_>EOL9zC6OXz^}!?w_CuFyidZ#xJ<&rv0T5c'
+ 'BP~R?A1Oe%7a=*k3KA>A00bMj2MZpz1qdO-1_>j=4Gtx@A0IHLIy_FNR9JYR&@m@C(=8-6*(Dz|;ujP!?F9-!>=OXX<sSyC<{ApK'
+ ';~y53)-XXpL87T`SEFXIu^-UawH(mez822k$ql^f&IT;<y9F)qT>w|@y$7i9!5F3QvNuBBO-Y!`p01jzro6N&q`kN%oV~vuTDQv('
+ 'L_OF5PASs`WGm4Iuq4n8y&%UNzAdUYqB>qllUHcC8nrjd7rHgf62vjh3DPUd3hyLG7XBU{3FRdp2hTzr8A;VALz<{qKW58L%~3*9'
+ '%s@9L&OS3E)I2I5-8dQ;={f@z`7r<#{YVQP_d5_K>_i?i$x}d2G^W0^N1B?Wm|D3SkCV6;WVN;sR?@HrN%C9;NB*=FLH@rTHT|g}'
+ 'Irg3`Q_*9xv^1VAVn?w$$kZ$8%GoLG%-|;R&+Z`j)&4B?==)6J<M}h$;`1)Tu+c9<v|h~1%+F(!pfbO<GBUlbFD}2YDJI0UAQ{cQ'
+ '76sYT7y#hnAOqy(BoO54QY+!(r!&OKwym_nTUb9my|_3VzPmLSzrisOz04~Gq}L<>O~EGzpsy$dX-g;wPDL&hbxuYpZAem5WlE#4'
+ 'INiqXH{i+fGv?0rE%Mg>CHmU>6Zq8fBRB@<BEl5UEy5$qN2)%dUXqbrY@2MJE2}yrD6KXjB(N<U9Jn15AHoL%J-7e@rqBis&(#wc'
+ '$InbLOIJHGxV%|PY?xS8(;Fn=)EFS-*Af}$*a!^i$N&W4wgL^r%n=XZ*$x@+*B~y|M6XD$wK_e-W}CA?U)CPDUD_MDTi_MCSL_PC'
+ 'PWJ$=pz;EpO63bdGu#zBE5kT1s+?hWO}$1^WlXba$vEWU%{1ub(=YAm;3@U-?;rZ^@fP*v^b7Ff@Br_=-Usfzzarj8musO|rNlY8'
+ 'UBoObT*fIWT+1UPTGJUAPTLI%JJSvTHro;dI@lTtGPWZWNMK=2x5P?TO-^D^x;4_dy)x9e!!6mf%Om1u&=l`8(+&MH&JO-S)gA6I'
+ 'v>x9jYbo6^vOmW}vSEjo(?v1P)kQDS*+nba<3lCo=T;g1`wGDCF3jXCzCKseUePn#p<}DPQLC7+q+nc>y}HoBzPr`Myu9AbTd(Tb'
+ '6)W)H02*0KARsTH7adbOA22sOD>y|=KuJqcXJ=t$CYH#vA*0N>7_rsG3(Dl$2Ic4N0N~~C4c}n#9mJyQJ4;5%Sd*fmRAgSFqHkqm'
+ '(%f3u*5O#&+3H^5;`X59+4QW{=l4Xq*!~wd+4T-J&d@k7xUWx6WSUP=b(U?RD>F|+CoWAvASFyb78gf25DIQH8xTW1CIHns69UpF'
+ 'A{WweM=ZjvPB5-ypG>K_(!;Ak*T<|s-p{T&>D;U~@Z~%=`t2w<{_i@x`s$F);l<+JWO1ot!MC=mS*DniH7?HQGAq>UE+^peCMWdx'
+ 'DLwx76+`*+Fe>)(MmGH7OVRP&x2xQ1R8_gMPHn+ppWw|^rsC3HtmWIJ!{h7AvDWs~Gu-&oE6V=ZE5z{OJf-Z^u|vDqMzunnptZzO'
+ 'I5SKoG%!sdFDg+QDIk>%GZd)?D;PfjG7m`%ISEY+vLRwVthhWtUsIWEd8~<|FG4}!E<ZixEjcypIWH;q<T)Jv_dEss=(h&^<E|3*'
+ '!98lj&>zgHSEgZ*u~t~K<k_t{>D;k6@Z-5O`t7(Z`tPF}{_t80=jka0@6R+3*vc;|&(Kght(R6!R%ooN2{|7%1~(cu05uXi1Stc#'
+ 'Dit^0O2XgPzhuGL#^Jo!oYr1ITeY^drK_Q<XpvuB9Lm|#7th?*5ZB<{1?A=E74YBX8}0kuI_Lh($f@$Ip1jB7QN7c_K*Pqbs*<W)'
+ ';yAvr<}<^y?k&o?`6AiL{vznt_$1@Z@)y&@uo1P<PAcxjxGT@Dw@+B2skCaZE!QQwE88KyC*vB%Ch-x~E&CMWF8(OoBmF4PDC<+U'
+ 'rqey)X}v+tOT$-gT(eoNzF_M@!d~w~$z=9K(xd)L*;n{a*;@5m(ys1dxw7C-DxBO|NM^v9Kx4*NPmNNYCfrh6A>mSP8R$=_4Dn9J'
+ '1@kD!0OTFG7wRED8@&ZP4%!vXDZ(E}H@!igYnP#wO(PpHPahXBO%@O_Oa}x#DF6(kA`2eFGds)E__ePH%_1i!+c!5Myhceva9Cq$'
+ '5FF2`4H(d)2N2R)00quE0|%ZP5(T~&Gzgy^G9M2lCod=#op3KPIYU!Wnp;s&)yC;G*vaiL-OuwT<JkTe;oS8B<=N)|<FVKmM&!~j'
+ 'z0=hutkBIdsHdt<XQ7~)S-49jR=GwYQouhON4PQ<JL)%6G5%04IrFhOM#)M=V7Ra`Uz}uKZiAVRjB;&?J6M-PF;QYWAW2It06!!T'
+ '0<sbi52hU=4mA}Z4MiCf7OWN@ET<_jKCd!8p`%l3KGM3mI@-U#FX+d|Gyc86F7*i*9OD!jANmXuHsC5KI?z5pO~XS`U1OqIVWnD%'
+ 'Pp7xOOtinkM#|2{IOyf46Z!ff5dQfp5cBLQHt6a>%*y34sjatBvT>zTQk0UK$G^Tw&cnn<+se{C@YL%u^Vjn?{@CHI{z2})`pwHn'
+ ';=d_AwV_i~vtmkiN@cEHxh<wJyeO_I#vivD&KA=IV;a@~L?KTEw;;h2I}fuUrygUfV^XF`t3X}9c8*+dw6;Axxwkbn#J46W+EWV^'
+ '*d_oQw<rQ8&@KWl#xxQz%UmQa%1%5yLzS($S7?-E%c@JS(z8pq-oZx1^5IF>_wqH-_Sz)a_Rt*D{+TY@@Ic(W#Fffgt(U-dqH9*5'
+ 'PC=qSMmnrHJ1Vy=BN5dXD*^NZLJjo*HxcX#CmY=aBOTccMLfecYgDFapOa{!J}x8BJSZI0IUEn#WDo$+$_)n_&mapI%q|`s&n7@2'
+ '(l%)~xlcz?o04u~VP#~Lwp%+quTU{KRyrOuA|L=eAs-AuCLaz@AR!by8YD0|NGM{oLrh7eO>d>Jq@$T)Ezz{QC)csR7~`|X0P-u('
+ '4EhnjE&3+FEASLKlh_n4Hp4DKRIpW|O|V!?lvbpePEN>DN=(m8LP*>~J4f*|NKO7UNmBeeMMnKKx;gVYu0GySsZ+vmU2Lw7n{t+G'
+ 'Hrl1sGTp7)D&f88F5JxewBM!vI@ZMaKjEqVKj}L2X}UYrs?v=`U8-`UmTZw-HB?DUE>J{AB1k(x4mc?_0Wcge04ppt3M0UEN<mt@'
+ 'K&Bx*HcLHGNK#f^Z<k+su23gIvPm5}xH$?eP$B>tN-h->qbdy&UN9LFTt6Zhj7l;pa9l=6W|UrHgmjc|vs=!^yjRuB$x7ST$vWcZ'
+ 'SPcH>91QQVI~?$`A{OGkHYU=uNISr)R#l^9i<f0<OQuCYL7_o9F<3k=9W_fJ3Lw5303FL96e*rAEIPe7NtbL(TbEH*b8DNIjFV+)'
+ '$uh&Q%PY;f${gX!O9%ew9}W5aED8Ji3<Lh|5*_jFEHmQZO;x$RT3oEAqn?hHH8LkJEGr=<AsZVQ5Evj004o*`4J!s33??Bg8YUDO'
+ 'DKI8CH()+vUX^1}b82ajM>Q?8R4yd7yCW94-xLYN<PHnj*$x2M;syrA<ry3^$Wul<vr$2*o{FPgag2wM-Kxe%<hRQ^?9b6B;_uZ4'
+ '*WWDy-p42e$jLnby;4mK!8TqiuAWXtXH{uhY;A*}Rm;IXSJcBeRprGhCi<TrCHGe$HvTj*TKFce)#fMK)JC<$s=u*Hn4O-Llyqoi'
+ '&qK;f);-Td;W64V@geRh_apE;{w3@^@-q56;XL<9qFUoro13+bVUDq8UXNOm>C;BI@7hJb_~bjy{pVEM{@l^s{n*Rf^w84R=(^X^'
+ '$zsU5wx6({WNDq1l96U=Tt*+KT0j@6S~v@<MKS=ds3ZZu&mjf4-5w27>nkiPys5!DUtmgLppv3xb8u#kMn=D}KRV61G$G}PlMnt$'
+ ';}+ea)*R$GP9@%1x+335yDQgJvO2?NYEiaYl4qG}oJCbvpFL7gsVGrFR1!23(JCSU(J`(M$2p%A%Rsj&vZb>*oSKkSjG2~)Yin$f'
+ '(Ym@z-@Cdz>p4C-(J(0J)J_KesXhh$w?_u_(^eng(@d?u$5YNjt6*eiaFA=6NlUt;Lqx--MnBWCOe*WSE)eUXCjj5QEECjNE+xV?'
+ 'LS>RjVNhVHZI6ptb&QjjIm5m}E6v9@6X47x0Omz52gNqn3c@zV3%nu1CA=iVt*lPMu%ou5nQxMhY;cx^DpN?y9ZNpg4lh3A6}THr'
+ '3*iV*3iJS35AF**Ftibxs7xzVXpdx9opO$oXpd!b$FE$V(z{ln<giez>JH1S{uK1e{Vx8i`WV_#=`-Au%U{z(ty{5FdW~b3Xp(1&'
+ 's+quZtC`GXU|!%+o>cidtC0ReI>P-xEz9~lA;;!nH><g%S(T!ilWS~vaExttIzl)zFEJ}0IUy_u6A(@b01`6_3`8UY3O+C*9Y-}V'
+ 'HmaXUUSY0ClVEC`Y?_j4(6C6i-@!1w>E0rc?f%p;$Mn1%*z~n3-sr<X#nIENqO7N$yOl~vtEi}EXKjd-Ag@3z54A-q2r@S>Cji|h'
+ '4+{7`5)SKD3=HTt92C1TC@{fEJ(8(fWs{d?cy4!%vNgrd$1Ta><s(A+;}I49=@>Eg-V-zM(-EEC!ydZMwLYV=m0g;kiE4L-hLL-Y'
+ '*rdXr=(5eQ_s8nR++zN3;%4nr->K}O-MH<);#KCx%}U+3qh_d-n2~0PXK#3L9keLD61^S52E-B51lTX-4Al?bB;F6>0F@@wA80gG'
+ 'KS!gvRhC(ylaZZob9QlgsaN1el~M0FC!YK#A3paOBtQNV5wiRe57+7)8PmdQOOU3YZ*Y%zl53T0g@kU4I!r=5DL^kUFfA1=!oDRA'
+ 'BG^m-9N{(yNAW5go!dgaW35cDoRwailxvlbY>IDi)vZ*{>&7SE@K_J%;ZPFY@?`+m{yPxJ`ZX`b;5fQWnY(9JvXe)zkC$bWY=(7;'
+ 'Qm!LNvZ@9?%()24_PH0$^SA)w?@too)KeDK#i1e5x>`k|Xq<^}adB*tijIwNO|Z&C*23jA*v;i8D8?}u6Kho!0GC)Y55*@hAhsO5'
+ 'Iypj6l9y_8Z-{S^i+68{+(h%c;YI$z>Px`h-4)g|`W@yU_ZP{v+9n|F8P3Gsqo=!=U8JX}bZT;LagU3Nq)1)0&@V=(<Qvht*at%C'
+ '+y%C>-2l>4JP*(xOD;z-pOjNPUz%H0keHK>Z)9y|!Z|J`#t|AJqzEY)0s*TKG5{qEAPXJ=SSSh-PevppomOK@P(`PvbDUmacVU~E'
+ 'zpzNIpU4izLeVut_&(CL{<OQ!>cT8n-^M9D#5CW>o}*+~Sc;`zm3WepZ;p0zwXL*3II*`MG&DZ|6E{N$2}jfo3p{lu5k|W^K9GG$'
+ 'r>Rk_Xq;VQbB=OucW-!&VVJkCI*Z`N99jL#AfEotBV_%`Ih^vwP_WRsqoP+^Qd>=6Wo>bfe0gtrgouuHQli9RKjiYdJpRSX7V+Mc'
+ 'yXLV(JM7J-8^XEPNJ%9=hP0b*Vx(DGkCJk0kB^UdKei&2Bh44P6vGe66Voxw1K$m{0MilE2pljyPdFGMw82|fnwF55bA)bgk9LlY'
+ 'r((;QF)ru35is`09yk8NDL44RH$CRb%11@Vy;D<QT3V%)V{Vggjfjnlba-@jsZ=|xMqDG*o3Ik#&aNENM>!h3(m((sy~k`=sZ_eH'
+ 'oS~kZjdO2^aE5h#ig|{ETr*fS(FF3Z%mMx4=miSs)d1eu%M8Wk+!xZ`O+8LcWMy-wZf0|im1>S|Z;y70r@%Dg5a_B&6GYW80iLz3'
+ '0J`$vG2qb3979q#Cc89AkE2&(k&9Ylk&l&=adU2OQNF6$vf|Y4Li6MN&-niH_Vew!=jEoj#lbEnPfwRys;O>~lb)82ZEc2(jg5MC';
const LOW =
'Qy`WjMIdb>5g;Wa0w9PY?jIl`a~-l8LLCYjA{Z+adl2CcLlKt{vI*r2-53@ZpCeNnfg>v$xFhHqJtJxwxFRDOV<6QRP#z%|a}=Bn'
+ ')DJNY%M7RsT@3gN{0=z}Fd*X~A|S*dydbS01R+i!ry(dHW+5FK93x5|9v*WW;1#bCmlg34ybP)icMlB^=pR8I=^rE>^B?yez#pF-'
+ 'ogs)Fr6P75^&k`+EE?4mbP@9px)1;j1P`nYHV*0wyd&5kuOq!6W+Rj!<{~m6lp-D=h97ww(j4{}^%tQMB@uKE2@L29zYGuzvJ7(x'
+ 'D<MfBEFmu-Kp_ku(;#pk3n3yOgCIB>ydCWqq!%m^w-Id(OAR9pdJhW?p9;+kq#h0+wI1#tvL2)#G9CsW-yNJDH65K8>>MT;1Q>P`'
+ '4im==4-mHuAPNi#AP5EsMjq-Ma~{qdT^^+z9Ue6tq#YX^a2)&<ha5f_3m7#L2@sMFv=3(urwP*u^9U3OfFR=}Y#_-bOCXjcV<1K('
+ 'lpq8oPan-3VH^?~T^iRG1`{t1$Pdm6LkU#~9Ro`R)FFc*zaeZPdLcI<TOrUOTOpqyKOk%!L?O-{Ng7BPY8LGgRTZrfFAf?Eb^z)D'
+ '%_cx1u_h`abSCa0`6b#RxFua7+9H`Ahau!0P8zcotQ2bz7!fNC_za*700000';
const DON =
'00000003+On*mt?Z2^S=Z2?&UuK`&BZULGBngJC6007znnE`JBZUKY=SpixBnE`JBR{^g9n*kL7003_Rt^sZVZ~=q?Z2=bm6##?*'
+ 'ngLe<gaMlYSOEY4765JmuK|MrngN>uZUGbk0RWl-t^t|>uL0TthXMcq00031*a4dXg8_vBnE{yrumP_D?g1+R+X1fuE&*5p0RR91'
+ '6#xMMuK}3>uK{iWZvk!rt^uzB#Q_%pgaMcV*#Im77626h0RWi+g#m5>g8^;<gaH5m762;%E&*-<g#m*BZUJur7626h0svP5SpkCq'
+ 'n*nVBZvhqn7626hLji>Wt^q&*Z~?;s90C>q0RR&KngN9YLIH#Ug#khVZvk!rSpjYVuL1D^000#LuK|MrgaMlYZ2?&UgaNMsuK^VR'
+ '#sRMZSOISV7626h006`RnE{&tnE^xrf&qj9g#p9?EC7T7gaNDoEddt*000yKZULJCZ2=Yl0RXN6umOVs0RR>NtpS<=@Bu3TumP9>'
+ 'n*kI67XTChSpjeXgaMcVngN;t6#xJL0sz7RSpkIs761SM6##7kgaKIr0RR>N003<Pg#nlWZvmMBgaH-+0RR91m;r?Wg8{GsnE`PE'
+ '7626h+5uSsnE`MCZUJopS^-!AuK_FoSph-;Z~+tmZvk%sSOEb50RU_PZ~<5W00000g8_sAMFA8500000n*oFYg8>x)761SMLji69'
+ 'S^-%BZ2@lqLji;VSph5nE&*5pZvhnmZUJrqE&&w)003D5SphBq6#y0hSpiuAL;)2500031f&p0pS^*RQ761SML;+a=g8^9qSpjbW'
+ '00000002V)SOJ6qZvk!rS^*UR000#LZvlh>g8^9qSpiuA000*N003<Pg8^>=g#m2=S^)q66aWAKZvj~Wg#m*BZUKY=6#y0h0RTe*'
+ 'gaL#Bg8_vB?g1760RV3SZUJopS^<Ost^oi5766(7t^u0?006`RuK`5@D*yoi007wmgaL2?ZUJopZvmMBn*rJZ6#$t5Spn?;7y%Uk'
+ '7XSbN006E5gaK{=Z~<)rZ~=k=gaMlY6aa4lZvlb;7XbkP6aWDLtN_9QEdg%<ZUICASOFIR00000Z2@ipE&%`l761SML;-C9ngIa-'
+ '000yK006H6ZvmMBZvk!rSpjYVSph=<000&M0ssI2SOHi8Lje~66aWGM003+OSpkFrLji9AL;)5600000SpjVUZUF!Q7XSbNm;ssr'
+ '!~qrn003?QL;*tqZUI99L;*tqLjhR<Lji>WSOFFQZUI>VF9AaVSOG%;Ljf-VZ2?*VL;+U;D*ylhF99t9E&*5pE&*8qZUHU<g#m2='
+ 'E&)RU003D5F98?<SOG2pS^;eVS^-%BZULDAngIX+6aa()ZvjLBEdfLULjf%TE&)RUZUI^WnE@66001rlLjgnq7XdB-L;*tqEdg%<'
+ 'g8^9qF984m6aYg3L;)=UF99t97y(%UE&(k8SpiuAg#iEn0RU_PSphBqEdd$<ECECTEdg2qSOHoAg8^9qS^+`<EdfOVE&)RULIE!U'
+ 'E&(k8LID{8E&(k8Ljf8ALjf%TLjhR<SOG-=Spg~l0RUM6ECE9SE&*Br7y&H-Ljf28SpjeXLID*30RUM6Edf~pL;+a=Z2@lqZvkxq'
+ 'L;(';
const HFG =
'B?OxU69lXTk^^f5p9Gx*umqq4o&|ab6$EGnYyrXoiU8CABmgu3MF2to2>=QJw*Z|0rvQ}zjR1rIUI1ADngE;t?EoABV*peDFaRz9'
+ 'GXO0BC;%w{G5|FI1poyAP5>$ZLjWiMIRGaBF90e4Q2<K-X8>^kFaRt7EC40|CIBS>9snQ!DF7+}0ssO4K>#iQJ^(NPGyp09A^;o!'
+ 'CIBD+BLFD?A^;=+8~`2w9{?c$9RMQ$CjcV=0ssO4Apji!Bmf=&DF7q@BmgA<A^;))DF7k>Apj--DF7k>AOIr(B>*1)DF7`10ssO4'
+ 'HvlsLGypOHIRH2SGypjOJ^(ZTF#s_DHUKjKH2^dKGXOaNE&wtBJpeuc1ONj7Ne%Q53k`!0Yz$!y_zTDl*9!6snhGBcK?yGlx&{dd'
+ '-2y-amjDa_J^()e7yuOj9R>&n76upw4FzBYbOq4_*aM0L+yXoT3<5y{_yFJlRsc-^GXN|ALI67e1ON;G`2ghr%mB*(tN^nBrvQZj'
+ 'h5)btYye#VQ~*x^F90n7DF844CIBk{EdVY61pov9IsifdH2^{YFaSINB>*b`F90?GBLFA>DgY_~A^;))A^;=+BmgA<DgY$_0{{X5'
+ 'AOIl%A^;=+C;%z|Cjcw}C;%q_Cjck_CIBP=B>*V^DF7@0A^;=+G5|3E0ssR5E&wn9F90n7G5{|CGXOOJDgY}0FaR+CEC4J3F90zB'
+ 'EC4b9DgZG6H~=*O0{{a6e+|V7Sq)SRR1BaBunFf19u3J0%n1$*R|???$OxYZ8U)b-MgjW(ZvfT+D+7Z7@df|@;RO-^j06AxSOnPs'
+ 'J_98JN&<5MSOF{n{Q$-Qy#TrZz5u%bUI4uSAObi5Wd;BMCj<ciZ~_1T@&E(?&j0`bivVi?cmQw!g#dQ|eE@*~lmK@CT>ysw+W}Aj'
+ 'W(5EMi30!tH39$tn*ag;d;kCdU;q{XQ~*~1TL4i2P5@K@WB^$JC;(ajXaOw%F9iSqX9EBL@c{q;ivR!sVE_UELjV8(H~=vKHvl^T'
+ 'JpeiYO#nau763v3K>;iPmIMF*0RsR4xB&nFegFXgTL1t6LjVi_I{-8QJpecWGypyTM*uef6aYW~904H!^bTVV3lISiZx9<0$PT~^'
+ '`3<xVrwdREiVU3!NC+bceg>rkpaQu84*-t<Dg$}}dJHTH?g`fikO#B|Bm<}ep#ua2)&gGxdjbRlX8~ma-T=)2zX06;y8xvC00000'
+ 'eE<Le$p8ib7XbhO2mxjQ0s-s+1_4U}!T|dKp#Z}Gk^q_jkN}$iqyUou000000RR91000003;+QD*8l(joB)~tr~rxpn*fRci~y7X'
+ 'q5zZtnE;IdeE^0400000000000RR9100000ApigXhX5P^fdGjBh5&{DdjNa@eE@d=j{t-Kh5&s4000000000000000000000ssI2'
+ 'a{vkeTmV=AU;tzQWdK|NX8>aWVE|tMYye{b0{{X5suEljMiUbiiV+eN!4cmRoDU!n(+*1yBMhMpS_k_GQUxjn{{Y<pPyko}RsakD'
+ '`UoBgzzKf|BnqDiISPCU`U$BBT?xDhItP;mZURdJ{s064wg9*QCje9c;{XT%(gO?ws{_dcp99|mUjr@#WCCLY%K}{k3Idk_pa7)+'
+ 'r2v!w!T_@XF91*gYXAWNjsXAwFaZhxzyJUM3IWCdtN^(H#Q>rJtpKY4ssO71tpJt)<N&k)Hvm8YSpWn8DFFZgLIDT>CIJ8d&H$MJ'
+ '%K+T~(*V){#Q@;|%>eHJ!2r_$`vBtrJ^)StQUC`4(EtDd!~h2X+yDRo7XhRI;{f#l=>X~g^Z@z*^8f$=`2gYo9RVBxP5@s3Q2+=4'
+ 'aTHt-m=*OD7!rLHQ4(+z{uFE!855urG!ax05e+>J4+H80nF6%}FaQ7mE&w3{o&^F3;|9qG$OmW#Q3xXjzy@;#B?edp2nCb`vH@oS'
+ 'umG$8zyPuUQ~*~11ON#D(*S`0+5sB@^#bq#hXZK>1_SB=(*j!oa{(m*zW}QMssN<`sQ{e-NB~3t1^@&A003?P1OTuA006-NJ^=Ut'
+ '8Ufb;v;dC)umGz7n*gK$hya}cjR2(pHUK&R0{{a6000000000000000A^@iV*8r;kxB#*MvjDaLumGw6x&W>KrvS+SLI6Vm0{{d7'
+ '000000000000093M*!yl<^awB*#OP}$N<m)!vNd>-2l`8#sJa)OaM&)0{{a6iW5T<!xEGd+7O=+0}i4Pw+<T-9}hVXUJ4=$Q3v7%'
+ '_W@u6V**P8003|RwgCVDx)8V#v=O@zZxR#|X%Sx#mJT2d&j@-5NeHF}vI8OmfdSwFR016WBmiar0s#X6Aqi;+c?p;YWC<7t&<M%~'
+ 'Xag_=_XC3iI0J|R`vCz0fdRJxLjwN+F91;hbN~SWoB}ccPXfmPKLYyzmI7M=+5z<e1OnXw!2$CD@&O0}fdQHUECM+KM*vFzG5`Sp'
+ 'zX7}eF#>=Ak^&C`Py$H;9Rei+IRXX(S^{YT(E(KgLjoECJOWJuO#op4D*ylhZ~?>s5duyD_W~gToB|I6mjZACmjdeo-~zV-+ybEj'
+ 'sRHr>(*ouKVE}9ZH~;|v9v4C!@)dL#Wfyf6mJ$yY)e<2Xh!K<sdJR4WHwae?WC7Fy`vI8&00000(*mOeP!pFFOca6?j}#>oTom>Z'
+ 'iWF)Qc@YZ_tOwQ$q6PZ{=>hfu`T@ZKN&qJSr~qC8xDZ|u#1NAaw-AI8#SpO#xehxF`3W`$g#si45CQ@MiUEQF00IR9WB>vH003kG'
+ '`v-&xJPDx+4-5Yba|$&JaRg-pvIE%yxB=(^ngP=ReF2&Q_yGk1LjV;30RZFxO#<r!4g%)_V*<PaC;~A8$^rEOAp!~l?Ex7A{Q=Vf'
+ '9Re2uGy*sRQvfCa0|3JS9s<k)AOfud9s*(l(E%F*5&}^IcmhTOj{=ASn*wwKU;-in`2zm}Y5*eu007PaP$h;Vpdx7@kR99}p%sA_'
+ '$`ZvEFcHrYSPZib{RQC%0s@l&y8{sf00000Obh`F#~5fBR2c{vbs4i6E*Oj#R}||K5)(`m(GT_v<_G@<E&`<jJ_DEo001%o^aC9N'
+ ')eS!qO%QJp3lgRgun;Z~e+`BZ!3@_AEd@>n$^zd4mjZMGLjzF*YXD*Z000C4p%5Mv9}q_qV-LC!a1O~3G7W1Dk_a3O@B-EatO8B~'
+ 'x&pcaRs$siO8{B`K>z>%n+3H5Yz0>YjRYbE%>+jVY6BAkW&<+=m;*usm;*!uI0J42y93+<Y5;Qp00000Hv{|twFB(|RRnngQv-Jc'
+ 'nghiH%>yn3Rs=5u00b5U!~;MCz69(9fdG8~00000`xI9U4HcCS1s3)dU<TwDLKEQ;oDH)R4-J$I001fgn+A&rzXmh~cmS6GmjM6('
+ '8W<EBUKt1*lo~4=A{r(enG>58`V7Sm6AVNRHwH`wTn7jU9|$i9UI2Cg!~(VfH4r8g2od}f#t}3VDiJ;qh6}t4j|k!kA_(;c`UegO'
+ 'pat>;7zmRG6#ydubpc}m9s^zohy>^d9SBVb^av~nVFv~X;RnVBX9fobq6VA>+y;;aatJO6Kmcz5hX4QoVhFqmtO#2PR0$agHwj}1'
+ 'RR>cDng^5zq6oAH1_*Bm^at7pi3rIES^#zce*gdgV+ySaZV5aILI{KixCkl<TnX+7q6m}-1PS#B&j=t1vIxTniVDXGOaOWSd;kCd'
+ 'PZwPn{1$H)7#EQhViNZfMh`I#Mif>Kwh$!_{Q(gH007AXbp$j7EC2ui3IT}(j~M6}q!{BE7#aK+KNCe0-U^xvr4a=X4+sqfrUJYI'
+ 'NdwRV`2*$ytN^J100000FA)Y3BoLSq#}2g;*$Dj*c?kjybp>w-R0Dqlfdd8u-vW&T1q3k!RRB`}00000$O)JU+z8SM-Uq)4_6c(a'
+ '?gsk@AO)WT4g&`Rk^^l6=mO3HNCXfBK>$Jk00000-waU;nF~4#<_KmA(FFzw00j>Oa|2lf1O&<hR0O~S+ykBiE(Hk%J^)4l00000'
+ '00iLydITQ>C<G1!Gz10&a0J-|p#;4IECo&lcm?_dl?34gU<L^WJODTV00000';
// --- base85 -> typed array ------------------------------------------------
const B85 = (() => {
const A = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
const t = new Uint8Array(256);
for (let i = 0; i < 85; i++) t[A.charCodeAt(i)] = i;
return (s, n) => {
const out = new Uint8Array(n);
let o = 0;
for (let i = 0; i < s.length && o < n; i += 5) {
const r = Math.min(5, s.length - i);
let v = 0;
for (let j = 0; j < 5; j++) v = v * 85 + (j < r ? t[s.charCodeAt(i + j)] : 84);
const b3 = v % 256; v = (v - b3) / 256;
const b2 = v % 256; v = (v - b2) / 256;
const b1 = v % 256; v = (v - b1) / 256;
const bs = [v % 256, b1, b2, b3];
for (let j = 0; j < r - 1 && o < n; j++) out[o++] = bs[j];
}
return out;
};
})();
const u8 = (s, n) => B85(s, n);
const u16a = (s, n) => new Uint16Array(B85(s, n * 2).buffer);
const f32a = (s, n) => new Float32Array(B85(s, n * 4).buffer);
// fp16 -> float, for the band-gain tables
const HALF = (h) => {
const e = (h >> 10) & 31, m = h & 1023, s = (h & 32768) ? -1 : 1;
if (e === 0) return s * m * 5.960464477539063e-8;
if (e === 31) return m ? NaN : s * Infinity;
return s * Math.pow(2, e - 15) * (1 + m / 1024);
};
// --- tables ---------------------------------------------------------------
const ZONE = [[27.0, 0.0, 19.0, 0.0], [33.0, 19.0, 19.0, 1.0], [39.0, 38.0, 20.0, 2.0], [45.0, 58.0, 14.0, 3.0], [51.0, 72.0, 11.0, 4.0], [57.0, 83.0, 8.0, 5.0], [63.0, 91.0, 7.0, 6.0], [69.0, 98.0, 5.0, 7.0], [75.0, 103.0, 5.0, 8.0], [81.0, 108.0, 4.0, 9.0], [87.0, 112.0, 3.0, 10.0]];
const NOTES = 641;
const NPART = 115;
const NZONE = 11;
const RELHI = 11;
const HFMIDG = [0.96, 0.82, 0.88, 0.95, 1.0, 1.0];
const HFSIDG = [0.8, 0.92, 0.86, 0.62, 1.0, 1.0];
const HFMIDA = [1.020621, 1.104315, 1.066004, 1.025978, 1.0, 1.0];
const HFSIDA = [1.118034, 1.042572, 1.078328, 1.270001, 1.0, 1.0];
const VELFC = [749.904, 999.846, 1999.69, 4499.54, 5999.23, 8999.08, 11998.5];
const KNOT = 12, BANDS = 6, HFOSC = 8, LOWOSC = 8;
const FREQ = f32a(FRQ, NPART);
const ESCRAW = u16a(ESC, NPART); // fp16, exact: it was fitted that way
const ESCALE = new Float32Array(NPART);
for (let i = 0; i < NPART; i++) ESCALE[i] = HALF(ESCRAW[i]);
const EBYTE = u8(ENV, NPART * KNOT * 4); // [partial][knot][4]
const LOWRAW = u16a(LOW, NZONE * KNOT * 2); // [zone][knot][2]
const HFRAW = u16a(HFG, NZONE * BANDS * KNOT * 2); // [zone][band][knot][2]
const HFTAB = new Float32Array(HFRAW.length);
for (let i = 0; i < HFRAW.length; i++) HFTAB[i] = HALF(HFRAW[i]);
const LOWTAB = new Float32Array(LOWRAW.length);
for (let i = 0; i < LOWRAW.length; i++) LOWTAB[i] = HALF(LOWRAW[i]);
// --- score ----------------------------------------------------------------
const SCORE = (() => {
const d = u16a(DON, NOTES);
const k = u8(KEY, NOTES), v = u8(VEL, NOTES), u = u8(DUR, NOTES);
const out = []; let t = 0;
for (let i = 0; i < NOTES; i++) {
t += d[i];
// ticks are the score's native unit; samples follow the shader's rounding
out.push({ s: Math.floor(t * 441 / 10), key: k[i], vel: v[i], dur: u[i] });
}
return out;
})();
// --- engine constants, verbatim from the shader ---------------------------
const SR = 44100;
const TAU = 6.28318530717958648;
const SUSTAIN = 1.2, PEDAL = 2.5, DAMPER = 1.6, RING = 5.0;
const QUIET = 0.0003, SUSTAIN_T0 = 0.25;
const GAIN = 0.245945, LOOKBACK = 16.0;
const ENV_ATTACK = 0.00299998, ENV_HOLD = 0.800145, ENV_DECAY = 100.022;
const SUS_LEVEL = 0.707945784384138;
const DUR_REF = 0.02, DUR_DIV = 16.0;
const WARP_TAU = 0.0015, WARP_MAX = 6.0;
const WARP_SCALE = 11.0 / Math.log2(1.0 + 6.0 / 0.0015);
const HF_MARGIN = 0.08;
const LOW_GAIN_M = 1.62, LOW_GAIN_S = 1.14;
const HLO = [6000, 8000, 10000, 12000, 14000, 17000];
const HHI = [8000, 10000, 12000, 14000, 17000, 21000];
const CTRL = 64; // steering-table update stride
const HF_FLOOR = 2.0e-5; // band audibility floor
const hash01 = (j, salt) => {
const x = (j + 1) * 0.6180339887 + salt * 0.7548776662;
return x - Math.floor(x);
};
const smoothstep = (a, b, x) => {
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
return t * t * (3 - 2 * t);
};
// The SF2 DAHDSR amplitude envelope.
function sfEnv(age, dur, rel) {
if (age < ENV_ATTACK) return age / ENV_ATTACK;
const d = age - ENV_ATTACK - ENV_HOLD;
let v = d > 0 ? SUS_LEVEL + (1 - SUS_LEVEL) * Math.exp(-6.9 * d / ENV_DECAY) : 1;
if (age > dur) v *= Math.exp(-6.9 * (age - dur) / rel);
return v < 1e-5 ? 0 : v;
}
// A bank of complex rotors: one multiply per oscillator per sample.
class Rotors {
constructor(freqs, phases) {
const n = freqs.length;
this.n = n;
this.re = new Float64Array(n);
this.im = new Float64Array(n);
this.dr = new Float64Array(n);
this.di = new Float64Array(n);
for (let i = 0; i < n; i++) {
const w = TAU * freqs[i] / SR;
this.dr[i] = Math.cos(w); this.di[i] = Math.sin(w);
this.re[i] = Math.cos(phases[i]); this.im[i] = Math.sin(phases[i]);
}
this.age = 0;
}
step() {
const re = this.re, im = this.im, dr = this.dr, di = this.di, n = this.n;
for (let i = 0; i < n; i++) {
const r = re[i] * dr[i] - im[i] * di[i];
im[i] = re[i] * di[i] + im[i] * dr[i];
re[i] = r;
}
// unit modulus in exact maths; nudge them back before rounding shows
if (++this.age >= 8192) {
this.age = 0;
for (let i = 0; i < n; i++) {
const m = 1.5 - 0.5 * (re[i] * re[i] + im[i] * im[i]);
re[i] *= m; im[i] *= m;
}
}
}
}
// Per-key shared carriers: these depend only on zone and playback rate, so
// every voice on a key reads the same bank.
const HFBANKS = new Map();
function hfBank(zi, key, rate) {
const id = zi * 128 + key;
let b = HFBANKS.get(id);
if (b) return b;
const fm = [], fs = [], pm = [], ps = [], fl = [], sl = [], pl = [], psl = [];
for (let bi = 0; bi < BANDS; bi++) {
const ilo = HLO[bi] + (HHI[bi] - HLO[bi]) * HF_MARGIN;
const ihi = HHI[bi] + (HLO[bi] - HHI[bi]) * HF_MARGIN;
for (let j = 0; j < HFOSC; j++) {
fm.push((ilo + (ihi - ilo) * hash01(j, 17 * zi + bi)) * rate);
fs.push((ilo + (ihi - ilo) * hash01(j, 29 * zi + bi + 7)) * rate);
pm.push(TAU * hash01(j, 41 * zi + bi));
ps.push(TAU * hash01(j, 53 * zi + bi + 3));
}
}
for (let j = 0; j < LOWOSC; j++) {
fl.push((20 + 60 * hash01(j, 61 * zi + 5)) * rate);
sl.push((20 + 60 * hash01(j, 71 * zi + 5)) * rate);
pl.push(TAU * hash01(j, 61 * zi + 19));
psl.push(TAU * hash01(j, 71 * zi + 19));
}
b = { fm, fs, pm, ps, fl, sl, pl, psl };
HFBANKS.set(id, b);
return b;
}
// One struck key.
class Voice {
constructor(key, velq, durq) {
let zi = Math.floor((key - 24) / 6);
zi = Math.max(0, Math.min(NZONE - 1, zi));
this.zi = zi;
this.rel = (zi >= RELHI ? 5.0 : 1.0) * DAMPER;
this.dur = DUR_REF * Math.pow(2, durq / DUR_DIV) + PEDAL;
this.rate = Math.pow(2, (key - ZONE[zi][0]) / 12);
const vraw = velq === 24 ? 96.0 : (velq === 28 ? 113.127 : velq * 4 + 2);
this.amp = Math.pow(vraw / 127, 1.1);
const fz = Math.max(0, Math.min(VELFC.length - 1, Math.floor(velq / 4) - 1));
this.fc = VELFC[fz];
this.off = ZONE[zi][1] | 0;
this.cnt = ZONE[zi][2] | 0;
this.tail = (zi >= RELHI ? 7.7 : 1.7) * DAMPER + 4.0 * RING;
const mf = [], mp = [];
this.pidx = [];
for (let j = 0; j < this.cnt; j++) {
const pi = this.off + j;
const f = FREQ[pi] * this.rate;
if (f > 18000) continue;
mf.push(f); mp.push(0); this.pidx.push(pi);
}
this.modal = new Rotors(mf, mp);
const bank = hfBank(zi, key, this.rate);
this.hfM = new Rotors(bank.fm, bank.pm);
this.hfS = new Rotors(bank.fs, bank.ps);
this.lowM = new Rotors(bank.fl, bank.pl);
this.lowS = new Rotors(bank.sl, bank.psl);
const np = this.pidx.length;
this.cL = new Float64Array(np); this.cLi = new Float64Array(np);
this.cR = new Float64Array(np); this.cRi = new Float64Array(np);
this.gM = new Float64Array(BANDS); this.gS = new Float64Array(BANDS);
this.filtM = new Float64Array(BANDS * HFOSC);
this.filtS = new Float64Array(BANDS * HFOSC);
for (let i = 0; i < BANDS * HFOSC; i++) {
const xm = bank.fm[i] / this.fc, xs = bank.fs[i] / this.fc;
this.filtM[i] = bank.fm[i] < 21000 ? 1 / Math.sqrt(1 + xm * xm) : 0;
this.filtS[i] = bank.fs[i] < 21000 ? 1 / Math.sqrt(1 + xs * xs) : 0;
}
this.glM = 0; this.glS = 0;
this.env = 0; this.ring = 1; this.hfOn = true;
this.n = 0; this.ctrl = 0;
this.dead = false;
}
// Everything that moves at control rate.
steer() {
const age = this.n / SR;
let src = age * this.rate;
if (SUSTAIN !== 1.0 && src > SUSTAIN_T0) src = SUSTAIN_T0 + (src - SUSTAIN_T0) / SUSTAIN;
let ring = 1;
const over = src - WARP_MAX;
if (over > 0) {
if (RING <= 0) { this.dead = true; return; }
src = WARP_MAX;
ring = Math.exp(-over / RING);
}
const env = sfEnv(age, this.dur, this.rel);
this.env = env; this.ring = ring;
// the attack ramp starts at exactly zero, so a voice only retires on
// age or on falling below the audibility floor after the attack
if (age > this.dur + this.tail || age > LOOKBACK) { this.dead = true; return; }
if (age > ENV_ATTACK && this.amp * env * ring < QUIET) { this.dead = true; return; }
const u = Math.min(11, Math.max(0, Math.log2(1 + src / WARP_TAU) * WARP_SCALE));
const k = Math.min(Math.floor(u), 10), ka = Math.min(1, Math.max(0, u - k));
const zi = this.zi;
// modal complex amplitudes: signed byte cubed, times per-partial scale
for (let j = 0; j < this.pidx.length; j++) {
const pi = this.pidx[j];
const b0 = (pi * KNOT + k) * 4, b1 = (pi * KNOT + k + 1) * 4;
const sc = ESCALE[pi];
let c0 = 0, c1 = 0, c2 = 0, c3 = 0;
let a, b;
a = (EBYTE[b0] - 127) / 127; a = a * a * a * sc;
b = (EBYTE[b1] - 127) / 127; b = b * b * b * sc; c0 = a + (b - a) * ka;
a = (EBYTE[b0 + 1] - 127) / 127; a = a * a * a * sc;
b = (EBYTE[b1 + 1] - 127) / 127; b = b * b * b * sc; c1 = a + (b - a) * ka;
a = (EBYTE[b0 + 2] - 127) / 127; a = a * a * a * sc;
b = (EBYTE[b1 + 2] - 127) / 127; b = b * b * b * sc; c2 = a + (b - a) * ka;
a = (EBYTE[b0 + 3] - 127) / 127; a = a * a * a * sc;
b = (EBYTE[b1 + 3] - 127) / 127; b = b * b * b * sc; c3 = a + (b - a) * ka;
const x = FREQ[pi] * this.rate / this.fc, den = 1 + x * x;
this.cL[j] = (c0 + x * c2) / den;
this.cLi[j] = (c2 - x * c0) / den;
this.cR[j] = (c1 + x * c3) / den;
this.cRi[j] = (c3 - x * c1) / den;
}
// band gains, with the measured attack calibration
const s = smoothstep(0.080, 0.300, src);
for (let bi = 0; bi < BANDS; bi++) {
const i0 = ((zi * BANDS + bi) * KNOT + k) * 2;
const i1 = ((zi * BANDS + bi) * KNOT + k + 1) * 2;
const gm = HFTAB[i0] + (HFTAB[i1] - HFTAB[i0]) * ka;
const gs = HFTAB[i0 + 1] + (HFTAB[i1 + 1] - HFTAB[i0 + 1]) * ka;
this.gM[bi] = gm * HFMIDG[bi] * (HFMIDA[bi] + (1 - HFMIDA[bi]) * s);
this.gS[bi] = gs * HFSIDG[bi] * (HFSIDA[bi] + (1 - HFSIDA[bi]) * s);
}
const l0 = (zi * KNOT + k) * 2, l1 = (zi * KNOT + k + 1) * 2;
this.glM = (LOWTAB[l0] + (LOWTAB[l1] - LOWTAB[l0]) * ka) * LOW_GAIN_M;
this.glS = (LOWTAB[l0 + 1] + (LOWTAB[l1 + 1] - LOWTAB[l0 + 1]) * ka) * LOW_GAIN_S;
// Hammer noise and air fall 5-100x from the strike into the tail. Once
// the whole band sits under the audibility floor it cannot be heard
// over the partials, so the 96 carriers stop being stepped.
let gsum = 0;
for (let bi = 0; bi < BANDS; bi++) gsum += Math.abs(this.gM[bi]) + Math.abs(this.gS[bi]);
this.hfOn = gsum * this.amp * env * ring > HF_FLOOR;
}
// One stereo sample, accumulated into out.
render(out) {
if (this.ctrl === 0) { this.steer(); if (this.dead) return; }
this.ctrl = (this.ctrl + 1) % CTRL;
let l = 0, r = 0;
const m = this.modal, mre = m.re, mim = m.im, np = m.n;
for (let j = 0; j < np; j++) {
const cs = mre[j], sn = mim[j];
l += this.cL[j] * cs - this.cLi[j] * sn;
r += this.cR[j] * cs - this.cRi[j] * sn;
}
m.step();
let mid = 0, side = 0;
if (this.hfOn) {
const him = this.hfM.im, sim = this.hfS.im;
for (let bi = 0; bi < BANDS; bi++) {
const gm = this.gM[bi], gs = this.gS[bi];
for (let j = 0; j < HFOSC; j++) {
const i = bi * HFOSC + j;
mid += gm * this.filtM[i] * him[i];
side += gs * this.filtS[i] * sim[i];
}
}
this.hfM.step(); this.hfS.step();
}
const lim = this.lowM.im, lsim = this.lowS.im;
for (let j = 0; j < LOWOSC; j++) {
mid += this.glM * lim[j];
side += this.glS * lsim[j];
}
this.lowM.step(); this.lowS.step();
const k = 0.7071067811865476;
l += (mid + side) * k;
r += (mid - side) * k;
const a = this.amp * this.env * this.ring;
out[0] += l * a; out[1] += r * a;
this.n++;
}
}
// The whole piece is one voice pool driven by the packed score.
const piano = synth.def(class {
constructor() {
this.voices = [];
this.next = 0;
this.n = 0;
}
process() {
const out = [0, 0];
while (this.next < NOTES && SCORE[this.next].s <= this.n) {
const e = SCORE[this.next++];
if (e.key) this.voices.push(new Voice(e.key, e.vel, e.dur));
}
for (let i = this.voices.length - 1; i >= 0; i--) {
const v = this.voices[i];
v.render(out);
if (v.dead) this.voices.splice(i, 1);
}
this.n++;
return [
Math.max(-0.89, Math.min(0.89, out[0] * GAIN)),
Math.max(-0.89, Math.min(0.89, out[1] * GAIN)),
];
}
});
loop(() => {
piano.play(c4, { duration: second_to_tick(176), env: one });
sleep(second_to_tick(176));
}, { name: 'Moonlight Sonata' });