r32$B&)GzZf@lJpW?$vzp8;|9@?ZL=X>ulRzS%1tvkEz%JM$I48IO
z+s@wwzX&)XA&eIe5UPdag)@aO3s(p?3O5TK!h^yS!jFabgh$#FB|7Q`)%TNh`FD~$`q?T{$BhK@h{?EB@&5RGDY&dWUgeP
zWQF8aNs45nBvZ0ok}YvaDkXkNoupCHEIBGUC+U=2m)w+mCHYqJz2t9_e@T9oJdnmo
zN$KO#0n&leXQa$Yk;vatgyujNB9lN(#KDL{ov)RHEi4_T01^stqA~
zLf9Uxc7>ePPM@>J-Qa5W9V5|di8s@|)1B)qa9W&&E<>m=Duh6iP>PnM2LqK%=@fLu{+sMR}a=Dz&?+obuSJ
zR2i+}K%=+$kn)hV+0>{y-YV{WQ&n8Fnk`mu&JYK*JH1(nSv##Jt$OouaqNYy*im$b
zbr-9SZx)-h)}2|(Ebq>MR+X_?{P5Mo=30-7O^wagLy6y_QOsI8Z1(GECeT>9zqqNm
z+0>BZ*VgW;)D&xMI%9rLk-0K2P!uQ%R(b;NYEPBVN&1|=GOx{Nt17CnI4xxso0aR!
zEmt^7O6_)~z1;4!s|sCSyDt$vDGB=h!Jyf1R6j2X?r%I6KI9Ae>U?!1{WLX~rS)dB
zp4R)#VcP5}bQD>~Pf*y49kwE6k<;d~tL!ef!;|Q7J6%=kN@rzdrOWO1R{MM;{R}Ij
zEycxFbF4j&$&>Fk*Jx|A-4uYNzT~jH$OI1}okEJC^6wOSdu5(Ezz)DJSs~V`tz_{z&of8yV^(rMp7o_iG
zy|u1hwXS}Zg+9=%ypCo_S5f!YQnbNrHqhsbDZ8CAOkn3OTRulc>jUNxjVHc_U2t{s
zl=%xLt4>mruPyj`Kg-z$pt~s)cXug8*BdihzU^dtKBeOBYA9M$+JWxQrs!=FL%!KysLcuIL9QR~v$
z+5yZcmNxp$!N$5gtx6}=Ta0?UN^duKEWx6HvC>pzDk{nIm>bj2>d(}Wr+o+8&MIlG
zUazH}r;61&-Hu`nOmD#L3E6_SfWc$5ky&Q__N~fw(#@3Wu;eAStWM&stn#{4JXYwC
z*>9k;mP>t2b!UQY-o4e0)%(fv%5rC=(qHT?aH&jgK%!|pr%{wx%dJ&bk`C3@hC=y%
zgPLB>uAtO(fXzy$BdH^(p44(SdY4YVy5Kf#uCn@yRRL>#l_z)=^;1+;@2M{KVKNE@
zFg?Ls;$m}_FP}qmM(Ji2U6e)}rI|O_u6Hl~?xR=UU8Lp~#${>_96Q!@;MkGoU749X
zcWqVEDwHyY<)$MON|w-Xv0Gjj4
zZo8*gQEzIr@2}in8TJPn=xVpu<8=pW>S}5E-6Tr>cP<%a1}It^GBoBQ`QR@#5;=(Juhz-ei4VpEaBxDDc|6Mb)+%YpB9k1KrEB>zz8M
z$!7@_1@d}l-4xz>D7VeV(+B1YD%xIA?6j$@RbG2dq5)Q$B-l#F){c{!#S|u$^J6=$>o&EG^y5(!*GF&(NpY$k0lPF4|hOt!S%hrD2|FndyDwb9uL;>*#xuU~Mr#TgcLf*<$xD
z%I&P8(eyDay@#@?X~FemmM-w?D$E6#U{tiy>u}QspDDp-4m!H3
z6k1ln&|JH8&il(hQPD;!;H7U|xpITH0U6MnsLh*6x`~~Cb;{%gz(SU>wAE!vhmEwvn4-NhEcH11>7^eJ}r;S^eT$=BLS(p@OC
zi-t8|q(hWS)=P%c8_TZDrG5G`O97o4NIjH=CUR30VSV*3hbGEinpoOWP_D1B0A0|S
zPED3lTAH3kOVf63Nw=udEomX$F>8Rf)ac92XIa{Gbm=EMepg3c4WE7gnvz~dxoB(M
zUW(q+g-kA%uCTd)VrdCpPqbthOQ%6^1y{R7^g;GEt*bF7)R^ikDEgE4uiQ|hRbCho
zx+|qkPQNvf7^v|ED%JE()~2FOE^iU!_&fopnr^kXRyQ{_?++Yvw9%29uwXiF-HN}`
zvGluJ-!1K+M^G}GKi_R~kTmRAYC2f!57AtTlxALMX}_b+?4_+rtJ6kXMvPMwc!58&
zeha-;vzjcmm)T2{G&WRXl>v*DlR&VX0&2)KP(uFS+$gKT8L@_V4ctKMz%j83Tq!I5
z!*%ge%y}^%)R4L0s+|pn?s!m``-3ZI0JwCp0;2?VW)O(*Ux2ISD7a!i1O*2R$+tlL
z`2*1m4wp|s#QqE%FjXMemVm+JYvPX(MsSDt288u%;D-4a)Fl_VLp}f@zY7fW--1Kw
zEQscBfm`StICV~ftEUazLuDZF9|0%IVNi+AgUhEKjN28&yCB%V2hOJ};EoD_%8}$kBnoy&IG%64bP8P=pe}^@QCzTfx1v4cs;}KrQP7%32ChEGQ+4
z1ZCjp$%miva(79~bNa<*XFkMPhLNydfwj(ghWu-qGMF
z!hr@k;M&)KJLw`goxURe1j?0LPzf%pFgT`s#C~u`?Ew{bGZ>u5fx{?+xQ)f-IM520
z5w##PPZF#Z91->rE)-@6eZt!y2JaBHihhohf!J$~I}q0%_q|vFVr`n(A^t!zSfY`f
zmiz(&=sKxg+9Gvmp
z=sT^ix9``FsUI8l*!Qw^GP~@u?9ct={g(Ax+pn-+OTRB3@Avqw$G>>uu_snNVSJ+b
ziCa(nVr|Csbr($=KUl71dEYS6qvDT68pwGH}oaKFLh2ERNwWw32-3H$O}WNhkQ6RX=v`y@X-6iMhwdv<{I{gVgDK~8NPn_o5QbDZzumeQaUnme;PMyoNe4s<7bWEFg|B|!T5a>gcDwx7(cOf;y2Ge^XzmkLsrri
z@mEpkZZ7pQl}cTTn3!d3FzBodsLoFJn(FNVPeQ<3Rc{Tv{)OUDbNJ+0B`(XePc-UV
zW;dnAk{7yb1@?zpfEk6U2pO5lT*?Jusjp-brSAE3HjB!?lK#NexD9pIpgSSt_SV^g
zV^F!`QsaqtyOdbSnt)8-N&wo*{QxKz*}h&W!g|~F8RC%Hm}bp}_0p)#+q#9&xrSZZ9Z8M&3C^s{$=nuYpkellJ$bQn5I
zLV4AO5`(=k&jlSs=KLnX9gu$JhzQ)I|q=rJam%!MY0dOKAT&hrBBl0svpo|J{pl`(;$dbcNt
z9z%8odW^GsjA2{QlMwV&!JQj|+7u`Oh24STDJ)9h!U}E(*H#3wcCaWxRt8-|&v%K?
z^XOBCJNq&PoxBnmhs96qc0f_jE;EU4oN`nGE(_q_~Ls9lUYmi{p35a#wk!Q~4UT
zea_bBGbfRyC1oWgO4-e;T_~$d6nWCY1f13WfHHCuB+A9o%{Nyf>H52x_cWI^mrt-J
zN1@rI?70cT$6{&gjQ2R{+0jj_npQNgaGPt30+oQWKF}`!=Is*QD`D`?sE)iSZCtbe
z)o_|m>(f^6CJS8E#kGlkZ&i(3UA@=W=xYq0-1pAD3uMoWOWDXxX`s+=a;i+ue4EW|
z*lfr!WbRnDZLaiHC2
z)xmBYNq7@fTl5tggLe1=jVDjl578AFgYgM2+5KV8wYS}2g^f?;Jz3uLu1b4V7
zNXq6#DGzM~RPW_UFUzo9V^S66n@V76O_d(A+UV66!$L8+bU`a=ZaiRXN^Cmd_BX1}
zHMP9`o>Inb2<3Jwa1Toutjwv8u{}4XlUS9kHI$1kcA+CW!Brsb&dARUSI*5v<*`0B
zpt)SbZ1xx{^OoQVioewL2Q+0-m;CQhrm>$z6Dz3^c){Gg%JrAbVh^$>n~t{mRBgVK
zhH$1awhrCYLjCK@<|}2X4Y{b@#l~NS1?Q0e{oVk+J1B&brR!PDH?HyBT2Eb#{GEXx
zSiN1vu4l2XtCsD@L=~ox1~1dTvnbS8k_JND9bh}(a-$gT4)Bo)5f{%>WO#lBG>t{=J6Nv0fs$k7P34jm(@-*~DK1k{f~m-!
z=P}*mUQ`U>Ruq|Bh5+!1E!_GJ!Q0$=CaUr74F>mmTTY<<3Ovpd(-`iGVl2914;cTC
z3Ny&*vSk$==uU^=&BIJFZz8dRiKHeGrASV)RqSDEe|ytK(g~c!rF4~6Ib15Qy@bu%
zvVMHp4DIXL=U7=1XzzdOK&iTFiVI6tP%8PK7ElXWwfyfeCE@GRhd<|`*^-56RCho;tl#0h>VXt$uC1a_`U1=4o
z8X+0?$U7r@4-j_}bhg>or7i3TC)IQJO-hAkL>d`xRV#(=T$jwFWYbx+tdxmL`6*r&
zG?&$sDl#-~CS|cuxH|wLgL1QIxrGADP5&uqKtzT26d|;mL2(xWZjy8?1$?4=xK8kK
zhX~1d67ceKO`5!gfv_^t{1nyGEKT2=aYUz*X>myyEcywUTRbu#85-!TDGL|)63o>s
z_Z+uW5fMpWp?ZDWIULtag!CL81Nt;d28Rf1MSTh
z!>yHV1#OnLU57Hmnc*}S=+TASwYi(ErYc>)3Y@aNOMn`oTN^)QdUb3NEsIRnVx6~+
z?P#G+O0TVde?eHaFg!mib6K=rv}4Uw+Aek&*iFC=xHP7x%SuoxmKamxksWSB<;#AL
z1~O_)
z=~4!4)h3t9;5JxnMlPvmvBF^~x4NyQe3{klDe)!BBG<<#V;3L@Wx0f)m
zd)~{Xw(hX%cc~C5J$WFyb)SN*+sa}Epm*b5#%59x3mUqpEB<1ayx~s75)~xmsprvj
zX;J1jPPvVHB5$^CTJ8kWQC8|unySqKhbmMOs;&%yX!@zb=`F3cc}b70sIoATvtcda
zIjOt>OCiuF3y2q0REM-(dpfgOb*#DZbi49oF2!4Y)1+$~Q;+XdW$e_Zu2n9TVjD2*
z^J|!sDO4KhA#*t!vveudkpd=WfCIxj2g6&3EX>gsZ+O3&BZI8oWrX>*>%698ORsLf
z8YVyHZ7Nu=;oBI&L(QbPjFQ`ecW;Z{Juv~G-REBFK#xDeqH7(34?0j^ShLA&ys->N`w%$oZLdgOuXs$J=IiY0!OHJ)-
z)oa>!b$y~l3#Hc%d~~5*b)o&E1J@Ei>DslXUERL6>81HfZVB-82FW|y&aDlr)`eFU
zg6Rru)-G(bK1p=Q7)J#ox5z=Tx{}E!!`)q5-pM5*?Mbfr;6+784K%k
zO$GGZe*VV!_=6o)aNE)$|8EqE++ogbZcRzqyg4PMb@RD%t*z%kWgAc*4A$2NjXIqX
zU%`5n{Qe)gFZcz2PZIKPqB8iNVC%UnW<5VxwI6pT0M_#uN8nB5$65K0C^_;OGnCrK
zs?fv%{JpwoHM5aDPH_X~G4g*7qd;0JyKwD-06ht*2))|{p~;87NPcTEracMM?fi^c)OHE`|j^g
zVzGwX^}}tDSGPc^EnV_|K8O1y>wbgLN6mfQ!~wCVD5hF&0iL$s`mRI%^Fv;Tt(5+d
zdJq*IAZ(i}m<=q~H;s}17~863plJxed2nhg3)`?XQo}5y#?4a{SWEM~W?w<5w8l%W
z;FdDSsf{ceaGjY+K`}BCV0;DPvYIjqoB9gQvgD1P5Z>}Ovb|e36r&>lWbTctv4qQg
zH3`wpb;Ngyf@-z^tcZRKiBfv9A7XVF1}ux5V_aT$rMKK$?nz<2{t~dqnLT+9tGT4e
zW-naFl4woOv_~po2?2$nA-mO9V*Skyo(p#H9I%7y%=x)i@M87a!7Dv(7l!UlVLhi=
zlg9|4$)i0#^HNGXm>5IUNtWLM@sAg{*c%`35*ttP%ju)X^%nOcR}>HxQg*IeiDdrr3~>URZq)@&!M
z@{LYIVj~r$s$o=AD$w1k?x?=ie@gvtq5%+LY_>QdCuDktf=Ch>3fm_L65a7S!6zM}
z0@#V7v-LV#16KLEhgNyNH3(LDAGU0+qYA7gc|LOj8q#9{VFQad7#4uR!pL>7fcyp<
z1R@RNr_5do&$PdcB4fqSGkErSQ3+-fge5;1NbwYtQ^rC49niI>C|OiMfW`Fl74#+J
zWI+memM0+jKVJY!PD2Z03k-;{mlOd3sk~4Y?MVz0*n8l=0lTj{1Ro}|9|F8nY@XBL
zHmTOX5cN(?H4EpaEsq1JGGvYg74+(xY$~0qv}SW{uF2#^u9=i0N3-w-OOFB;8)NE~w*gY0|H%cf=I?Z{D?Lx!4M
z&${}R7Q9n}^V^D@->^&BtHP{HUEgppwqx$vshl@Yx;kasI-NRqZS~5w&8?d+S+>FJ2HUY_fCb`-SY{6S
z`TYTQ<3%h=0bh60Jjn|H=TMs(#Ye_6QQph*)^+S7oEdm052RPB{*RsJM#RpHn-|?g
z08w}sSqKaST0MQ38AS2T@mc_va&Ii#5$O+0t0ua%c$QOBP*>*n^>%X_`@i+?JM=4%
z3-F%=?3D6jr&N?`qP&Te?Jw~UZg>yS3p?rrp|8fy-eHJTbseyu~}&=_?&7QIbj
zwiOjPO{B?L1I{S{I!~2grFn}KDu@5mIz(x6)U)u
zhg|Ra{X_;9tIlDXTV7c`hdnJ!sps@0mEZ%H+6AXWRmD^x)
zLtDGk7%=;V%v)F+cKVquj;JFud9#(p
zkO0gJ-A+7RHM-IQb3%c6k1mkPme!kV$n^W!Otjgjqs<;*uYw&fT73ky5eXRH4d}0_
zs4wsbfsA6t*1O$urq`f4J8Dq9)N4>}>NTiFt+8M)#J1Q~y|&n$V2eHZ4+gqh=?l!I
ziuSVlz^nKjsl!U@q7B
z3rJiYcX=FzZMyYfblvFs0qm*h8gEdIL>`Ck=CNF{#8g;pwyQZ`
z&!;wQv9~1zZDFwWSNdGOsv2)Y?ZM!25-lDgnS>|dXP5-QsALg0k>SQjE+OVqH2y#*
z306%38r5CM%%yC5Ssg#eSSB>>wwfwnj-v!XBG3KTu~gnI*9p*T-Jb&Qf(a;KLwl(x
zMgK^-M^f~qF45G
zrGVYq#a8O#FU_H_DLEG#BaR%Z26az<8a&+Ke5s@UB|-hcvMhfSrtLHk3?ca!Xsd%^
zSxcfm5&4dAc2@P)MC{s9F9W&tW*pdZbyDu4R96nRY#acD)l*O=CbNOa?-(-$UTv6n
zaIS-)G#Z%{3oC%AByefrKpwD%;MgQEM$hU#$XGBfrLqs{D;E|p8HI_~=_a$G$cIc*
zLo}Qrq2?79f&zXEbij=~$8uzM26L2hdEAwra!3aofE0Z)7_Jqs0lZ1mg3OPwEFc?9Gs-`
z%ON^XwTvUcfU5@sZlI{nF#Nw@n^_J@}zY*>MUFhtk*_{Gat>aORVbRC!hrt&KNes&EA
z5@<9t?z%V@4^v#r7O)`UTbgneAZhZ!L%^S5zl82}Pq`i6(W^E7Db|5CYz&%8e)B|k
z<8tbu_v`h@P-X^Y+5u5>nw*qftMnPS?}H@oD2{Sv$)T-iFQ{GZqq
znjcil|9YOQgEg#r#1#_tz#<#hH|7yR`Bx?KKfeULb^+QNb&fbn(AGuR>aqx}h`&0A
zk{>~uh?|-4y+j^*=)YRZ%Civlt(3R%b?_!W6?ZXi0EDwo^k_SC?921#xsE)4-;)|!G!ReohbiPZvXq1jqs(&lBGbf)^e{ia4MculOZ
zU6ykHU~pWT8r5Bs%0GUIg#|AEvyYO0b{((-p>HyD)zKLVb_&`$(a28pL8l1ai!5Xs
z+>I8W-fOA@E(=bMI%^>N^qRfv%;Y-rmee(1){w3_ntI->I&Z$vu>VZd8V(97xR5M*
zttHn@YTfyDjY^axZLFK^cvUKgZBjNbFZu`3M~JJqBXq#_!(vAn)aot=Omz)Kx`A9Z50}T_Ar{=+MTMk
zKebDm_wLrHdmew0(NbC#eTr(B94ecVy;My3gAX3aQ?{aOhC*wBt;I&;Ssn*PFZ8~-+2CPAA
z7!0)j#`@X=t^=-p_68$a&=YcCS)=53q&4bQj9Fl|Mg9aTEC>@r$3REW#Xs!1EjZP6
z7R)qH;qCpko~z6;&6&c}-qW7r2O8S|qxG=ofm5!?WY+G^~{a=eTl%xxs>lvL>>$
z$+I^ce*1!=r6q9Ur1I7n>N%;lDA$muTC*lA%arQekl;*hNIRwhZ^8y{NN%L{Hn=z=
zsiiD8F%CNu`JPmQUtXy;JZuu5dMSgc(h3llZ6#2okm9AW<~wDg&hW@Hf16QnED>4Voh8>iPku
z%iGhuM
zfs`i07I6jwM1DZ!45yK5a&tAHuwWNK=Y=b8&k
za1n54H8tdF)Th|3zFm8BRl9R7yS9RZe#CQK-DA7r_wmkk@Vs`XG9&nCP5HH}_s1@f
zL>o}wgV4=x4L<`g>q?XiRFetzoKGxQwI8XpzuWr3`YV=zzT9L1`AhFJom_KpWy4ak
zep&UB#mdEnOLWUrxl6aNOj~0yIYH$MV7pV`%%!6rG+reeKk{Au1iF%lMcCr*v7oe+
zLaYLWDwJ7F8&+&xqFq30U$QKitDNgw(D;&S@4_Q5pIzZIh06lIfUhEK38Y=vekr$u
z)V*KW`JwW|>dyN2RSg{nFP*#qD2xpd`~9cLuibl&xi^ND-&xQ-7Ph8qNz{l6_Pz$I
zO@LmIWl=j|_%6iFjKX%qC_ev$6|BMRk~M3V0Q#b6|2xL1T7H+uDNM$a3u`*Tl!Ie&
zqnOu751CtX>s1Z){+2^Z*^eC{plV=znr3E+R9}>jtsZ=U5g+!1#qtIa%R5`yAJOXW
zI3~xwJKvOR1!M&*0V{+V2b@(^m0qW}++A9Q!8J9L1=B4@SI?WnT5RZ+^~
zY+Q=M2F7Bm(o$IjrzSwsNP#|9`1}d7u5TGDgr(Y5HRk$KzYp0^io#P>>G8$d;UL58
z)0GvN672R;>{qR{x^1e%yAI|wY0syshz@=I1w`7so=3-Ri1B
zs@Sc19x}zzM;UOFOI?>>YNp-pe+NxN;~8z8e&5qnPM%(?dK2y7xt1Q{Jw{E~%7VU?
zMa5srqU4?E%dTi|kWC_gFNs3lICIc+tQoDNltFLF^PB5lV+*4Wl6H1hy-lBtD5GGa%kp)cETf>Ka1fmLrEx
zgzH1Pqnlc`hV*s0b-6X>a4AHVgZ^ppLwwOfg}WHL)(VSCY{hDBJhzZ1f`SC_%*Xod
z;v2+$OpY;exdV4}S#%jK#C-(^CLfqBjl=+wLX^hdGR|uX^{MH7=!4ho?dEFrd^%d@rHDquDRd=U$
z2_lZ}2Bycsv&x9$Ilvt^6z4woZ<&$rc`uDO?pM)p&=dBM1Nq-<%6;3RdR7sqm-dCvi
zgOA0RyMfXj+t!%wCEw}YA77Pf?YcsPX{#nLtvr=X4R1Q4QK2V>0#DVJ=OpYZ%+(uJ
zW>cO`2S!z!q_lKTd8sGf22pr7TEx}eSMR~N&9DTuOs>83h=Yo;As?=SETYm>yZ}5h~r2JYH9J+GD
zLfQjC3ube#VYsNM!f6F|tnr!lH7kL0HwX4L`BdQQhtR6{QW__3_8yp9F}>Wds?zt
zyEW6SGJzNl21KJ1_OV2C1mA8F~W*w)i?c
zphajgw-{*w>AJ{u28X@XXU0r7L2sbfbd%Y!;NSv18MWUm$AgJNy>|ZTb-kNA3~spu
zqS3&zb5(!!;IWRFRmcQJqnK4lmzicr_mSzoT}MwUufY1`g^US5p#Bp1Wh`%S{jW%(
zUVPbi5Zfw1|G39|I5oOSBt&=V>3>(8xq+=sCU1htt1Gb@v(_lGkG$|x*H?#5y57k>
zn~-}pwRw$mjdQ&wXG2bE#j8zkkY(j%((H#d|WsA){_H}3T}mF)+^FAlo|vu`eWk^6|rT9dz94}yL|wq0AP
ztJMYa>mB<__E+rphn;r8
zVW$K5g2PU`c4ex$XCoU|QV1b>2=!iFF4ABnGDR03#v1oeSm&1iita`pW3EZ$-JsrD#L&UbNQNVJ_=igV3yx@;F;6Q06%^xNt|!lv#Gbh26414m|HLiFI`?wWprc?U
zOMYUKlYf%RZoZrwnb?UIbjBZr2ETwNpOp7R=6A~&&MBF)aN(3GA20Y?UERX~oNnHM~584mVd1}o}v2^E@*%E36Ie^{{v*&!SXw#`Q16)ijfyvgt90YrCsP7ix$
zCcsLENK}9iJANj_%1dKaBr?!{Ac6{qt?R=@dn=j}Dh|{%Gz4+j`hNR<``-K-?FLHQ
zmI3kV(QtJ#Q?i;_)PR(WI9t>=5n}I-r^Eg
zpP4!_?Ro9%yUww(uuV9u1}9rnAT(wngl!(cVVf0v*d|wkUT4xNO|Bve*8xCx@^RII
zuR)r)3VX(MR4V!Q0Gc`MbSemYg+NX4cgJEL(N^h)
z#{(X0mch&h&f<|W2M*m#>4*%)?%s=$^$cED7ZrP!boXOMuq#;EAaopR=puPTAP_gQh#xTm)BN%)AOdfR
zWHK;J*BrKY(6W)}9c-<67ZMR#(ES**p=P~an;s2Sh#sc)1w)>&oebN3M!oWlH>4|5
zd3(+!^>zNxv>I)ep55M->QgP`3K(qEQJtFlh6G!9)I4~U-3$Mj?k5;r8!SLlun1kc
z_}1x5DzNBXeEqG}D((nx6;fjhAGQqPbGcAXn+JCpIvtTST-s|@b>x)cZPejMUzAB)S5Cgl;zv2ch%+U^2}QMPV0uy
z`eQrDatVqXFRrz?^YzMdLy^I3$uBUL7@T>|e4rnI0#S>w>LH3PyzQXwxUIGLsJCT5
z+1wa7R(70hu^!OxRfWT~jjkr|8N=C(W^w~JlzFZzuhtf>Y)o(ly#CtiTZkkZBOKwp@TX3%%d;8
zm#W_Ws?}Hpl5qkAO1iwkgYPIT%`L?T6Fj|6-Q2TjVqm4X{0de{iZ<%6O7k+^&rL}OiIKnN&>BM~q_gTvr3Iw2Oz
ztI*VM@ouwa+IQvYcO0d>wSI5N8FB_4eoKv2(VTn8cC_lSt0f$UZwx@t3&dIsV$;*t
z7ExHD+zk??-4$8C2QAto;nwewsv4%IJ?Pt~i|V4URQ2t9rK-N~W08sdBO{no^5|D3
z`oJerCcwEx{6W_|_!MI?oD4hzC+%(sRD#ih@q#IWd4dx7-bAI~gy6KGL-2{MA
zH-f(ieiE|6!NRe^XN6OR&kMH;bA%>gp|D=KPk2~(O8A!W27LbL8~6ZHk4PXI%zyf5
zoakB6YSC_yQREbPMGc}OqLZR?qKl#{q7Oyii|)aZJaJtAxUq3F;^xFHirXBwGj4ZW
zUR*((Kkh`_hjCZqejj%$?rz+_;v#W9Vv(2>%f(NMRpKGyk>b(f+2Y0G<>FQ146#OR
zgzr;0#Fb*N*e|XVH;a#p-xQx0e|-SrE3xYTgXjgvny~2Q4i<-vAbK?#LRziCpv7miX^8nxiA8(tq-2w*NpQk<29D6B
z2g4y0743r}{U}=_{oWck
zJ~n1Ed)4mzJ^8kL5_}#uQzGX2YPyN)HT&{19W70!=*2kT4$e+PSab>;iAiZ>X?}Ok
zGsEs&;LY=!YOL-ex77{aK?m3cO-_T~^4&ZId>^9Jo#=K~xLoS*uKRt?kR^~{@f(~b
zh{9o}vh*BEMAz887PpE&Uu0awkqWzIPob;uD%TJDri+7Kk~Wy&D-<|BNT=J5KR*D0
zI+-kucPPn6|U*@tV3<^odqr>y4$8ecv%Ijf$vdFr|7QjEbVevxv+!OQ|c~>
z8Rrf>bdcf|ZSp~Mp~+rkE5Qy@=u}p^n#O^$f`ia09Lz&U4?s{`uD3+K?%Su71-<#54w
zGGg{>j@roJ*T=y4J?F~u8#GonUW%#&yt46#Dv$}Pfr?%LXVk0sgZZc60}Qlu8bz0d
zs~}JnLRDR1N7xZ4^cqc6RBF*5&Vp@+#)o3CNM>~vKNsqrkJEK;B~`fpk<959PTsbftWzUiKx<&4SZr1}~9n>&ys_pbT9{P)b4Y5w2`oYa7W>kzdF=loSLE#5>g9Szi8+phWD
zHRZLYkJ{T+?d=~mT}z}t>C&w24bz^ld2#Jp)!Ma-G;6esk++
zM~Awz^T-$9DCzdLv~{%fHhj8-Hr0b1Kx6)Zjf&X3NN;0%ITDR*@Yw^ej-o+7EeUw&
z^CzLA&(u&sfo-pbqE*kcG(K7M4A2f(U1;g~lPAx^P}|yRxg{DGhfOWP17K=t^3j!S
zZ1K_aAZm_IqGZ4XYB3YAx(nR}JQJXqV-%)3V093H7mXaFt8BK_EbVlA;bRLlc;Vp<
zEdk*cSfh$g^tzq&M*heL?7ej6`|}Si)RVLnuWQ;;ruP-l@dr@SQV2AhD8G9N&KJE6
zw6Gb9f*0HbA3nfw&iIo6^c5t#LPLm3iiGB!;}G5fR%sd^Zc2KSg70JiBbyG?1P2mn
zwFkZt0jIU-sIwkEIijY+xe@sKjC31Cx1fPtwCPngR1|cRbp5fDp*9QIW;w7WO-bW}
z1R2e$oP116U8OfbB~qfq69xd3V}s{4NiJl%);w9stGaF7c3JmB^g?&;IE
X$pSMJ_x7fp)7CFrziiI7Zfy2b
literal 0
HcmV?d00001
diff --git a/res/panels/Burst.svg b/res/panels/Burst.svg
new file mode 100644
index 0000000..cf45e7b
--- /dev/null
+++ b/res/panels/Burst.svg
@@ -0,0 +1,1131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/panels/Voltio.svg b/res/panels/Voltio.svg
new file mode 100644
index 0000000..df367fb
--- /dev/null
+++ b/res/panels/Voltio.svg
@@ -0,0 +1,1338 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Â
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lienzo 1
+
+ Capa 1
+
+
+
+
+
+
+
+ Capa 1
+
+
+
+
+
+
+
+
diff --git a/src/Burst.cpp b/src/Burst.cpp
new file mode 100644
index 0000000..f8108ab
--- /dev/null
+++ b/src/Burst.cpp
@@ -0,0 +1,349 @@
+#include "plugin.hpp"
+
+#define MAX_REPETITIONS 32 /// max number of repetitions
+#define TRIGGER_TIME 0.001
+
+// a tempo/clock calculator that responds to pings - this sets the base tempo, multiplication/division of
+// this tempo occurs in the BurstEngine
+struct PingableClock {
+
+ dsp::Timer timer; // time the gap between pings
+ dsp::PulseGenerator clockTimer; // counts down from tempo length to zero
+ dsp::BooleanTrigger clockExpiry; // checks for when the clock timer runs out
+
+ float pingDuration = 0.5f; // used for calculating and updating tempo (default 2Hz / 120 bpm)
+ float tempo = 0.5f; // actual current tempo of clock
+
+ PingableClock() {
+ clockTimer.trigger(tempo);
+ }
+
+ void process(bool pingRecieved, float sampleTime) {
+ timer.process(sampleTime);
+
+ bool clockRestarted = false;
+
+ if (pingRecieved) {
+
+ bool tempoShouldBeUpdated = true;
+ float duration = timer.getTime();
+
+ // if the ping was unusually different to last time
+ bool outlier = duration > (pingDuration * 2) || duration < (pingDuration / 2);
+ // if there is a previous estimate of tempo, but it's an outlier
+ if ((pingDuration && outlier)) {
+ // don't calculate tempo from this; prime so future pings will update
+ tempoShouldBeUpdated = false;
+ pingDuration = 0;
+ }
+ else {
+ pingDuration = duration;
+ }
+ timer.reset();
+
+ if (tempoShouldBeUpdated) {
+ // if the tempo should be updated, do so
+ tempo = pingDuration;
+ clockRestarted = true;
+ }
+ }
+
+ // we restart the clock if a) a new valid ping arrived OR b) the current clock expired
+ clockRestarted = clockExpiry.process(!clockTimer.process(sampleTime)) || clockRestarted;
+ if (clockRestarted) {
+ clockTimer.reset();
+ clockTimer.trigger(tempo);
+ }
+ }
+
+ bool isTempoOutHigh() {
+ // give a 1ms pulse as tempo out
+ return clockTimer.remaining > tempo - TRIGGER_TIME;
+ }
+};
+
+// engine that generates a burst when triggered
+struct BurstEngine {
+
+ dsp::PulseGenerator eocOutput; // for generating EOC trigger
+ dsp::PulseGenerator burstOutput; // for generating triggers for each occurance of the burst
+ dsp::Timer burstTimer; // for timing how far through the current burst we are
+
+ float timings[MAX_REPETITIONS + 1] = {}; // store timings (calculated once on burst trigger)
+
+ int triggersOccurred = 0; // how many triggers have been
+ int triggersRequested = 0; // how many bursts have been requested (fixed over course of burst)
+ bool active = true; // is there a burst active
+ bool wasInhibited = false; // was this burst inhibited (i.e. just the first trigger sent)
+
+ std::tuple process(float sampleTime) {
+
+ if (active) {
+ burstTimer.process(sampleTime);
+ }
+
+ bool eocTriggered = false;
+ if (burstTimer.time > timings[triggersOccurred]) {
+ if (triggersOccurred < triggersRequested) {
+ burstOutput.reset();
+ burstOutput.trigger(TRIGGER_TIME);
+ }
+ else if (triggersOccurred == triggersRequested) {
+ eocOutput.reset();
+ eocOutput.trigger(TRIGGER_TIME);
+ active = false;
+ eocTriggered = true;
+ }
+ triggersOccurred++;
+ }
+
+ const float burstOut = burstOutput.process(sampleTime);
+ // NOTE: we don't get EOC if the burst was inhibited
+ const float eocOut = eocOutput.process(sampleTime) * !wasInhibited;
+ return std::make_tuple(burstOut, eocOut, eocTriggered);
+ }
+
+ void trigger(int numBursts, int multDiv, float baseTimeWindow, float distribution, bool inhibitBurst, bool includeOriginalTrigger) {
+
+ active = true;
+ wasInhibited = inhibitBurst;
+
+ // the window in which the burst fits is a multiple (or division) of the base tempo
+ int divisions = multDiv + (multDiv > 0 ? 1 : multDiv < 0 ? -1 : 0); // skip 2/-2
+ float actualTimeWindow = baseTimeWindow;
+ if (divisions > 0) {
+ actualTimeWindow = baseTimeWindow * divisions;
+ }
+ else if (divisions < 0) {
+ actualTimeWindow = baseTimeWindow / (-divisions);
+ }
+
+ // calculate the times at which triggers should fire, will be skewed by distribution
+ const float power = 1 + std::abs(distribution) * 2;
+ for (int i = 0; i <= numBursts; ++i) {
+ if (distribution >= 0) {
+ timings[i] = actualTimeWindow * std::pow((float)i / numBursts, power);
+ }
+ else {
+ timings[i] = actualTimeWindow * std::pow((float)i / numBursts, 1 / power);
+ }
+ }
+
+ triggersOccurred = includeOriginalTrigger ? 0 : 1;
+ triggersRequested = inhibitBurst ? 1 : numBursts;
+ burstTimer.reset();
+ }
+};
+
+struct Burst : Module {
+ enum ParamIds {
+ CYCLE_PARAM,
+ QUANTITY_PARAM,
+ TRIGGER_PARAM,
+ QUANTITY_CV_PARAM,
+ DISTRIBUTION_PARAM,
+ TIME_PARAM,
+ PROBABILITY_PARAM,
+ NUM_PARAMS
+ };
+ enum InputIds {
+ QUANTITY_INPUT,
+ DISTRIBUTION_INPUT,
+ PING_INPUT,
+ TIME_INPUT,
+ PROBABILITY_INPUT,
+ TRIGGER_INPUT,
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ TEMPO_OUTPUT,
+ EOC_OUTPUT,
+ OUT_OUTPUT,
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ ENUMS(QUANTITY_LIGHTS, 16),
+ TEMPO_LIGHT,
+ EOC_LIGHT,
+ OUT_LIGHT,
+ NUM_LIGHTS
+ };
+
+
+ dsp::SchmittTrigger pingTrigger; // for detecting Ping in
+ dsp::SchmittTrigger triggTrigger; // for detecting Trigg in
+ dsp::BooleanTrigger buttonTrigger; // for detecting when the trigger button is pressed
+ dsp::ClockDivider ledUpdate; // for only updating LEDs every N samples
+ const int ledUpdateRate = 16; // LEDs updated every N = 16 samples
+
+ PingableClock pingableClock;
+ BurstEngine burstEngine;
+ bool includeOriginalTrigger = true;
+
+ Burst() {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
+ configSwitch(Burst::CYCLE_PARAM, 0.0, 1.0, 0.0, "Mode", {"One-shot", "Cycle"});
+ auto quantityParam = configParam(Burst::QUANTITY_PARAM, 1, MAX_REPETITIONS, 0, "Number of bursts");
+ quantityParam->snapEnabled = true;
+ configButton(Burst::TRIGGER_PARAM, "Manual Trigger");
+ configParam(Burst::QUANTITY_CV_PARAM, 0.0, 1.0, 1.0, "Quantity CV");
+ configParam(Burst::DISTRIBUTION_PARAM, -1.0, 1.0, 0.0, "Distribution");
+ auto timeParam = configParam(Burst::TIME_PARAM, -4.0, 4.0, 0.0, "Time Division/Multiplication");
+ timeParam->snapEnabled = true;
+ configParam(Burst::PROBABILITY_PARAM, 0.0, 1.0, 0.0, "Probability", "%", 0.f, -100, 100.);
+
+ configInput(QUANTITY_INPUT, "Quantity CV");
+ configInput(DISTRIBUTION_INPUT, "Distribution");
+ configInput(PING_INPUT, "Ping");
+ configInput(TIME_INPUT, "Time Division/Multiplication");
+ configInput(PROBABILITY_INPUT, "Probability");
+ configInput(TRIGGER_INPUT, "Trigger");
+
+ ledUpdate.setDivision(ledUpdateRate);
+ }
+
+ void process(const ProcessArgs& args) override {
+
+ const bool pingReceived = pingTrigger.process(inputs[PING_INPUT].getVoltage());
+ pingableClock.process(pingReceived, args.sampleTime);
+
+ if (ledUpdate.process()) {
+ updateLEDRing(args);
+ }
+
+ const float quantityCV = params[QUANTITY_CV_PARAM].getValue() * clamp(inputs[QUANTITY_INPUT].getVoltage(), -5.0, +10.f) / 5.f;
+ const int quantity = clamp((int)(params[QUANTITY_PARAM].getValue() + std::round(16 * quantityCV)), 1, MAX_REPETITIONS);
+
+ const bool loop = params[CYCLE_PARAM].getValue();
+
+ const float divMultCV = 4.0 * inputs[TIME_INPUT].getVoltage() / 10.f;
+ const int divMult = -clamp((int)(divMultCV + params[TIME_PARAM].getValue()), -4, +4);
+
+ const float distributionCV = inputs[DISTRIBUTION_INPUT].getVoltage() / 10.f;
+ const float distribution = clamp(distributionCV + params[DISTRIBUTION_PARAM].getValue(), -1.f, +1.f);
+
+ const bool triggerInputTriggered = triggTrigger.process(inputs[TRIGGER_INPUT].getVoltage());
+ const bool triggerButtonTriggered = buttonTrigger.process(params[TRIGGER_PARAM].getValue());
+ const bool startBurst = triggerInputTriggered || triggerButtonTriggered;
+
+ if (startBurst) {
+ const float prob = clamp(params[PROBABILITY_PARAM].getValue() + inputs[PROBABILITY_INPUT].getVoltage() / 10.f, 0.f, 1.f);
+ const bool inhibitBurst = rack::random::uniform() < prob;
+
+ // remember to do at current tempo
+ burstEngine.trigger(quantity, divMult, pingableClock.tempo, distribution, inhibitBurst, includeOriginalTrigger);
+ }
+
+ float burstOut, eocOut;
+ bool eoc;
+ std::tie(burstOut, eocOut, eoc) = burstEngine.process(args.sampleTime);
+
+ // if the burst has finished, we can also re-trigger
+ if (eoc && loop) {
+ const float prob = clamp(params[PROBABILITY_PARAM].getValue() + inputs[PROBABILITY_INPUT].getVoltage() / 10.f, 0.f, 1.f);
+ const bool inhibitBurst = rack::random::uniform() < prob;
+
+ // remember to do at current tempo
+ burstEngine.trigger(quantity, divMult, pingableClock.tempo, distribution, inhibitBurst, includeOriginalTrigger);
+ }
+
+ const bool tempoOutHigh = pingableClock.isTempoOutHigh();
+ outputs[TEMPO_OUTPUT].setVoltage(10.f * tempoOutHigh);
+ lights[TEMPO_LIGHT].setBrightnessSmooth(tempoOutHigh, args.sampleTime);
+
+ outputs[OUT_OUTPUT].setVoltage(10.f * burstOut);
+ lights[OUT_LIGHT].setBrightnessSmooth(burstOut, args.sampleTime);
+
+ outputs[EOC_OUTPUT].setVoltage(10.f * eocOut);
+ lights[EOC_LIGHT].setBrightnessSmooth(eocOut, args.sampleTime);
+ }
+
+ void updateLEDRing(const ProcessArgs& args) {
+ int activeLed;
+ if (burstEngine.active) {
+ activeLed = (burstEngine.triggersOccurred - 1) % 16;
+ }
+ else {
+ activeLed = (((int) params[QUANTITY_PARAM].getValue() - 1) % 16);
+ }
+ for (int i = 0; i < 16; ++i) {
+ lights[QUANTITY_LIGHTS + i].setBrightnessSmooth(i == activeLed, args.sampleTime * ledUpdateRate);
+ }
+ }
+
+ json_t* dataToJson() override {
+ json_t* rootJ = json_object();
+ json_object_set_new(rootJ, "includeOriginalTrigger", json_boolean(includeOriginalTrigger));
+
+ return rootJ;
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+ json_t* includeOriginalTriggerJ = json_object_get(rootJ, "includeOriginalTrigger");
+ if (includeOriginalTriggerJ) {
+ includeOriginalTrigger = json_boolean_value(includeOriginalTriggerJ);
+ }
+ }
+};
+
+
+struct BurstWidget : ModuleWidget {
+ BurstWidget(Burst* module) {
+ setModule(module);
+ setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/Burst.svg")));
+
+ addChild(createWidget(Vec(15, 0)));
+ addChild(createWidget(Vec(15, 365)));
+
+ addParam(createParam(mm2px(Vec(28.44228, 10.13642)), module, Burst::CYCLE_PARAM));
+ addParam(createParam(mm2px(Vec(9.0322, 16.21467)), module, Burst::QUANTITY_PARAM));
+ addParam(createParam(mm2px(Vec(28.43253, 29.6592)), module, Burst::TRIGGER_PARAM));
+ addParam(createParam(mm2px(Vec(17.26197, 41.95461)), module, Burst::QUANTITY_CV_PARAM));
+ addParam(createParam(mm2px(Vec(22.85243, 58.45676)), module, Burst::DISTRIBUTION_PARAM));
+ addParam(createParam(mm2px(Vec(28.47229, 74.91607)), module, Burst::TIME_PARAM));
+ addParam(createParam(mm2px(Vec(22.75115, 91.35201)), module, Burst::PROBABILITY_PARAM));
+
+ addInput(createInput(mm2px(Vec(2.02153, 42.27628)), module, Burst::QUANTITY_INPUT));
+ addInput(createInput(mm2px(Vec(7.90118, 58.74959)), module, Burst::DISTRIBUTION_INPUT));
+ addInput(createInput(mm2px(Vec(2.05023, 75.25163)), module, Burst::PING_INPUT));
+ addInput(createInput(mm2px(Vec(13.7751, 75.23049)), module, Burst::TIME_INPUT));
+ addInput(createInput(mm2px(Vec(7.89545, 91.66642)), module, Burst::PROBABILITY_INPUT));
+ addInput(createInput(mm2px(Vec(1.11155, 109.30346)), module, Burst::TRIGGER_INPUT));
+
+ addOutput(createOutput(mm2px(Vec(11.07808, 109.30346)), module, Burst::TEMPO_OUTPUT));
+ addOutput(createOutput(mm2px(Vec(21.08452, 109.32528)), module, Burst::EOC_OUTPUT));
+ addOutput(createOutput(mm2px(Vec(31.01113, 109.30346)), module, Burst::OUT_OUTPUT));
+
+ addChild(createLight>(mm2px(Vec(14.03676, 9.98712)), module, Burst::QUANTITY_LIGHTS + 0));
+ addChild(createLight>(mm2px(Vec(18.35846, 10.85879)), module, Burst::QUANTITY_LIGHTS + 1));
+ addChild(createLight>(mm2px(Vec(22.05722, 13.31827)), module, Burst::QUANTITY_LIGHTS + 2));
+ addChild(createLight>(mm2px(Vec(24.48707, 16.96393)), module, Burst::QUANTITY_LIGHTS + 3));
+ addChild(createLight>(mm2px(Vec(25.38476, 21.2523)), module, Burst::QUANTITY_LIGHTS + 4));
+ addChild(createLight>(mm2px(Vec(24.48707, 25.5354)), module, Burst::QUANTITY_LIGHTS + 5));
+ addChild(createLight>(mm2px(Vec(22.05722, 29.16905)), module, Burst::QUANTITY_LIGHTS + 6));
+ addChild(createLight>(mm2px(Vec(18.35846, 31.62236)), module, Burst::QUANTITY_LIGHTS + 7));
+ addChild(createLight>(mm2px(Vec(14.03676, 32.48786)), module, Burst::QUANTITY_LIGHTS + 8));
+ addChild(createLight>(mm2px(Vec(9.74323, 31.62236)), module, Burst::QUANTITY_LIGHTS + 9));
+ addChild(createLight>(mm2px(Vec(6.10149, 29.16905)), module, Burst::QUANTITY_LIGHTS + 10));
+ addChild(createLight>(mm2px(Vec(3.68523, 25.5354)), module, Burst::QUANTITY_LIGHTS + 11));
+ addChild(createLight>(mm2px(Vec(2.85312, 21.2523)), module, Burst::QUANTITY_LIGHTS + 12));
+ addChild(createLight>(mm2px(Vec(3.68523, 16.96393)), module, Burst::QUANTITY_LIGHTS + 13));
+ addChild(createLight>(mm2px(Vec(6.10149, 13.31827)), module, Burst::QUANTITY_LIGHTS + 14));
+ addChild(createLight>(mm2px(Vec(9.74323, 10.85879)), module, Burst::QUANTITY_LIGHTS + 15));
+ addChild(createLight>(mm2px(Vec(14.18119, 104.2831)), module, Burst::TEMPO_LIGHT));
+ addChild(createLight>(mm2px(Vec(24.14772, 104.2831)), module, Burst::EOC_LIGHT));
+ addChild(createLight>(mm2px(Vec(34.11425, 104.2831)), module, Burst::OUT_LIGHT));
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ Burst* module = dynamic_cast(this->module);
+ assert(module);
+
+ menu->addChild(new MenuSeparator());
+ menu->addChild(createBoolPtrMenuItem("Include original trigger in output", "", &module->includeOriginalTrigger));
+ }
+};
+
+
+Model* modelBurst = createModel("Burst");
+
diff --git a/src/ChowDSP.hpp b/src/ChowDSP.hpp
index 873a4d9..4d7cd6d 100644
--- a/src/ChowDSP.hpp
+++ b/src/ChowDSP.hpp
@@ -225,7 +225,7 @@ typedef TBiquadFilter<> BiquadFilter;
Currently uses an 2*N-th order Butterworth filter.
source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp
*/
-template
+template
class AAFilter {
public:
AAFilter() = default;
@@ -255,10 +255,10 @@ class AAFilter {
auto Qs = calculateButterQs(2 * N);
for (int i = 0; i < N; ++i)
- filters[i].setParameters(BiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f);
+ filters[i].setParameters(TBiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f);
}
- inline float process(float x) noexcept {
+ inline T process(T x) noexcept {
for (int i = 0; i < N; ++i)
x = filters[i].process(x);
@@ -266,14 +266,16 @@ class AAFilter {
}
private:
- BiquadFilter filters[N];
+ TBiquadFilter filters[N];
};
+
/**
* Base class for oversampling of any order
* source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/oversampling.hpp
*/
+template
class BaseOversampling {
public:
BaseOversampling() = default;
@@ -283,13 +285,13 @@ class BaseOversampling {
virtual void reset(float /*baseSampleRate*/) = 0;
/** Upsample a single input sample and update the oversampled buffer */
- virtual void upsample(float) noexcept = 0;
+ virtual void upsample(T) noexcept = 0;
/** Output a downsampled output sample from the current oversampled buffer */
- virtual float downsample() noexcept = 0;
+ virtual T downsample() noexcept = 0;
/** Returns a pointer to the oversampled buffer */
- virtual float* getOSBuffer() noexcept = 0;
+ virtual T* getOSBuffer() noexcept = 0;
};
@@ -305,8 +307,8 @@ class BaseOversampling {
float y = oversample.downsample();
@endcode
*/
-template
-class Oversampling : public BaseOversampling {
+template
+class Oversampling : public BaseOversampling {
public:
Oversampling() = default;
virtual ~Oversampling() {}
@@ -317,7 +319,7 @@ class Oversampling : public BaseOversampling {
std::fill(osBuffer, &osBuffer[ratio], 0.0f);
}
- inline void upsample(float x) noexcept override {
+ inline void upsample(T x) noexcept override {
osBuffer[0] = ratio * x;
std::fill(&osBuffer[1], &osBuffer[ratio], 0.0f);
@@ -325,25 +327,26 @@ class Oversampling : public BaseOversampling {
osBuffer[k] = aiFilter.process(osBuffer[k]);
}
- inline float downsample() noexcept override {
- float y = 0.0f;
+ inline T downsample() noexcept override {
+ T y = 0.0f;
for (int k = 0; k < ratio; k++)
y = aaFilter.process(osBuffer[k]);
return y;
}
- inline float* getOSBuffer() noexcept override {
+ inline T* getOSBuffer() noexcept override {
return osBuffer;
}
- float osBuffer[ratio];
+ T osBuffer[ratio];
private:
- AAFilter aaFilter; // anti-aliasing filter
- AAFilter aiFilter; // anti-imaging filter
+ AAFilter aaFilter; // anti-aliasing filter
+ AAFilter aiFilter; // anti-imaging filter
};
+typedef Oversampling<1, 4, simd::float_4> OversamplingSIMD;
/**
@@ -362,7 +365,7 @@ class Oversampling : public BaseOversampling {
source (modified): https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/VariableOversampling.hpp
*/
-template
+template
class VariableOversampling {
public:
VariableOversampling() = default;
@@ -384,17 +387,17 @@ class VariableOversampling {
}
/** Upsample a single input sample and update the oversampled buffer */
- inline void upsample(float x) noexcept {
+ inline void upsample(T x) noexcept {
oss[osIdx]->upsample(x);
}
/** Output a downsampled output sample from the current oversampled buffer */
- inline float downsample() noexcept {
+ inline T downsample() noexcept {
return oss[osIdx]->downsample();
}
/** Returns a pointer to the oversampled buffer */
- inline float* getOSBuffer() noexcept {
+ inline T* getOSBuffer() noexcept {
return oss[osIdx]->getOSBuffer();
}
@@ -411,12 +414,12 @@ class VariableOversampling {
int osIdx = 0;
- Oversampling < 1 << 0, filtN > os0; // 1x
- Oversampling < 1 << 1, filtN > os1; // 2x
- Oversampling < 1 << 2, filtN > os2; // 4x
- Oversampling < 1 << 3, filtN > os3; // 8x
- Oversampling < 1 << 4, filtN > os4; // 16x
- BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 };
+ Oversampling < 1 << 0, filtN, T > os0; // 1x
+ Oversampling < 1 << 1, filtN, T > os1; // 2x
+ Oversampling < 1 << 2, filtN, T > os2; // 4x
+ Oversampling < 1 << 3, filtN, T > os3; // 8x
+ Oversampling < 1 << 4, filtN, T > os4; // 16x
+ BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 };
};
} // namespace chowdsp
diff --git a/src/PonyVCO.cpp b/src/PonyVCO.cpp
index bc424b5..ad6f52c 100644
--- a/src/PonyVCO.cpp
+++ b/src/PonyVCO.cpp
@@ -1,6 +1,7 @@
#include "plugin.hpp"
#include "ChowDSP.hpp"
+using simd::float_4;
// references:
// * "REDUCING THE ALIASING OF NONLINEAR WAVESHAPING USING CONTINUOUS-TIME CONVOLUTION" (https://www.dafx.de/paper-archive/2016/dafxpapers/20-DAFx-16_paper_41-PN.pdf)
@@ -8,46 +9,27 @@
// * https://ccrma.stanford.edu/~jatin/Notebooks/adaa.html
// * Pony waveshape https://www.desmos.com/calculator/1kvahyl4ti
+template
class FoldStage1 {
public:
- float process(float x, float xt) {
- float y;
+ T process(T x, T xt) {
+ T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5,
+ f(0.5 * (xPrev + x), xt),
+ (F(x, xt) - F(xPrev, xt)) / (x - xPrev));
- if (fabs(x - xPrev) < 1e-5) {
- y = f(0.5 * (xPrev + x), xt);
- }
- else {
- y = (F(x, xt) - F(xPrev, xt)) / (x - xPrev);
- }
xPrev = x;
return y;
}
// xt - threshold x
- static float f(float x, float xt) {
- if (x > xt) {
- return +5 * xt - 4 * x;
- }
- else if (x < -xt) {
- return -5 * xt - 4 * x;
- }
- else {
- return x;
- }
+ static T f(T x, T xt) {
+ return simd::ifelse(x > xt, +5 * xt - 4 * x, simd::ifelse(x < -xt, -5 * xt - 4 * x, x));
}
- static float F(float x, float xt) {
- if (x > xt) {
- return 5 * xt * x - 2 * x * x - 2.5 * xt * xt;
- }
- else if (x < -xt) {
- return -5 * xt * x - 2 * x * x - 2.5 * xt * xt;
-
- }
- else {
- return x * x / 2.f;
- }
+ static T F(T x, T xt) {
+ return simd::ifelse(x > xt, 5 * xt * x - 2 * x * x - 2.5 * xt * xt,
+ simd::ifelse(x < -xt, -5 * xt * x - 2 * x * x - 2.5 * xt * xt, x * x / 2.f));
}
void reset() {
@@ -55,55 +37,29 @@ class FoldStage1 {
}
private:
- float xPrev = 0.f;
+ T xPrev = 0.f;
};
+template
class FoldStage2 {
public:
- float process(float x) {
- float y;
-
- if (fabs(x - xPrev) < 1e-5) {
- y = f(0.5 * (xPrev + x));
- }
- else {
- y = (F(x) - F(xPrev)) / (x - xPrev);
- }
+ T process(T x) {
+ const T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5, f(0.5 * (xPrev + x)), (F(x) - F(xPrev)) / (x - xPrev));
xPrev = x;
return y;
}
- static float f(float x) {
- if (-(x + 2) > c) {
- return c;
- }
- else if (x < -1) {
- return -(x + 2);
- }
- else if (x < 1) {
- return x;
- }
- else if (-x + 2 > -c) {
- return -x + 2;
- }
- else {
- return -c;
- }
+ static T f(T x) {
+ return simd::ifelse(-(x + 2) > c, c, simd::ifelse(x < -1, -(x + 2), simd::ifelse(x < 1, x, simd::ifelse(-x + 2 > -c, -x + 2, -c))));
}
- static float F(float x) {
- if (x < 0) {
- return F(-x);
- }
- else if (x < 1) {
- return x * x * 0.5;
- }
- else if (x < 2 + c) {
- return 2 * x * (1.f - x * 0.25f) - 1.f;
- }
- else {
- return 2 * (2 + c) * (1 - (2 + c) * 0.25f) - 1.f - c * (x - 2 - c);
- }
+ static T F(T x) {
+ return simd::ifelse(x > 0, F_signed(x), F_signed(-x));
+ }
+
+ static T F_signed(T x) {
+ return simd::ifelse(x < 1, x * x * 0.5, simd::ifelse(x < 2.f + c, 2.f * x * (1.f - x * 0.25f) - 1.f,
+ 2.f * (2.f + c) * (1.f - (2.f + c) * 0.25f) - 1.f - c * (x - 2.f - c)));
}
void reset() {
@@ -111,8 +67,8 @@ class FoldStage2 {
}
private:
- float xPrev = 0.f;
- static constexpr float c = 0.1;
+ T xPrev = 0.f;
+ static constexpr float c = 0.1f;
};
@@ -148,10 +104,10 @@ struct PonyVCO : Module {
};
float range[4] = {8.f, 1.f, 1.f / 12.f, 10.f};
- chowdsp::VariableOversampling<6> oversampler; // uses a 2*6=12th order Butterworth filter
+ chowdsp::VariableOversampling<6, float_4> oversampler[4]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling
- dsp::RCFilter blockTZFMDCFilter;
+ dsp::TRCFilter blockTZFMDCFilter[4];
bool blockTZFMDC = true;
// hardware doesn't limit PW but some user might want to (to 5%->95%)
@@ -160,10 +116,10 @@ struct PonyVCO : Module {
// hardware has DC for non-50% duty cycle, optionally add/remove it
bool removePulseDC = true;
- dsp::SchmittTrigger syncTrigger;
+ dsp::TSchmittTrigger syncTrigger[4];
- FoldStage1 stage1;
- FoldStage2 stage2;
+ FoldStage1 stage1[4];
+ FoldStage2 stage2[4];
PonyVCO() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
@@ -191,22 +147,21 @@ struct PonyVCO : Module {
void onSampleRateChange() override {
float sampleRate = APP->engine->getSampleRate();
- blockTZFMDCFilter.setCutoffFreq(5.0 / sampleRate);
- oversampler.setOversamplingIndex(oversamplingIndex);
- oversampler.reset(sampleRate);
+ for (int c = 0; c < 4; c++) {
+ blockTZFMDCFilter[c].setCutoffFreq(5.0 / sampleRate);
+ oversampler[c].setOversamplingIndex(oversamplingIndex);
+ oversampler[c].reset(sampleRate);
- stage1.reset();
- stage2.reset();
+ stage1[c].reset();
+ stage2[c].reset();
+ }
}
// implementation taken from "Alias-Suppressed Oscillators Based on Differentiated Polynomial Waveforms",
// also the notes from Surge Synthesier repo:
// https://github.com/surge-synthesizer/surge/blob/09f1ec8e103265bef6fc0d8a0fc188238197bf8c/src/common/dsp/oscillators/ModernOscillator.cpp#L19
- // Calculation is performed at double precision, as the differencing equations appeared to work poorly with only float.
- double phase = 0.0; // phase at current (sub)sample
- double phases[3] = {}; // phase as extrapolated to the current and two previous samples
- double sawBuffer[3] = {}, sawOffsetBuff[3] = {}, triBuffer[3] = {}; // buffers for storing the terms in the difference equation
+ float_4 phase[4] = {}; // phase at current (sub)sample
void process(const ProcessArgs& args) override {
@@ -216,130 +171,160 @@ struct PonyVCO : Module {
const Waveform waveform = (Waveform) params[WAVE_PARAM].getValue();
const float mult = lfoMode ? 1.0 : dsp::FREQ_C4;
const float baseFreq = std::pow(2, (int)(params[OCT_PARAM].getValue() - 3)) * mult;
- const int oversamplingRatio = lfoMode ? 1 : oversampler.getOversamplingRatio();
- const float timbre = clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getVoltage() / 10.f, 0.f, 1.f);
-
- float tzfmVoltage = inputs[TZFM_INPUT].getVoltage();
- if (blockTZFMDC) {
- blockTZFMDCFilter.process(tzfmVoltage);
- tzfmVoltage = blockTZFMDCFilter.highpass();
- }
+ const int oversamplingRatio = lfoMode ? 1 : oversampler[0].getOversamplingRatio();
- const double pitch = inputs[VOCT_INPUT].getVoltage() + params[FREQ_PARAM].getValue() * range[rangeIndex];
- const double freq = baseFreq * simd::pow(2.f, pitch);
- const double deltaBasePhase = clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f);
- // denominator for the second-order FD
- const double denominator = 0.25 / (deltaBasePhase * deltaBasePhase);
- // not clamped, but _total_ phase treated later with floor/ceil
- const double deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio;
-
- float pw = timbre;
- if (limitPW) {
- pw = clamp(pw, 0.05, 0.95);
- }
- // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option
- // for it to be added back in for hardware compatibility reasons
- const float pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw);
-
- // hard sync
- if (syncTrigger.process(inputs[SYNC_INPUT].getVoltage())) {
- // hardware waveform is actually cos, so pi/2 phase offset is required
- // - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25
- phase = (waveform == WAVE_SIN) ? 0.25f : 0.f;
- }
+ // number of active polyphony engines (must be at least 1)
+ const int channels = std::max({inputs[TZFM_INPUT].getChannels(), inputs[VOCT_INPUT].getChannels(), inputs[TIMBRE_INPUT].getChannels(), 1});
- float* osBuffer = oversampler.getOSBuffer();
- for (int i = 0; i < oversamplingRatio; ++i) {
+ for (int c = 0; c < channels; c += 4) {
+ const float_4 timbre = simd::clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getPolyVoltageSimd(c) / 10.f, 0.f, 1.f);
- phase += deltaBasePhase + deltaFMPhase;
- if (phase > 1.f) {
- phase -= floor(phase);
+ float_4 tzfmVoltage = inputs[TZFM_INPUT].getPolyVoltageSimd(c);
+ if (blockTZFMDC) {
+ blockTZFMDCFilter[c / 4].process(tzfmVoltage);
+ tzfmVoltage = blockTZFMDCFilter[c / 4].highpass();
}
- else if (phase < 0.f) {
- phase += -ceil(phase) + 1;
+
+ const float_4 pitch = inputs[VOCT_INPUT].getPolyVoltageSimd(c) + params[FREQ_PARAM].getValue() * range[rangeIndex];
+ const float_4 freq = baseFreq * simd::pow(2.f, pitch);
+ const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f);
+ // floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator
+ // becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't
+ // a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz.
+ const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3;
+
+ // 1 / denominator for the second-order FD
+ const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase);
+ // not clamped, but _total_ phase treated later with floor/ceil
+ const float_4 deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio;
+
+ float_4 pw = timbre;
+ if (limitPW) {
+ pw = clamp(pw, 0.05, 0.95);
}
+ // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option
+ // for it to be added back in for hardware compatibility reasons
+ const float_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw);
- // sin is simple
+ // hard sync
+ const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd(c));
if (waveform == WAVE_SIN) {
- osBuffer[i] = sin2pi_pade_05_5_4(phase);
+ // hardware waveform is actually cos, so pi/2 phase offset is required
+ // - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25
+ phase[c / 4] = simd::ifelse(syncMask, 0.25f, phase[c / 4]);
}
else {
+ phase[c / 4] = simd::ifelse(syncMask, 0.f, phase[c / 4]);
+ }
- phases[0] = phase - 2 * deltaBasePhase + (phase < 2 * deltaBasePhase);
- phases[1] = phase - deltaBasePhase + (phase < deltaBasePhase);
- phases[2] = phase;
+ float_4* osBuffer = oversampler[c / 4].getOSBuffer();
+ for (int i = 0; i < oversamplingRatio; ++i) {
- switch (waveform) {
- case WAVE_TRI: {
- osBuffer[i] = aliasSuppressedTri() * denominator;
- break;
- }
- case WAVE_SAW: {
- osBuffer[i] = aliasSuppressedSaw() * denominator;
- break;
- }
- case WAVE_PULSE: {
- double saw = aliasSuppressedSaw();
- double sawOffset = aliasSuppressedOffsetSaw(pw);
+ phase[c / 4] += deltaBasePhase + deltaFMPhase;
+ // ensure within [0, 1]
+ phase[c / 4] -= simd::floor(phase[c / 4]);
- osBuffer[i] = (sawOffset - saw) * denominator;
- osBuffer[i] += pulseDCOffset;
- break;
+ // sin is simple
+ if (waveform == WAVE_SIN) {
+ osBuffer[i] = sin2pi_pade_05_5_4(phase[c / 4]);
+ }
+ else {
+ float_4 phases[3]; // phase as extrapolated to the current and two previous samples
+
+ phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f);
+ phases[1] = phase[c / 4] - deltaBasePhase + simd::ifelse(phase[c / 4] < deltaBasePhase, 1.f, 0.f);
+ phases[2] = phase[c / 4];
+
+ switch (waveform) {
+ case WAVE_TRI: {
+ const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0);
+ const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv;
+
+ osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ break;
+ }
+ case WAVE_SAW: {
+ const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0;
+ const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv;
+
+ osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ break;
+ }
+ case WAVE_PULSE: {
+ float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < 1. - pw, +1.0, -1.0);
+ dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pw) : 0.f;
+
+ float_4 saw = aliasSuppressedSaw(phases);
+ float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw);
+ float_4 dpwOrder3 = (sawOffset - saw) * denominatorInv + pulseDCOffset;
+
+ osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ break;
+ }
+ default: break;
}
- default: break;
}
- }
- if (waveform != WAVE_PULSE) {
- osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre));
- }
- }
+ if (waveform != WAVE_PULSE) {
+ osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre), c);
+ }
+
+ } // end of oversampling loop
- // downsample (if required)
- const float out = (oversamplingRatio > 1) ? oversampler.downsample() : osBuffer[0];
+ // downsample (if required)
+ const float_4 out = (oversamplingRatio > 1) ? oversampler[c / 4].downsample() : osBuffer[0];
- // end of chain VCA
- const float gain = std::max(0.f, inputs[VCA_INPUT].getNormalVoltage(10.f) / 10.f);
- outputs[OUT_OUTPUT].setVoltage(5.f * out * gain);
+ // end of chain VCA
+ const float_4 gain = simd::clamp(inputs[VCA_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f);
+ outputs[OUT_OUTPUT].setVoltageSimd(5.f * out * gain, c);
+
+ } // end of channels loop
+
+ outputs[OUT_OUTPUT].setChannels(channels);
}
- double aliasSuppressedTri() {
+ float_4 aliasSuppressedTri(float_4* phases) {
+ float_4 triBuffer[3];
for (int i = 0; i < 3; ++i) {
- double p = 2 * phases[i] - 1.0; // range -1.0 to +1.0
- double s = 0.5 - std::abs(p); // eq 30
+ float_4 p = 2 * phases[i] - 1.0; // range -1.0 to +1.0
+ float_4 s = 0.5 - simd::abs(p); // eq 30
triBuffer[i] = (s * s * s - 0.75 * s) / 3.0; // eq 29
}
return (triBuffer[0] - 2.0 * triBuffer[1] + triBuffer[2]);
}
- double aliasSuppressedSaw() {
+ float_4 aliasSuppressedSaw(float_4* phases) {
+ float_4 sawBuffer[3];
for (int i = 0; i < 3; ++i) {
- double p = 2 * phases[i] - 1.0; // range -1 to +1
+ float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11
}
return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}
- double aliasSuppressedOffsetSaw(double pw) {
+ float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) {
+ float_4 sawOffsetBuff[3];
+
for (int i = 0; i < 3; ++i) {
- double p = 2 * phases[i] - 1.0; // range -1 to +1
- double pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
- pwp += (pwp > 1) * -2; // modulo on [-1, +1]
+ float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
+ float_4 pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
+ pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1]
sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
}
return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]);
}
- float wavefolder(float x, float xt) {
- return stage2.process(stage1.process(x, xt));
+ float_4 wavefolder(float_4 x, float_4 xt, int c) {
+ return stage2[c / 4].process(stage1[c / 4].process(x, xt));
}
json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "blockTZFMDC", json_boolean(blockTZFMDC));
json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC));
- json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler.getOversamplingIndex()));
+ json_object_set_new(rootJ, "limitPW", json_boolean(limitPW));
+ json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex()));
return rootJ;
}
@@ -355,6 +340,11 @@ struct PonyVCO : Module {
removePulseDC = json_boolean_value(removePulseDCJ);
}
+ json_t* limitPWJ = json_object_get(rootJ, "limitPW");
+ if (limitPWJ) {
+ limitPW = json_boolean_value(limitPWJ);
+ }
+
json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
if (oversamplingIndexJ) {
oversamplingIndex = json_integer_value(oversamplingIndexJ);
diff --git a/src/Voltio.cpp b/src/Voltio.cpp
new file mode 100644
index 0000000..b25df3b
--- /dev/null
+++ b/src/Voltio.cpp
@@ -0,0 +1,94 @@
+#include "plugin.hpp"
+
+using simd::float_4;
+
+struct Davies1900hLargeLightGreyKnobCustom : Davies1900hLargeLightGreyKnob {
+ widget::SvgWidget* bg;
+
+ Davies1900hLargeLightGreyKnobCustom() {
+ minAngle = -0.83 * M_PI;
+ maxAngle = M_PI;
+
+ bg = new widget::SvgWidget;
+ fb->addChildBelow(bg, tw);
+ }
+};
+
+struct Voltio : Module {
+ enum ParamId {
+ OCT_PARAM,
+ RANGE_PARAM,
+ SEMITONES_PARAM,
+ PARAMS_LEN
+ };
+ enum InputId {
+ SUM_INPUT,
+ INPUTS_LEN
+ };
+ enum OutputId {
+ OUT_OUTPUT,
+ OUTPUTS_LEN
+ };
+ enum LightId {
+ PLUSMINUS5_LIGHT,
+ ZEROTOTEN_LIGHT,
+ LIGHTS_LEN
+ };
+
+ Voltio() {
+ config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
+ auto octParam = configParam(OCT_PARAM, 0.f, 10.f, 0.f, "Octave");
+ octParam->snapEnabled = true;
+
+ configSwitch(RANGE_PARAM, 0.f, 1.f, 0.f, "Range", {"-5 to +5", "0 to 10"});
+ auto semitonesParam = configParam(SEMITONES_PARAM, 0.f, 11.f, 0.f, "Semitones");
+ semitonesParam->snapEnabled = true;
+
+ configInput(SUM_INPUT, "Sum");
+ configOutput(OUT_OUTPUT, "");
+ }
+
+ void process(const ProcessArgs& args) override {
+ const int channels = std::max(1, inputs[SUM_INPUT].getChannels());
+
+ for (int c = 0; c < channels; c += 4) {
+ float_4 in = inputs[SUM_INPUT].getPolyVoltageSimd(c);
+
+ float offset = params[RANGE_PARAM].getValue() ? -5.f : 0.f;
+ in += params[SEMITONES_PARAM].getValue() / 12.f + params[OCT_PARAM].getValue() + offset;
+
+ outputs[OUT_OUTPUT].setVoltageSimd(in, c);
+ }
+
+ outputs[OUT_OUTPUT].setChannels(channels);
+
+ lights[PLUSMINUS5_LIGHT].setBrightness(params[RANGE_PARAM].getValue() ? 1.f : 0.f);
+ lights[ZEROTOTEN_LIGHT].setBrightness(params[RANGE_PARAM].getValue() ? 0.f : 1.f);
+ }
+
+};
+
+
+struct VoltioWidget : ModuleWidget {
+ VoltioWidget(Voltio* module) {
+ setModule(module);
+ setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Voltio.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addParam(createParamCentered(mm2px(Vec(15.0, 20.828)), module, Voltio::OCT_PARAM));
+ addParam(createParamCentered(mm2px(Vec(22.083, 44.061)), module, Voltio::RANGE_PARAM));
+ addParam(createParamCentered(mm2px(Vec(15.0, 67.275)), module, Voltio::SEMITONES_PARAM));
+
+ addInput(createInputCentered(mm2px(Vec(7.117, 111.003)), module, Voltio::SUM_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(22.661, 111.003)), module, Voltio::OUT_OUTPUT));
+
+ addChild(createLightCentered>(mm2px(Vec(5.695, 41.541)), module, Voltio::PLUSMINUS5_LIGHT));
+ addChild(createLightCentered>(mm2px(Vec(5.695, 46.633)), module, Voltio::ZEROTOTEN_LIGHT));
+ }
+};
+
+
+Model* modelVoltio = createModel("Voltio");
\ No newline at end of file
diff --git a/src/plugin.cpp b/src/plugin.cpp
index 49dbfdc..475f31e 100644
--- a/src/plugin.cpp
+++ b/src/plugin.cpp
@@ -27,4 +27,6 @@ void init(rack::Plugin *p) {
p->addModel(modelChannelStrip);
p->addModel(modelPonyVCO);
p->addModel(modelMotionMTR);
+ p->addModel(modelBurst);
+ p->addModel(modelVoltio);
}
diff --git a/src/plugin.hpp b/src/plugin.hpp
index 441efcb..f9d86e8 100644
--- a/src/plugin.hpp
+++ b/src/plugin.hpp
@@ -28,6 +28,8 @@ extern Model* modelNoisePlethora;
extern Model* modelChannelStrip;
extern Model* modelPonyVCO;
extern Model* modelMotionMTR;
+extern Model* modelBurst;
+extern Model* modelVoltio;
struct Knurlie : SvgScrew {
Knurlie() {
@@ -221,6 +223,21 @@ struct BefacoSlidePotSmall : app::SvgSlider {
}
};
+struct BefacoButton : app::SvgSwitch {
+ BefacoButton() {
+ momentary = true;
+ addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/BefacoButton_0.svg")));
+ addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/BefacoButton_1.svg")));
+ }
+};
+
+struct Davies1900hWhiteKnobEndless : Davies1900hKnob {
+ Davies1900hWhiteKnobEndless() {
+ setSvg(Svg::load(asset::plugin(pluginInstance, "res/components/Davies1900hWhiteEndless.svg")));
+ bg->setSvg(Svg::load(asset::plugin(pluginInstance, "res/components/Davies1900hWhiteEndless_bg.svg")));
+ }
+};
+
inline int unsigned_modulo(int a, int b) {
return ((a % b) + b) % b;
}