From 5a3fe4aa206232bd242e71a1b8c3edee9ff343a1 Mon Sep 17 00:00:00 2001 From: "W.P. van Paassen" Date: Tue, 11 Oct 2016 19:21:21 +0200 Subject: [PATCH] Inital support for Steinberg's CC121 control surface --- gtk2_ardour/icons/cc121.png | Bin 0 -> 26938 bytes libs/ardour/ardour/debug.h | 1 + libs/ardour/debug.cc | 1 + libs/surfaces/cc121/cc121.cc | 1290 ++++++++++++++++++++++++ libs/surfaces/cc121/cc121.h | 342 +++++++ libs/surfaces/cc121/cc121_interface.cc | 80 ++ libs/surfaces/cc121/gui.cc | 629 ++++++++++++ libs/surfaces/cc121/gui.h | 116 +++ libs/surfaces/cc121/operations.cc | 340 +++++++ libs/surfaces/cc121/wscript | 34 + libs/surfaces/wscript | 2 + 11 files changed, 2835 insertions(+) create mode 100644 gtk2_ardour/icons/cc121.png create mode 100644 libs/surfaces/cc121/cc121.cc create mode 100644 libs/surfaces/cc121/cc121.h create mode 100644 libs/surfaces/cc121/cc121_interface.cc create mode 100644 libs/surfaces/cc121/gui.cc create mode 100644 libs/surfaces/cc121/gui.h create mode 100644 libs/surfaces/cc121/operations.cc create mode 100644 libs/surfaces/cc121/wscript diff --git a/gtk2_ardour/icons/cc121.png b/gtk2_ardour/icons/cc121.png new file mode 100644 index 0000000000000000000000000000000000000000..098ed8eb11f86891b084c1575c47043b8bf8bb6a GIT binary patch literal 26938 zcmV)lK%c*fP)waAbx`5Wr$YF_dMQG6#{2ICOJrcC)*>x@&&pozAfL zS}Q-!y|-??D)OQ5>eYSso^$rzYp?ySZ+&YOe)hAU{qmQ;?7c^10009(06+jkL;ye} z05fR6+Sh=H5Wvh3$xPeNYsQ)| z6%l6s;0HhG#^dcbzxf$6WAe+@TTNm+uy#spIlTK80BWkr%RPyzLL| zN}pN3)v_%&#`@Dlk1(lrbyG^tx%J1*j0o4mX|JQIh~}JgF{PLi5t+u#t>12fc6x2; z2>=mPtz8~OG%%C2HVtb(aeu3U8MM!-poq5lpqoapzGFM`P3vx&_&dMzKi5seh^D~I zs%j5xn+JB%>Y50qt?6kyc?huDzF+{hntdWLLqr8@N4m5r1E2@|1R{d9mq7yqTskTs zT=|dX7Xv1+dS3o7|Lgy>zrWu;(%QGR|6IRvW_OB6`>Xby8~%egHzxp?-8YoYv8R;U z>uhc(5!*00Fq7p9nz{C;Y& zrW1-WHchPx?M?+#15?pQJ>>Eu?O4|KcHi2-Y`qa}^$e=EVF3W3wqBL{`wtP^{zN`-mPCJ zA~OL1w;ccGLYkfNRH}An*D|xI!Ny6v+#;*{u2&P62c-S*^Y4rWFmwAJA~HkoSWHAz z)M#UPYg<`8Jeuipa#sVkr8fgyVVC7t0N1xR1A}&*+jqf(8x8?z{oE_Zzl1indfNNT zjA(>Fta{c`+s$*v`ZrhW3?zEU0t|p22`M&h;EZ9drfB)(rt53keG7QtWHKcrKrZqO zkgNK%$Oi0eu_%d<$*XFXXJA-WWzp$W%|b*>l^Is3(3VHXrhRMxy)%=tHve`ZTL8G) zkp^PPRB-9F6oBqOEU#AUK5ktuYI_8P$>R8_SPUPTPZxqu9)KuBr=i0vt0 zpa2MF2#QD>Xa)_I;B!5M`wN3;h6JeE+Mc$faqj+XZ-SxCpEr-ic3ZA|^ZIeVOz-%tOW4}9PQpZe6NRP~pB>6bq7iBG)aozJ}OZEtIu_>etX3tu0`{Oggo-`gdB zq+Ofd|C?I-e{eLLUSu_2xLR#90BNz$a-G`!hl{d&_Sp{|pB#^d!xvwA`OcktjsmD; zo>T$BVCgRamw&@qU);a24e*RvLUd>BZWGyUTKDGnl^?d%`aN(Q zSnF$%4Yf62)D6&e-)iZRezA4;dOuPFGXo~!=@}fbvdahNd^vUPz?Op{1O!uvjX39n&wl0a|MD;V{C`@<`ak&S&-{~r z^4~oD^qa0+y8Mxk{M3hk@+WTIyz!6!%l~3)dklcguAPzf7?^qe+?xpY3@kc>{Lj1% z5lzhy5P*$Yuwi5a0ZU*87**6%OiVyU>R6$)I9CBo!N3$vJrWsHyDVa+>KuRj+uu1o znU2Ogw{PEF9-W|3%mb5=87_m4m1owaa{w~~H8e00RTZQ}44|u^8P${&Of@AFNg~=B zP)aGLW;UCZWwlsTEiy^ctgpX`8_uldCb)YD^Z}wFt^xPzoHA(t0BoXK+q}j7Q(%P@ z+xU$U%{%wnYp?yz@BEKfu3QmG|NNi*-gs;H>%ac%WmSFt3t#xeCqDj>k9=f4pG~I| z5f#x8vZiU)xF)6KyzwyYd4&8h?qk%^i{W8Lqm-e zN|Ow+`HcMj`J7JiKI)HE*%~oO(xSPp14dzzxHdtRutJg-}&~UD2V7cfAcqo!+z5= zts8gFHBA#jXn6_m{rbRL+eB;Wo2KwcZ`RJZsx1Ghs>~#Aw$tT#lUI#5p*q%{CS;ui zl*Fl|7QrE92^8?&FzZ$^uhY&Xc^8fyoQ!(&axtu3onn?{s?xG!GZ7pOWHyD^!Q~hHJjfzM)=iF#Cx_9qhi+b9H&$7%p*XeX_-MSS**xK51&aJV}nb2i@BDUXS6hu-LHmeMq zUY0FV-ZUoBaxX;Z@?J5SE{e=1$)XAh6NBi43_CuY&U!}Z=pdv}xK7^PI-E;BE@wyN zEJ@>(1VKUSZnrr--Lb@&sTg<~ce=`SOC`=N#F)?!7%Y)*h;;^_5S4@=sp9{u&`MJ7(vc|IjL> z*n3Yzi^a@2>U26WrYy@34i3z$)9Kv3dj}B#D5W$SjR5ra?b|6O07xkvA0KaTZ`XCb zSS-BvWm(Q^IGR>r!QQtWk{K;N8G&WVF}hY+#Ip_xf)9OaEBXUs|Ybe5eP*Ohqh8Hob- ze130w8hc$mI6_CJ3WQ}7g`u8K_r?QcFoZns^?mT&&TKAmZ>za|i`a=IGpnmw)l!m! zgTqgM`ZLGJrwGLCoO9kg@3WNBSHJqTi;rDoUdBYNr`#llTjJ_$_QYU`y?~;T0fJgJ zFMY6+lS#LmrR!Ucd-~;T|{ZpU~vXR(8RCQ}>Yd9Q+GiT% z+0E1IHt5AX%SuF(DmFq!L=pupg`y+Ss7L^;W6c8f=?u>A5C9o0>M{z5DrNmQYy=8I-R}#`i`$3E81$tF$IhN5RO1*>gqW(<`1&LA*ef9b~2y;o=D;f)(NwzjsJxh%_PpMAF5 z?WUC43$*d=iZ$6_gowzNR%WdkrWBb20M&KlT@bNeZ(yn+_>@xXrrO6!QGD?d5cLo?S;c{701#8Ucya&Uy*s^rSHa`mY11E)v!dIJA`z@S zK1{JwEE7>$;5yNag1~_OoC7%9mfdakrBdd7d9kPj|*! zQkI5X#=0)y^yH|zGz#d1jM=FrRjBKFGM!9L=P7BPXR{eir;|L-Mbd0G@t&RY0Qk;# zz7qgi2e66Q*RK1a=*|!Y(UIf+*q5b^wt9@`E*1MvUVSQLz9k;K_bZZZ4G|tl$1Kld zvo`4gy2?)=Qk(v0*)cQ_Q7}_W>&SAITwzJl1`)QSwdsSjHqUolBC!;=f>lah+MjK@#f82)9EzN z^AOzj&Uic?BjO+Y!5{qAZ~fL_FjxcR2f^UOdDJCoq%j@enUmwAgF7$2@XafaUw+|* z?_7NBeA6`Ve)qdulR`u@Y-x^7t1>g!8VDjTq1gio5WOF75SXfSPL)=P)O9La+jRJu z{e3@-elS<@$fVBtgAZj39TwICu#oC1dm;%>K-gfcIcdWK*yjR~Bum|2@Di9|ER+UkDZsR!G6 zijFJ-Aoly70CcpjhUTuz20E4SiIq>tDpUM zpSybX>f?_;QP&kQ?C+m@>BSd6{NWE1!9V?{|7}rp)~bjz*_}sb(%WDju>wg+^s)WX z-rkRzrmIgrR+d$#(_!YOXaz>-XOI9a(iG&3V4BWkyJduF%pg^0{_ zZ4>KEV|&XpvW0c1b0$4{CJ0<(LjW+9l%yy+7*p9S-0{5J8xL-j8KfpN7Io&K*OhJ- zj>=?Jx6{*@$a|N{sU+Uoi8l{Cdhn)LIp@0F zE+P>_2w79h=RWuOJkN&1K~*ol_r34_%CG#&```Z~d7jmE-HHj;O!*mVZ?!j;s8H1a zl!*+YX_bW>#5^$~GgFpj?PPA^%7-`uYH@&yL;z?x-5*3TipXNIVCJGITDS3lkmNzO z|4hPunKfTQfgeNwJnRyf9T5;AAS^s6=KaaxsoOrhc*#vqvJ+*O>Py?2x!qUq0$81Q zAcD2t$mQKdorXnv!+tzD<@ub#Xtp!<=XNfB>sG^F)Di=e7e@inPiM1i6yNxyPEX2N z?Xn!Uhd3N=-MH%-REIzyj!3Ow@>SBUo^Cykw+?#^Qjd>zM%~@Ly;tv^%A~_Nq9CAi zoMjmig%I*Q8x6Pib}u~j)RV8idi~(w-lfYI|IXj}nP;APOG*)u^1Rrbv9<3cB4&;; zt`pzh`xc3>!UT5gbVC6kV(WC}LpjDx7Xkp_+KF!UUK!V|P+7^wjmYcq;>7 zUM%RPYfjUw%WUAIIzT0YjL~%Gw5)3=pSU9Eh;?KzeCd_S(Eth|V`|fz*pg%3`=Tfi zad#J=diqIb4#974ZvjAzX`Kr_BNkdGkN_advUVSOAIu6iIAju~@vaDoYVIaA149BtYGo$C$l$cxx_vNZ4K8zwlrmz&29?baNKKvd zE%VV5&xq)pYw5hBqr+aW>zuP?))Ve$dkGl%=FOY0zWQn#G@4nj*K5JnbUJs=?d|Qo z^{sDPGwz#gF>}imKH331NGC6mAhuGvO%>N#tkN`1yJlvn3M)Uc)Qmiw9BaSYaPiKa zJ9(a`l)Bw+JMz|*ZThz}huqp|TBw4E#IBXZ1to)&3n7JIIJRQeR10r{P)kBrI6{ z#6hSk907HdV!a5#5we&dw8}~ZL?R|)0A=<7*rGxrYMO?~nNf^d6h(~9a(NICLCu)( zYhU}??c29!vsu62-`?K-^FROdr=EK1;NW0?e}89Z=aZlO<9EON-7%&!c{L)4v2m_l zR@@|G*1+NnJ?vHhjn*0E*67S^Hk;kMcMkwU$hzHLo4r5;B3x5cX8_$=L4=4ez4THm z^SXNV>iP5MtE$?hy4JAq;c#HnuMnZC03b05WEH4EWRcp~_lj}ZBxeYMsWx~ZL1M-h zW9EZ_AyZk`07{4%VG>a%rU1z6`zrzicC&mDQ(_DNbw(YbL6s7dE>Ff~NUdm-%<_b} zf*}q#Kjugxfr!-rK}DIVWkf_;bqTJqGczMvT`vHzY07m{A3)l4X!2RN+r4@7X6rl- z4-c@-m_-@A&K{7c#aj_Cb z1y78S)W`^1ttYBkm2n=5i8TqxtHoFes#C(Hr~uH6m<-J&@Ei`eTvF)^_Apz_=cov( zLgt9qvr5E7#O&0}Gn*O$9v&P>66c*`heRnQVn&dbo?k2u2E*Y$_y_+v5=u%fh+g4A z5|!B0&pr2z^XJba0+CxO89_C*Hg|YA1#aZJeg7>l%pe{j`_aUX^LjVL5C8n6<(&fiP z2qMW;TI9km7y=O;pPbC+<%Ru==m69Zab7NpEJr{!bI#qncPFLv#MLM3DthM)v}u}_ zift*KkY#mU=Xq`jn5=B7c2_?L2GPviIy~Dv&DXhkRTU9LP%$uUx0kA%K@*$gPg$0~ z`@6sU+;iUwJ~z|-{r#etef;B}_~=J}s^9NB=Zm7#GS#hv`5_TOYZJ=^1SjicQ

0 z;cKrALnoo215iGl&$ZLlW+7z>VW6rW&`g`WJFCPUznXc@b2mnRR8O}S;b?c5Ucbw^ z6F^ZhQzn-@Pv)oDLU1b}U{U1fV)lAcAtof+WKLs@na}Rtx_#~1wPm>P++ThEb1iG~>l$E6@#N%mG#n|U=bn51J@0upQKM?kxjfI?8?^}KVLRB! zDw&zF;Ue?-V8E~6?RGjPniC&8?s7J$Ze3qsNTy zi3=C+-MrJ8mG!U#^SPJ+7%`T04ZWbVX|c7%QJJV{$dB_Yvj6>Z>I$eQQ}11ADRX`r z1ze>!Z|`;o_dIm~`#$78+v)V~NHe#7b~pjIR4p$7FJTkw|N9ev#EOm?X(?1rDIOo4 zyz;%5{_ZdTV%LTL^MC)tOZ)p;NA^Cd#FPYJR##P14@SeXEI;vyPh8%=1ZFA`nVXo1 z+|Ap!^CElS``-83_3NMd$fKZ`{^Hq?BTeP1DqMeR?{%efw@v^s=mjK$-8DQdO0s;rNwTUP~#p zgktM>)-uRPieZ+wFcnRYUwu3j84t$I$?c2d0k}>OM3ABZb%eyeY1jpF9!<(}UdO7* zv-$D8PT{O55^)2d1`-?Vdnan%$0UdvQ`DFuKw8W@nalEk-U~s)P&G}593D@s(_zA> zO(Rm43k21w29vDU8$dTGiI}Q+h921N&R${zRYGi~W&qkAV>?@0yW892Ucbols;P@k z(d+jwUB1-mbxmZpm_K&$Vo`Lu{k{?3xOuB6x`SSScR2FMgea0O?VlU;dV~G|fR7H3 zJ9*LXcCTE#;MoxqV%ooWZa5xi{`|i4zU2=#rgz2m+SytTT*@mq zszJ;k#;9tdqN;UWw-jMr*L7XT7+WJK%W^uMPEIDn;lOzgnKKd&+(-f>Q!q33u1#32 zAo2a>Z?WeY`DoK5Vto5M-Xz*o9)=ZELAylGJ0Mg|i>fTk3hJb$Q6sZ^*ZbbPb8asq zuT>ohvygfAfG7lYicrPrVvb^+ya3a8zV{vd@z_G1L{-6)Cjc~vb)~}B@77N-_ZFT`MIZL;!5++Bt3kc%J7?-DDwSA|`5e-${}%Dj{e7_R+!J zDj_Gsgd0~xJN`nU}6r*5MR0WN;R3mXykoRk$GKt7Z`~Q@7}vxFG}AXL=j2J0l)VB z>!&9Z=y(d5E#|Y+Ig%2RfF2*+nKV`4tRfK7K<<0r{_f=HSeX(5Bne)pv(vzyi01Q! zsCeXh(5+ECc@HhE$t^pgqGn(KNrVi}@9ZHkDLGt9HCq(y*cr%RFa(2^Kd34tLXl(u zOuXj%&Ye5gB5Pu{rL0;5aqfI<2+sSkySqCa4%Rkauibq8iN_!B_Bw-MFVFIpCR;;_4L*z4jNuAmDuBKG(yL@_i$yi+;nC5} z`VbqH7(q=^%`TUKjGUp10E_8dRZ>;!xW@;l(}P1QGN~E_QfnaNn4)73V#;uOJT)+_ zYu_JZy_n8VwJja=lopF_Z%__KRxP?&$6`77CABJ8erj+VfsTmr!Y5G`3M?9ZT!LSh}htq0~0H&r7mKrWqVrTXFW~yd@NmWhR zh4U9K#wO-TLhww?OxxSrRaK71Bk#TUzD0}ca-R=<=!5_KfBS=Re|Y}FekWvQU4Q@8 zSB&v}Kk}Z(9=nuf!EvC~*E$KdPB}hYTFcH6p=wO0$G)>S8}z#;r}Mg&6azamazrrb z@w`TK-eF1!h{K|WQE$>M@N^Q(SyXXAeek{x(~ASYXpjlX(34B(Rm)sa#)Ff)bBK*$ z(fB+cmw;}&&o}Q9pb{e?5OQiXNi4brP;WLX9nL*Uz)s?_G@PjcWxhj&aG3o?Mk%LB@s!I*!x=abD#gb5BX@kHJ{J7x3?}|xFE?S*?2q( zAklA_>ZqVe!0`>s2Ve^Ny^P! z@R{H1<;=c?u$GpH&tr8>gl37or7h#O27 zDhYxs2ffK|K0Yav)IRelrVQTHBb>V>$LG6u22Z%V2UJPf_tNc#E9d#nQ4hGW=z=3j zi6l3VW#e|XqR$e$&`3W~9>%9norme9!?B4Bv`kSh$AzF~Z+qXn-}T=2EX#)h$a9LR zB?_y$4$l3^hyDfwdFH4MA|Zex6QRm7rg-+Je~OU^DM>Pb5JFW|A@i!RSS)t7cRu>j zzePSXAkc)!%$`7FOd*8XblU6nTJ-(EDX?CKcCK|v0whZboew_r`lBrG2J-ScnU!49CL~Jt%H0vtsQgln#7YABg!z-l#Y*e zyj5MeEXt|lz;S-(aO-q34cuc!qSmKv7C0iHLaUm0fKb;>7D7xZNeVuQsv}M!3To&; zjfeoPiAhDWkTJ8U8ek)dh#8@&s>}ydRk41*+oYPnSyV$M1wxP*Bc?25Raths-MX$f zWLjvZB67cOaHDihonuuYuuB&&?C$MsU%dF(1;->Y5;H7iBahAtYYZV*khEiqVzS_JSr93q{x3yG;ss zsTM!85!;+&mSr!z@ZHy5yIIX=8k@yDOc!%N0@2;Q?c9Yw{^TG3?0@*P=g*%%!!v{s zoM$r-6>hcX!5NYm(n_E7NO{P^Fm=^|X8B`lVQSVm1TzN7(G1auYf0F)H6cI)GE`Oo za7Y>eNr}M3j7$OTem&~4PYg4Yv$hpYbY2Nj?W8bo$c^}AwQXCr2!!B}Q7t(@hAcGz zh(u@>k%J~7IMXj-voOo<>o}yX6CzwPZ)Z|v0Dx+QWJz(E^WA7S0iXzqK&!qofGA+Z zguqGSs(#*Ywxu~!m!Yc5?C@q87l1j(brs+6)YET%%iBKwul^_mZwBMxP()&^`~9Aj z<Gn4$~Qdomhn?hdgo2_VG3x|3W-Q0 zwMocj2LePk5kvv-MiI=~@)|KOro;(>fM8WFt|}>Tp(G|nq!_44k|QM*FmgubFiBd< z;{eSpSt7&);3aw>K+I|&N|8{M2n`HO6bTie0crP%V_tY<^sE@9R(3$Js%*tGw91(j zXnTeBS8d+>5xF~rmW^L)>L2Fy9?Yc?n5h~e|8NP`+6bIT+pcGs9bQ$B3p=M~ufKld z`!B!bd@xl)^1&O}#m6pKY$m6t-EQ}O{UU%lF3b9*FMVap`Jw5oUO-9%?@1R!gYSRw zOYeW%8=G3QRs6XrN7&3{Z}w3Dur`Zi-~@4yU{1*+OXee@6RYYfHI)*xDYrE*0ESRR zX}qFBl@U3gyP{0dRTw0S(-ettqjQIvCuaam2o{7fITVc@F%P1O)^;jtxj8c?@Sq`t zn!twuF?zEIfRYpd$*r@Rt(vdR-fpJQ&J>D2tdUvo+P3>iYo?nWNzPQ@Z_qi*UO{Wf z^@D1+?^lnv%>>p(YqspMgN|rEpAUz_JTKZ}P$u?y*xlJFCzEckcSiH?D2s9S*w$7j zS*YQa8@K-Y+KZzi`@^I5@Z0 zoE&T)FH%m`Fn1GlRa69{`&Ip=gHD4@zq8mI+nq@&qj2gD2X$FOGx0_WjEd{DNR5<} zn~lP8)li*$!cLaYErjW;bV7hZ01=}BdItsz1L=3>i&74!koivLhu$?+;{ef&*J^i_ zRp$PO)$*O$&~4cEL$>>wzH4VT_bO8?o9X`hO*vM`{eCQfMryKZf3eg884?lC=kt_O zt3*gCIc7yWK0RTz&8*-ukgK8+q}T)`M3ewocYJ(&G|2j{j0tOeD3d~RL-r>Q>`7UUqF@h&&4D3v!nY%M`di~CMmw{tNP@7MYVz;|9 zSv07PLqKo_(lljOV6S)l{g=1ZXM+rZoU!_Drqnc*7txl2S$0%eqnY~=?-?2A8L8ab z4jyj0^T=ZRhr881{Qieqnyqnwnjx)9&({(&0rTwZIyOyIUFuU208(=7GEbXezP+M= zWMY9_jhHcShyEK!3WFXrwR0{22$~0^lpONZL!P`^)vgva+m8kgXyB_cFAgM8gwB^1hGn_bNRyb+V{6QU1cI9 z@BP(_+qaHRBoB2HJ4{u*0M~KEf>z}jT))>1&X^oiC(8!I{K0wdcuaT`O|OHt6@MTR7>zaB{FSEfb)sillb;uR+`yh1|oz@Y)W3klOPHRa-pJ z!oqA*^7AnI-h97Xw#9ld<*%x3moyO>kH;cH#GOtj&vVDTH`+QpIoj-f*d8T@$c7*y zB*gC0PVe&0Aa_pcYD&bNF0nHKa$ZeG^9kJe-fq^zZgt-E)EbD?%u?-&ll=>gfhnk{ zh>FB+uQNM7NSB}J+`X~ovV}27f&>VqugQST~9YNG@IzHE{iOy`R}aVOS!&qopVnE-C4zRqm)&Ayn*)T?afNf4P# zfEuJs#28tZ-Pj&g!~rp>^KL0%RA8r~?vcRa;a+iTNtkP!DgyF47HBb|q^Q75w3yET zuxhHwWRgAnv0>h^2J_SK_e@6AY+yD>rSA~-WhDSGeotk~V({r+(UK4@$lD>{{=NOX8` zR|qU7@7S1}xeFKeUw`%byeKft>R9fKwomF)F;SsKSypw8z=9Njopbw-UAR-0*Q&A` z96`u(pC?Yy1g);B3e_b|<_N8p^u8zF_|5sZr}N740H%pKp`j@wHK}2P+tn#|_Lg() za1GrbY;^TNJch~gMovZMACRXy7KrHLj1he#qRED-si(vEHV&7)OoXzn4A$ft`Ks-2==Y#o>MiD zEcW}o#q{9h1PK5orc~9~)X{ab2Bd44QvCnE^3^S4E=W29AFEq;ZWIL@iYZPt)YBPv zIu`59FkET**O9qnGLH{E+;_?T^s^=}r zxT2}gs(xs0+)0!Vw>l>@Aq>#U`P2X(Y&AvXJ!?~+9v>A0OU)$O-`G= z-5E92^dMtW5RJkK$!tE#?Re&lj0h=-YUBsfmm|`Q+RbOun>2yv2q1%a71ES8PvgCZPSsZb!Bm0CAEAd6R zmybG=gT&|ny-_Q(Gt+hBvCU1Xpk#nbK@gdfr=BjF&-}&C_g-A|I`KdHSmAn_k(nw1 zn9+)MVRqJ*Wi$@0`t0Zahw~R`ck8F0dGjxn=@h60Elk^n*w!l0A12?{eVPf0+QzRN ztg!Pw^FDLVg%F6?d6(yz0kB7AUR9+)N~vuQwF(NP04Eu`E^$VXL&*C=U7w9I?~;oi zd_M^UmsJ8Ga2$zotUFgeIC=9&G7O!jz9K+#fUNF;zQgXvGGo&hR7kyn9(sA!+c)% z`X}Ao3qc7UXoTJQg&l(idl!d~K^Xn@^RdW8#2GjN&+EQ_7L%f5 zq{OcI>#zRVdp|IJ^ILfM*uVJlcRo5lc^#VlGS%J_P^qxkB$^(A2ur1COIj_HI&I(5 zGaZYTUI)oJXV!X^v#J6j$~e0ti^u->3zuKNG1?i@s7u~}(H5zkRMW?w`p!Gv4SA{? zvmQo&p}O_m0t%2f$C%I^4yr4Dno?Q3+P$ESLpE5&PAwGy0`+9lnBjPfwNJV@1;b8G zC)1sIoCQ)d5^VD*WtaC)7Iy{~_XoA8PbiXP(8(L^-I^{kqGW(bM8Ocjwwt+3ak4co zdqXE+YY1p{M#S(d50Xw6x$@3 zs%BYHH??>9@twP_^Z#s(-eicw{$hV`cKznxzVrHj_V}fLSXa?!B*}cnYs&eNU4xg^ z<16O?>xQIjw6fBHnVBw|`>)JrO=S?GnpG)1HqkLP!?(YI3wKzZ939*pjz{As_Ok_~ zdcH-?X$1o&Ez^rnzjc0m6{?{3=EdD#RmwmX7IBt4B|~zEHL+Xiq7cx4pzDK_bCU$F zV@|t>a5&AHYMw!+PE?Vx)nDW>uk+Mz7XBbvmJ2fx4(?3ra#44)5D^iKPzhKG94G^% z+2CGm23_YpSkj|A{RTZlQe76-5t0D`i1VWL_G`DQQ5QR%;ljE{_hPjmpNYjx8YQ2= zx4l3g8IrMD5`~=k%U?bBt(V=9$|rh@dnZ`~*Z%73kA2`>Zg=PJ6}!Lx*I)bavmX*$ zFI!t{5Q$jp^4cqZboouY^Hd!l-m_Qg{l{OueE#is{inY1^=rQO({Fp@$3iBX8Ha}y zcxKi%-UCMDmPsaJL|gsY#4H;zDr(z`@;=H%bbw?S!G`FaxO%SN`0{uD>fe5KaqrYi z1k>C*mP&%u1cA6)Hm_{^hONZjA$JZ!c`J8!`#9$mQc+V{wkuer37Fc-Ml3?slN6ds z5o7S?2BkgQTuzMGA?!x!%wyKX{$v*8r+!gQqwb?AY1kx02m@@r5rWZSrIPpZ$EZ^?=S!I%jgHl0fHRj)*kyB6OSHsO%TX0Is|}jk}#*|M1=s8l)yNQt*M19O?Glv zNA{{&Q1BZ8I?FQ9`+YoqP)Az#!|9~;8mFYVN@K5az@}p{#9$t3h}JAZh(D2h3{P9MU-NmI?dXE0JT zVpbCg+}pWamZ#8Ro>#ef?sf?f=BF5|Rc~TLVCMiJm1zd~$Zc;$jrWL0lG*j|JIp#3 zP)WIQOlwKj!$~}gf`iHVqIhn!3{oMQ7*fr~kV<_x`=~{@H@Rke$4f!i~4Q>0PB9U4QZ1hkyKUMXd?c zfH&EQ2Q`?s$_yA4vK_olcTVhi_|0LtM$t$lYSF+*yNRo9cjLT&{nDDp z9DxddAoS9xbsu;H%dx6Wd!*d-1` z@~yTqgUK%du;Y~o(pkPZS$%wN#g9a=oV!M2asNHM^Padedg1l6d8<1=zwr9C>+RLm zZjy*dtu|608QFW!-o`|uHWBXM^Y%{bh)urTls`eVI+(rmYW3vNk6F&0b6U}`iq96I zr`}{}IPV)Qg3UOEV#+xzalC0XEC|Ml3d`5u_U0=L2ihtE$VQYn5UWCvW{KN|OPL_( zHn_t#(uw$BfU#1D20&>qC9k;QLfvvV8eklFLY5w;lz>p^J?}XAf&a1YqqVU{JB)I0 z^I1N%xa?ngG5o}DG=-Ij0`P!hJwN``ru2+-t*vs`ASa#ix?X=3TyfFRg*E8>oq7Xz-S;XMv$;L1I zVtH&ND~%r~yb?8+Zk_t(-(0yh^MmJKSZS^*r9lb_4ps_9SR2!5jJ@fxH(lO4bM)xFJEq{--ZP9avC@FZKnZX?#{|6r0Ky0;tTcQ`SI&R;KmV8CJ#ys8-}*cMNTmwH z$jX{UjEd%TZ@vzwROX~HRtkb83ahtno|~R5r}?;qRIBV68*2gNO2;lo00#6(s#;Ma zB_sacX7&xslc2AM0OQr^MtvnP#<^Hj$54=ExsE8R%#zuoD?aH|+pFzLBaCsEsb}Sy z4}(%W7a~unh1Q&HjMUoI$|xXrc&>drE>|3N_mtwGTJ0D~oo^A2u-{$`Y@k5zK@ebK z$3*k$joHT@-v7wsUROJqKY!|PE}Xu2=G5t%m*;G3454>c2L=@e#yIBzsom*hnf;?b z_~Si$cD?EGhaZ39@dJmackg@0p?iKdl~%{jRKmKoPAQa)V(!B#{>3%axDIUQ{v;dw zP7FQ&BqH$M4)hrYFEQ}OD$3rvrCb~+xD~001M=Vm1(j!~0n4a!;f%;)UXmzTOK}tf z9i2J-(mS4g@9N^M_HuJ#d^+`Fuv-!1m7^ZsN<_eWYl4s{K&h^^n^&*GtLHDa+TEw# zd4zCL=?Ii&wncplqQr^Dj46%FGvEE@2$fcrZcqNiPlkDxDH;cy17Yy!*=ZFADpovI zi2|b4(=SO7>-q1Rx4m=CC=Hq%cqAq49Ds&Mj1UJXc~H*Jo!yth;{2=}+JEcFp*@w7 z1_u-;75M`FTVWc z_G&@OTdOI$gCG_41>;)qzs0WcKg|%{OBt${q??+&s3|En1ixbN2=_oOAI~P{6g!} zdD}6j!m=DkV@Hp>xKc&G#^7vclA$FMI=@UffMOh zoMcD}$)hI$FU;OKQOWQXUE3^vrvA9Xc7tku@#}O3kuLw0Gnc_IRuS zqQHPKa}9M)*fc7ulyzjrl{?|Z31omi3`&KJ3IclSWYP6Z-)Q+L96yvA6V`Ro9ru!v zX930Dc@`wm8nZ0bY&5F?u%nx~uBE{^`V&@evw8)|6?`kf9Lc9yuBEC7m=;_*nyH<+ za#^q!a^P&WgP?OVWEfl2{SyV?Tei z(Wf4)hAm=qz4mZ1Ir82WSm$9#=cWXLQCugV`SzEx$@b2jJL;1vhyo*}WU;)q6t3u6 zXZMH+A_vX@&)(zdwyL7`-8?GQ!04pY#ysMX4dfPs)GO)RoE2*fb@=?L?@+xSM|N~P z&UrDYUhTHaomTMT_o6IKND)gWtN|&=gEwA?&Q4b=<%R3Ntm>|MNg;nfT0zyHJ2Q5;n& zwSV>-zkdJG`#bG5t#yynpcM#s$BGbyvET-HK5^{$$3OAEed?2+T3v0gEO-9vpZvuy z{qorK^qvAFco(c#Uu_(<2yANqt%J^rVITGo1jQWrPGXQEqKVeB{J??fsY4Xk^r_cQ zUzxe}=0_fV==j6oQiqr1w&Wq!0%t)o+OOt!-CPCZ6e*eW2nsL>P_fSgr86>USWx<= zwLqXL{>HbT``R}_6MD}8h`s{Y7O*N1gn+;UVbR>C(7$qa=H)Z9NCvS2VweL00qznZ zg#;i0+9)O042+-zEJ|?gs{GEsxsAvPVOPHZ03ZNKL_t*GD?kx&z(QIf(26!t6v0Sj zZ61br=C$wJ*9X?e^LWFTD88 z=e`q{!YB%V<2U~4!9)96tyLmQlFVqWfNsrRzjpPS%{dNBlT%Z*`bec()&f4u4(!|W zv0we4KJkfv%}&l=xb*d}fAd2h`jFPR8Hli5A%F<96+pPI5(13q1snhith-EZifPnG zvwO#0Tqa#B!CHFz^qK44yFdE#uXs~UF&FI!!3!W6A>{zNfm<=Qt?i029i^LLVDiX& zEh#7B`dLXI?B*Fk5d~owsz{LzqS6S(YZzuaR>DMt%u3h*C`1tsjE;jqmxIuNF$%&E zO`uE|=pY!VbpjAbAj0ge2|x!v4&q3MQ7GlAs4!DHTUS_OAs}(2G*QGV(qUNwomN65 z=^8zzmDbwp9&^0^MLm7IjgFz~d&HG1S6+PKOqx39^N)V?f0&$_Xth?H%kn(Q^L%c0 z?n{6A=fC$~K6n1r*Skqxt<|fQS{zjzW99@^;DjE0@WBs#=;t~~v)k=nx_J4AKm6`M zRrW`sll8!;aVzi?QT0t;3cU~3Mh$C|Zw@lzL>wCQ;7dWAbM5N2g`GQ(tR*)U1qh0A zBg#6?3CrGk1qKa)QvqepBYE4#+@n#7zaC~ZV=k7gIljF6~ zvC&3D2SL)!SC(!INS?X(|MX9N>94;qe{1#3>6acpc`WT_q?CZ%U8!k<+uR+5e*54? z0VGhW(2-vEP2DhUBt<~R2cR^0`juaPXQk@8%agq34UA-YrcJ~mA;c-CIxayLI+u%z zI+{4t1YYN^Mrj9(286=>s}DzJaRH8#Nk2ToycoF&Xi6fw&H5?9D#7s6VF6CrX8Ab<`)_2^=Ka1LvQ-6WVOpE!1C z?&dXZqmhyN^yKK$a+_T?dGIY~m#$5PyIzPd#R$Z}8>tbSe7=fsn`QZ_Q>U{mH$imd zzI#ihDCs8NQ<8M&=Wox<-k6=6pW3}=YY=>-Qde=x09ofkTH5Mdix4 z_@%{L$^>cB`S6E7{E7el-$W*wxiRzl#Y^|yf7E(6kczz%$hZ*o6Zlb@UU_Z{mL15Q zUJhBDj!N@Ol+~7OL}A`lNlA&#l6bXkE!T7<%Cas4A_bSOHfFNXse3g~>s6IkQ2Ep) zD?EBTJy2c@bS5A~x=^|zwPa} z=B}=_I=5z)!}85*SFbfj>y5^as8kE%wJd8Rs(TOLH#*wLy4jgCXP$cN(^_lS_tCnO zqgQ_}P%L$jI}r>3uuzcGT0@U`3|krlqK)#P_dNVWo^?D0t;{yPvECw=H3Eg&IY$)a zY>;dgk0Q!NiIMj;!5|OKlquibKa^ds=z(^YeM;3eDN+ozqE@crWet z+Ty~Z()#`*N3Y+yb?CkWJMP(A8><{S_xjZQ zzWsYHzIJ8d_Ck_$lQazilUsWiIJpS)(5Bm7{*4P3`er2{f<(ydHX%@%A<&osfUe|9 z2}b}BM&*K!;KhkBfbx`)RXLZl${Ko5#{A|#d1ZQU{KVg_cvI?@!THFW09@Wr0||fy zQ0oLaLetXP5OsBg;0$|G%ETqWUPX)Bl6LPn_$1=>?O-!~XLCE9B$P^*+eSwa83@X$ zvN50ra3C5~IR-!u5|1dNRnPklw+C%{FGRSqvf``;)pz?R0ZzX(K^# z(4y|&C)i&BeJJX-`$R#&C~gtelPuoe*R)sliU@#ru8^xP6y!t%Ng*m=B!ym7x5XU} zA`LHJw71UQeC*i=A&NP7!jxM99RlV9;1VGU3cIq11BnDv0Hq*iTw90a%e>iA_0NH9D^7fiy~x1WdmZ<5ez+86j&EP*_I;R3!hn* zA)#j<1R*n9YrEYfP18b2s@51OmCHWO(=0Cq;aFqrk50XG_T1UShYwe4^*22+O)N^7 zMMwk*OXUdAqr~1j@4V<9YUU0&!JZ#!PoLo~jvs?owjwPuY#>xDv7Wmr3p?j50)UWF zIRYX%M5mR6fe@$y>%kx>MtOxEJ&fH zqhQP4L=IVc6KzK94b_GNz(A?hdk(`81PE20=Sh+l`i$QDg0Pij`P9_Zg$ow~g#y{@ zZ@hYYVeW@N{J~#+;R~zFs|rMcOFJC`@SYK++wBxwdmtog~c)#=vCo@O#Jo0qt3a?Xut3tEQ; zvK>jhMt(7@A9#OV@68ge7a#~6m(eRp0T5Ns?UzN^f;Qf9>XXqd1)Hk0i%Hj~N$#9? zxd9*nFLFnp&S73=AL0fmpk(vq-wNsJJ?@1fN|GF14fC+%Sm%|l;SRYRz0N9&dZy8C zS%pFXf-H6b!`$ifkwX7ZOW9;#I8?j*zjxJ&Ul?fwkSQKKBQUYmbEGRb|pjmAvb8J87QL zsngflgMcmM5bhNBHvLMr-}*L3g5J*po;)!4zGGw$dw1Wnci)l2@qv5x0l>n-f^#ki14LoYozLF5G6MpQ(R#UDR-}-K#SP@Ux7Cr* zL?pn%G7J&k^t{4OO2ijH>^r4bj(shM{X6#rlaXk6?aU8H>-BbE_8&bNdNa=XYFLVL z7Q+g%9rfA0woD6^OoRe0j`sp-0nw-$`q$ncFZaw*NXpqEVEn{M3B-gU`O61-8x+h+&Fyu9R>o ziLhqCmD-g4bR< zrtmqE7>4R5)lw?yRKwy#n8=?7hihe+NBGNGq(&7 zTkEs~VZ$K{5!AuN-C!j*i(apG;g$=R({Lf<74KJbOc7DQv2f88Dx5Ezx4^Y1<{;Ez zgi)+W8&F0Cnk8d)!!dmQs1SN`$s3J)ijK1Y7ir90wv6?7K*(al92I*VnTjfNsC{6O z(8Rh;KC-ZaAea|TbP$4`v@mRvTPS$9X_|&%cEPd1-mMJ~}eBV-ipV7?F&QKmUiHkK)o=r*q%2WA_|5 z;H)cHH|qhvPx2gWg+tCMTf_%8X+9SQHia$3uxG_^;!Q~4GQdm&7@;-hubn;h-uM5k zWQp=xSpXJqC;81`V0L^kD45hDz+7nBdGuY<#xoKLdqapoxl+17QE!uh6J=Re z$nx&rzkl!EUDvMPxOVmWtLHA9eE6X_hBQs%I4)Jn&1N%6lln+~$JDgeL8%l^?U)=N z8*en~NIf7jvwP{x+3$bvbh$iw=+MD>y*AotBuO%a?8U8ofu3&MU1oh-ZRCb6q=utq zKh8Mi6x)E5R>;5dv47|R!l+_fP8&_gMi2oqkBn+mcl~PMO%eo2B`+hON~H@##Lnk{ zVKG!;;UW_SAOS9F5(Gx-`|crlw&+zKOH#O>DnKZhiafM32uZj=LJADEy?Z#|fr5^} zy4hA?Ue;sMi1#SsfkZ*^7OVgTwTKj$aODJ00E9p}=k8Qw6)Jpbnrf|g?b`LkTi!e~ zdtEsE{ontC>FJ$^?>(rscFx76(!}^Ui-;FMESIX)TB%yCgkflm$#bg}I`3}Xy7}>s z|ErOaF=jY^?AV(hd$iDa-sJ7KY4$tddTg+l*sAqjw5|q8-=+rVU_d~Kgt{|PS+r?# z;}*@Y;!HDaS(s^>)lBA^tq@mBg_SK!sOC>C#Pc| zMj!~p?0dp#*l$}E^_%zDQ#Al0g_r|of*q6!f+>8S5Q`KC5tImlK%Qr+AjkD5b%+Jx zffN!mFlrQ!;*niXUzw%fIRO$-BnV7W==6FJPQ*723ugr4!3#2?D#%yPIaLszHg+2a zgQDWWMx$}$$bFAL{)DqxmSw;DyZ`ax#Yrv>V|r@$?wylUje32g zTrMl6oOMbm&wgcP`BR_zKk`gOc=w*kr=NLiA^K5-uHlVuSn_9}Gom-}Shmzr1JU>4 zG-}vhbbVhzi~+G!o%^{*W(`LY0Fx0=zyO>Bl9;&T9FY$w&(|ah;HUdVKt0GrPTZ0_frQB9w;1Pha^H} zTZixBVLdTRv> z9YjIS3Wzm2;}O83&6!at!{E>7D1(OUrATdSEJ8S7;WNA)D20`;yMNLW|>>XPyay z__=RC|H><`X{EpRwXZ+-+;a~+cw+C~-Me<~tyIdSFiq2$nd@`6ZohKw{OZbT99Oc; z?%TinvBw{N;w^8U-my2!(!u^xP!%?n!JGL4MM4O;ZWKBwUe_IBMT&%mNUaysTRsvS z@<<@u+XjKO0D2GvRouOrl;EzezcFfdpz|($@X2Gx-nAb9-oI;y>Q=n%aAMHg04g3l zlE~(f6$-P$Qedh#7rw5I4T7=U2?B~+iDOpke(7t=GKP6rnxA{Y*{=7VnUpG%?o`VY zlatflb7A^ntqTK_cKO!aQms?J@0@t)%u92#E8gSPH)g^h z{@!rreTv}L(OHrD3o_XeN4?Xn2Q%}Do41?m|5O5o1{>-hZI|Hn1v8^Pug)PbL@>@8DO@;qicCitXhPXKti}(zh0{B);5oBW6LXN z7&8_sWY`9q*+4yoooAaxq)4gqbmQHJ_x$|x7ysw9)j&(Oj6VjZMCM?zNQ_4s>9d5t-Si;%sh9 z3F zmo8j8efn&t(**RZ&84@${jEvbX|-BPLzZ=GBh|x)4?p(!Bm4L7KYsi~U(mx9mc(~i zbDOT3?g^0s6zRq=Y!MH#yw z=U9;xV-nGXj`G~Fq*rgAMhFTkSZ7%W#YDp?g*Im(cEW<{+u!=0O)@gqtoJGi834$8 zPa&bh&}n9^yz6Er`rM1BGnHZ9AOp zPlpF8?*k53uk$7%%(QpUzI*oKkt0W*eDcY{y`yi}sz|9?wOSascDqSWX~nxiCO5bV zH`6;etiqvPVEEvQI5zs;LB! zm?X+rD;2Of^n*stCdM5iw&dG$3%^9c-!~MI)DVGj)!+$a$==dy{Z z#~9m&7%2nj0y)oie&JdeD(_JQ10A)R?RICeUey>vNCB`I21`Eetfomo=)Ce;WGPE8 zug;$s8#m;u4ojk(_!Vte*Oo4HQe{H0Oe~;q*e|glKHr77GyK@V?`05@<2WvS9D4c> zq?OWHmI2Vf-f7q*bjX+mw_I}EQXlr&Lh-xZ_#C3m4rt=m9r1Lwc_LZbyku z0ThGw;A_ZbZPp|o(}4ygG6|~oeDa6$Zv^ZKkqMC<>p(HH)&UU|UaCq{vy*)1%ylJX z#Uf*%H9N0_lwz;gP?RtjR4X3-_`)n$W{tw=Nj#zk)Zil>h-K*m8rJVc;jNm_cLri@ z02UVp1pPT77n`iLd2Vx~jrVTYtY;G(d8+{RM#v8iSYvuvUUI->C=8@~cBx^&%+}^v zmJ$&X2m>MA8Bo+K78Vy5i@=p6iT5rHgCyxrPw$MwXmN2dNs{r=QR|$DM5PjZFbg6faWA7PM!E_jZlFVLN)9Xtizr38@;WnGL;yViuq`VBQMr~7m=GQ4 z!qJQX#Cz#`tb)>k5+FrdG-wnM6&Mh2IYi8ufe=Lso01TP142eGB822grQ!`}V($ys zaerdYTnxC2?h|jiNJC6=*j7M%5RcczWCc{uW{S6M)7?c`dZXqG_gj;NhKLlWP;Ih% zEta15LWm#!$VYzcUGEqf8TpaQ=+I9pS65fd<#Myt)JltRv)T0CH&<8HXrt9?HJhu1 zSPYqT5E`Sj(#~gqLMRBx+>XjTaFN(n!v=-R{BW56~fEKc_B841) zcoYvq%$C|~GXkYiUS<-Jv;2WNKuEd=hw z+*n^FcZBM1AeU|3VecX_J@}L1g|cbXS^TV_FQw!~sQ=QNEFcBsfd@{A+&}CQymgPk zo~pAbr82hliLtAf-vGx!7y?n2+c*waSDMY$CID3`BXcXaOL0j&w{NuH^w?u-YwgIC z1)XQB6c^l&-t`c`LgNGwKonYsnGFNWNY0kInkov@iVZ!(qEN%|iYF;B`$`l73$g$* zu|fk5SeQr)SV9od3{2=Xy%F>?W?bLcfGO_r6iASkvZI_I<` zK_(usIKuVLselZ+ZCgCGc$lEQZ4o~}N&zL2tkqEuyw6Aq*dcbI#`W)DK@!UF(od5QL}*u)Dk(mdbB_{NZ3EVuu8z z2+qI!s?0iNR1ijK(mDObv(A%6X-qW^Jvh~;3iU)v3y+Or2;@N=$9D3!O83Ogi8_yE zbCHxflq`2-DIfd6|LyUN1*KdK)iSF$#o1 z!^f*5rF^8(i9!*tIM1>cG#5*!&U!(#iXG~h=$9Ln9kr2$QKOz6kzzOBRkxh|)0G=n z^H_ls@#|wn0Fi?E#M|b}gJ*6U-3}u#ifa)B+I#Pu)!KN^!v=xFn1f>EIBeU#N!EOm zqazXNfx!nN3JdnPpTU|P7{A?#j4?QLrDgv5+@JmaAFp&;sn0d4h*ZkX=6CO3DYe@&R0?(kd^-`wNJD>i*j(v?%O$ql>zX6(~PY1J}e`fKc z&(^;CJIYG75S0?6qTJdP-ubpg`NjGR|0Iw=RLi;X+UU=h{LAQ;QCf_tg3RWqbYp6& zcJSL>H@rzw&%7joWfkodw=H4nX$34 zy?gg&neDlKXg$cf1~`Vo2BP?rp8C!m%w~uE1$vnv4JCw|3W-5f7_Kt6(MV#>ybmZ? zXf3J0XrQoMGdgHzO)#{UbUXlRa%@u%AaU1sR`QifX~dvem|K7dmh&_y8SkX0b&n{0 zj}ZjHLRQFon+0mOtNr1BS{dJ`9(%A6jpfRh_?0qQVf2lBAt~|M@Ud<)GGFo!b>goh(v^_D5P*0$fwh zWX7;wl|VjsY0*`ahi01l%OhV|Y&J0%Z|t6M${OoDDi2IOtb&LPa0qvBQX52LS>Hs@NG&m=%wCkC}FfNxXBh~u#8&{PV z7ZC%q^WJ--OrBe%p_9AJky1f>u?v7l?>TmP{)!JF_w79I0@R9uMXw4a^p066W(jma zwk*S5O~ zawXurWysMXC`g@U3bH&DDgEcadgJ&01AXZ)w6|3RVr)AOa}DSd;*ziph=X?izy9qH zp4@HT_}xD^-3@fHTxJRY00^T=L_t)w+)dK)BqAeLixGmJB-=VV4L0|Thj$7tTFAYS z3{Y$(%-(xBb?Vgn-uM0k2lgy3ECc{U%xstGwVG+@W2BrD=X*EpFe*5 zxQLuS{le7Lj-yA9&Mw`IH6r z9k*#Wk%yMAd)rLg#=m+Y*stl<{qXt=<+a-jN^3Tt7(j;KG*5q zBr%B#%g7>tq$rRiVUG!;chC)t{l|ZI_qFTnu>|6sEP1a&9PLUcU>0ziO)EE1tpD9N zzW#7|{8SdDkaY5N6oS-bF%bwNVc#>mkm}*#Bz%+a-&Xs#VF*`IQcX=wBI=KS{8Pt| zAMbY3h1<*j{@?#!jmGHy{rf-jna@1^^wTRVD_{KL7ax7}(N|u1`RS*h{^Tb=nPu6@ zlMj96D_{BXAAipaFPwVUk3IX;(@$=@4e7#SsIV=(%Tm}{(hTB6FS%d_#I&1)rTEzB z5d`n8RT^o8c6b>rRl_ijqd2z~NW5U?`NXdKo8x;5v7+38ZH{11*C zh$|8&YPGEYmwWHa^3{h&YTu3^Zi3d3Du6zz(t8g>_4n4AxxbSZD&%S?HWbdI#0>R% z{l<+OCr_UIwO{+S=bn2`DIJDkDK6c6?-B3avHOn`(XL&)q9}UHTb@{4Tv%FKI(hQp z+w+TYTzl-Xw?6X7BWGTExyVL`;T=P@z;HIQ9mQ!l7!3D(iuhc7FE$9rcHe>hd+yom z(+molDL7`?Wg;*B*zw~&_gbL`uGQ;rdFGKTuV30ZJ}usm8YeJFXx-Az{S$E&6=~=5 zIMzxRRu~9Cz=Gf`1)~0{{x6^H?tUsiaLetUh8-%sx*E<^OaEfkUid;}Jg^a+h@>ms zFICjh7)F#?i-MNXYg#e^)oRZIWNZViLLK4Cl6&r}u2QoSXS+tCKvmXOGBohqx5iiI zD`1<(_#E(D*82RJZ$40-3<9Z)MkS^^<*u4ozA*p!&T^| zZi6@Fu=hhz$36P!qrdlipZn@pzmg=~g9i`p*}FRo!+-YA|Ec%v9hb}HEK8A~NDn{u zv0v>wkoclHSX^8@aPZ*4LkEUYqdYWk!*6}_y8r^n+jcIu5^1-pk_=^};2qa0HS1l0 z#Uw23s;tp^b#>K*VPUFdv$R~P3n~OFpb;_w4}glo+)D1+1PVb#QS{rt{oCJp{(DK1 z^-r82YLGBjZ#bz%&9M}n$2>M+Mr$_68g|S_7A+}p9@v3Tl~>*?0cI3o0u7)@u~7kt z1}vcaAT|=Q$OOveoQn!IW?F;6Dng!vCq$CiW?e73;@N}qQIH8GdQGEgNJ&ASD6oW4 zA6D_i6Hol+Z~lwc+Uh{Lf2;XuarX~*05`!&3VinB!eSi9wULoL&#bjtYeZ61(6O;- z5pL?t4)Ca2>m*6G+Ku)6!V9-i+}L>9nz?Kp&upa@F$*bdcAC8!lDTMy0C>nm88zrm z7+vaSmM{~@5fu6o7ixyoRSLBPfF4k6?w)$;>5*C;Q6WNM7c4-IbMo)i6kBL%5hP4e zM=Su|0eG|o2wXTQYKxk@?Hk;9B3)q2Spc;}E%D+Qu@%^%|a>-3(6M15u|_oWUBdJ_orF(yb@>^AAP9;~g2)v<;Kkt)QDK7xI6TS54g916QsEF8LO=nH2nZxf13=-B z2f!jMpbN11;971g@kkK&9QU*xK&WwJ9;ZQ6u`1DkA`gJ10_l%K`-m`)zJQ1gqWbXe zyYTbKy@EfE}E0sS-BTctz@+ULxKNQ0w8#h!_%;IsFoatD074}@(5fGSN0V)=896|F*R}1^{iXHAXq_lu|>m z%ykiJ=iGM93K@QRSc)he6m|Iy1fb;c!3TPTCiDQb*Y8BC_@5Y%4YU{{yV=df(_1 RGgklr002ovPDHLkV1kK9xrP7$ literal 0 HcmV?d00001 diff --git a/libs/ardour/ardour/debug.h b/libs/ardour/ardour/debug.h index 0c259b480f..0b7e5fefb5 100644 --- a/libs/ardour/ardour/debug.h +++ b/libs/ardour/ardour/debug.h @@ -80,6 +80,7 @@ namespace PBD { LIBARDOUR_API extern DebugBits BackendPorts; LIBARDOUR_API extern DebugBits VSTCallbacks; LIBARDOUR_API extern DebugBits FaderPort; + LIBARDOUR_API extern DebugBits CC121; LIBARDOUR_API extern DebugBits VCA; LIBARDOUR_API extern DebugBits Push2; diff --git a/libs/ardour/debug.cc b/libs/ardour/debug.cc index f52181611d..8c88dd4d1d 100644 --- a/libs/ardour/debug.cc +++ b/libs/ardour/debug.cc @@ -77,5 +77,6 @@ PBD::DebugBits PBD::DEBUG::BackendThreads = PBD::new_debug_bit ("backendthreads" PBD::DebugBits PBD::DEBUG::BackendPorts = PBD::new_debug_bit ("backendports"); PBD::DebugBits PBD::DEBUG::VSTCallbacks = PBD::new_debug_bit ("vstcallbacks"); PBD::DebugBits PBD::DEBUG::FaderPort = PBD::new_debug_bit ("faderport"); +PBD::DebugBits PBD::DEBUG::CC121 = PBD::new_debug_bit ("cc121"); PBD::DebugBits PBD::DEBUG::VCA = PBD::new_debug_bit ("vca"); PBD::DebugBits PBD::DEBUG::Push2 = PBD::new_debug_bit ("push2"); diff --git a/libs/surfaces/cc121/cc121.cc b/libs/surfaces/cc121/cc121.cc new file mode 100644 index 0000000000..5e68b7b446 --- /dev/null +++ b/libs/surfaces/cc121/cc121.cc @@ -0,0 +1,1290 @@ +/* + Copyright (C) 2015 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + Thanks to Rolf Meyerhoff for reverse engineering the CC121 protocol. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include +#include +#include + +#include + +#include +#include + +#include "pbd/error.h" +#include "pbd/failed_constructor.h" +#include "pbd/file_utils.h" +#include "pbd/pthread_utils.h" +#include "pbd/compose.h" +#include "pbd/xml++.h" + +#include "midi++/port.h" + +#include "control_protocol/basic_ui.h" + +#include "ardour/async_midi_port.h" +#include "ardour/audioengine.h" +#include "ardour/amp.h" +#include "ardour/bundle.h" +#include "ardour/controllable_descriptor.h" +#include "ardour/debug.h" +#include "ardour/filesystem_paths.h" +#include "ardour/midi_port.h" +#include "ardour/midiport_manager.h" +#include "ardour/monitor_processor.h" +#include "ardour/profile.h" +#include "ardour/rc_configuration.h" +#include "ardour/record_enable_control.h" +#include "ardour/stripable.h" +#include "ardour/session.h" +#include "ardour/session_configuration.h" +#include "ardour/track.h" + +#include "cc121.h" + +using namespace ARDOUR; +using namespace ArdourSurface; +using namespace PBD; +using namespace Glib; +using namespace std; + +#include "pbd/i18n.h" + +#include "pbd/abstract_ui.cc" // instantiate template + +CC121::CC121 (Session& s) + : ControlProtocol (s, _("Steinberg CC121")) + , AbstractUI (name()) + , gui (0) + , connection_state (ConnectionState (0)) + , _device_active (false) + , fader_msb (0) + , fader_lsb (0) + , fader_is_touched (false) + , _jogmode(scroll) + , button_state (ButtonState (0)) + , blink_state (false) + , rec_enable_state (false) +{ + last_encoder_time = 0; + + boost::shared_ptr inp; + boost::shared_ptr outp; + + inp = AudioEngine::instance()->register_input_port (DataType::MIDI, "CC121 Recv", true); + outp = AudioEngine::instance()->register_output_port (DataType::MIDI, "CC121 Send", true); + + _input_port = boost::dynamic_pointer_cast(inp); + _output_port = boost::dynamic_pointer_cast(outp); + + if (_input_port == 0 || _output_port == 0) { + throw failed_constructor(); + } + + _input_bundle.reset (new ARDOUR::Bundle (_("CC121 Support (Receive)"), true)); + _output_bundle.reset (new ARDOUR::Bundle (_("CC121 Support (Send) "), false)); + + _input_bundle->add_channel ( + inp->name(), + ARDOUR::DataType::MIDI, + session->engine().make_port_name_non_relative (inp->name()) + ); + + _output_bundle->add_channel ( + outp->name(), + ARDOUR::DataType::MIDI, + session->engine().make_port_name_non_relative (outp->name()) + ); + + + StripableSelectionChanged.connect (selection_connection, MISSING_INVALIDATOR, boost::bind (&CC121::gui_track_selection_changed, this, _1), this); + + /* Catch port connections and disconnections */ + ARDOUR::AudioEngine::instance()->PortConnectedOrDisconnected.connect (port_connection, MISSING_INVALIDATOR, boost::bind (&CC121::connection_handler, this, _1, _2, _3, _4, _5), this); + buttons.insert (std::make_pair (EButton, Button (*this, _("EButton"), EButton))); + buttons.insert (std::make_pair (OpenVST, Button (*this, _("OpenVST"), OpenVST))); + buttons.insert (std::make_pair (InputMonitor, Button (*this, _("InputMonitor"), InputMonitor))); + buttons.insert (std::make_pair (EQ1Enable, Button (*this, _("EQ1Enable"), EQ1Enable))); + buttons.insert (std::make_pair (EQ2Enable, Button (*this, _("EQ2Enable"), EQ2Enable))); + buttons.insert (std::make_pair (EQ3Enable, Button (*this, _("EQ3Enable"), EQ3Enable))); + buttons.insert (std::make_pair (EQ4Enable, Button (*this, _("EQ4Enable"), EQ4Enable))); + buttons.insert (std::make_pair (EQType, Button (*this, _("EQType"), EQType))); + buttons.insert (std::make_pair (AllBypass, Button (*this, _("AllBypass"), AllBypass))); + buttons.insert (std::make_pair (Function1, Button (*this, _("Function1"), Function1))); + buttons.insert (std::make_pair (Function2, Button (*this, _("Function2"), Function2))); + buttons.insert (std::make_pair (Function3, Button (*this, _("Function3"), Function3))); + buttons.insert (std::make_pair (Function4, Button (*this, _("Function4"), Function4))); + buttons.insert (std::make_pair (Value, Button (*this, _("Value"), Value))); + buttons.insert (std::make_pair (Jog, Button (*this, _("Jog"), Jog))); + buttons.insert (std::make_pair (Lock, Button (*this, _("Lock"), Lock))); + buttons.insert (std::make_pair (ToStart, Button (*this, _("ToStart"), ToStart))); + buttons.insert (std::make_pair (ToEnd, Button (*this, _("ToEnd"), ToEnd))); + buttons.insert (std::make_pair (Mute, Button (*this, _("Mute"), Mute))); + buttons.insert (std::make_pair (Solo, Button (*this, _("Solo"), Solo))); + buttons.insert (std::make_pair (Rec, Button (*this, _("Rec"), Rec))); + buttons.insert (std::make_pair (Left, Button (*this, _("Left"), Left))); + buttons.insert (std::make_pair (Right, Button (*this, _("Right"), Right))); + buttons.insert (std::make_pair (Output, Button (*this, _("Output"), Output))); + buttons.insert (std::make_pair (FP_Read, Button (*this, _("Read"), FP_Read))); + buttons.insert (std::make_pair (FP_Write, Button (*this, _("Write"), FP_Write))); + buttons.insert (std::make_pair (Loop, Button (*this, _("Loop"), Loop))); + buttons.insert (std::make_pair (Rewind, Button (*this, _("Rewind"), Rewind))); + buttons.insert (std::make_pair (Ffwd, Button (*this, _("Ffwd"), Ffwd))); + buttons.insert (std::make_pair (Stop, Button (*this, _("Stop"), Stop))); + buttons.insert (std::make_pair (Play, Button (*this, _("Play"), Play))); + buttons.insert (std::make_pair (RecEnable, Button (*this, _("RecEnable"), RecEnable))); + buttons.insert (std::make_pair (Footswitch, Button (*this, _("Footswitch"), Footswitch))); + buttons.insert (std::make_pair (FaderTouch, Button (*this, _("Fader (touch)"), FaderTouch))); + + get_button (Left).set_action ( boost::bind (&CC121::left, this), true); + get_button (Right).set_action ( boost::bind (&CC121::right, this), true); + + get_button (FP_Read).set_action (boost::bind (&CC121::read, this), true); + get_button (FP_Write).set_action (boost::bind (&CC121::write, this), true); + get_button (EButton).set_action (boost::bind (&CC121::touch, this), true); + get_button (OpenVST).set_action (boost::bind (&CC121::off, this), true); + + get_button (Play).set_action (boost::bind (&BasicUI::transport_play, this, true), true); + get_button (ToStart).set_action (boost::bind (&BasicUI::prev_marker, this), true); + get_button (ToEnd).set_action (boost::bind (&BasicUI::next_marker, this), true); + get_button (RecEnable).set_action (boost::bind (&BasicUI::rec_enable_toggle, this), true); + get_button (Stop).set_action (boost::bind (&BasicUI::transport_stop, this), true); + get_button (Ffwd).set_action (boost::bind (&BasicUI::ffwd, this), true); + + get_button (Rewind).set_action (boost::bind (&BasicUI::rewind, this), true); + get_button (Loop).set_action (boost::bind (&BasicUI::loop_toggle, this), true); + + get_button (Jog).set_action (boost::bind (&CC121::jog, this), true); + get_button (Mute).set_action (boost::bind (&CC121::mute, this), true); + get_button (Solo).set_action (boost::bind (&CC121::solo, this), true); + get_button (Rec).set_action (boost::bind (&CC121::rec_enable, this), true); + + get_button (InputMonitor).set_action (boost::bind (&CC121::input_monitor, this), true); +} + +CC121::~CC121 () +{ + all_lights_out (); + + if (_input_port) { + DEBUG_TRACE (DEBUG::CC121, string_compose ("unregistering input port %1\n", boost::shared_ptr(_input_port)->name())); + AudioEngine::instance()->unregister_port (_input_port); + _input_port.reset (); + } + + if (_output_port) { + _output_port->drain (10000, 250000); /* check every 10 msecs, wait up to 1/4 second for the port to drain */ + DEBUG_TRACE (DEBUG::CC121, string_compose ("unregistering output port %1\n", boost::shared_ptr(_output_port)->name())); + AudioEngine::instance()->unregister_port (_output_port); + _output_port.reset (); + } + + tear_down_gui (); + + /* stop event loop */ + DEBUG_TRACE (DEBUG::CC121, "BaseUI::quit ()\n"); + BaseUI::quit (); +} + +void* +CC121::request_factory (uint32_t num_requests) +{ + /* AbstractUI::request_buffer_factory() is a template method only + instantiated in this source module. To provide something visible for + use in the interface/descriptor, we have this static method that is + template-free. + */ + return request_buffer_factory (num_requests); +} + +void +CC121::start_midi_handling () +{ + /* handle buttons press */ + _input_port->parser()->channel_note_on[0].connect_same_thread (midi_connections, boost::bind (&CC121::button_press_handler, this, _1, _2)); + /* handle buttons release*/ + _input_port->parser()->channel_note_off[0].connect_same_thread (midi_connections, boost::bind (&CC121::button_release_handler, this, _1, _2)); + /* handle fader */ + _input_port->parser()->pitchbend.connect_same_thread (midi_connections, boost::bind (&CC121::fader_handler, this, _1, _2)); + /* handle encoder */ + _input_port->parser()->controller.connect_same_thread (midi_connections, boost::bind (&CC121::encoder_handler, this, _1, _2)); + + /* This connection means that whenever data is ready from the input + * port, the relevant thread will invoke our ::midi_input_handler() + * method, which will read the data, and invoke the parser. + */ + + _input_port->xthread().set_receive_handler (sigc::bind (sigc::mem_fun (this, &CC121::midi_input_handler), _input_port)); + _input_port->xthread().attach (main_loop()->get_context()); +} + +void +CC121::stop_midi_handling () +{ + midi_connections.drop_connections (); + + /* Note: the input handler is still active at this point, but we're no + * longer connected to any of the parser signals + */ +} + +void +CC121::do_request (CC121Request* req) +{ + if (req->type == CallSlot) { + + call_slot (MISSING_INVALIDATOR, req->the_slot); + + } else if (req->type == Quit) { + + stop (); + } +} + +int +CC121::stop () +{ + BaseUI::quit (); + + return 0; +} + +void +CC121::thread_init () +{ + struct sched_param rtparam; + + pthread_set_name (event_loop_name().c_str()); + + PBD::notify_event_loops_about_thread_creation (pthread_self(), event_loop_name(), 2048); + ARDOUR::SessionEvent::create_per_thread_pool (event_loop_name(), 128); + + memset (&rtparam, 0, sizeof (rtparam)); + rtparam.sched_priority = 9; /* XXX should be relative to audio (JACK) thread */ + + if (pthread_setschedparam (pthread_self(), SCHED_FIFO, &rtparam) != 0) { + // do we care? not particularly. + } +} + +void +CC121::all_lights_out () +{ + for (ButtonMap::iterator b = buttons.begin(); b != buttons.end(); ++b) { + b->second.set_led_state (_output_port, false); + } +} + +CC121::Button& +CC121::get_button (ButtonID id) const +{ + ButtonMap::const_iterator b = buttons.find (id); + assert (b != buttons.end()); + return const_cast(b->second); +} + +void +CC121::button_press_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose ("button press event for ID %1 press ? %2\n", (int) tb->controller_number, (tb->value ? "yes" : "no"))); + + ButtonID id (ButtonID (tb->controller_number)); + Button& button (get_button (id)); + + buttons_down.insert (id); + ButtonState bs (ButtonState (0)); + + switch (id) { + case FaderTouch: + fader_is_touched = true; + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + framepos_t now = session->engine().sample_time(); + gain->start_touch (now); + } + } + break; + default: + break; + } + + if (bs) { + button_state = ButtonState (button_state|bs); + DEBUG_TRACE (DEBUG::CC121, string_compose ("reset button state to %1 using %2\n", button_state, (int) bs)); + } + + if (button.uses_flash()) { + button.set_led_state (_output_port, (int)tb->value); + } + + set::iterator c = consumed.find (id); + + if (c == consumed.end()) { + button.invoke (button_state, true); + } else { + DEBUG_TRACE (DEBUG::CC121, "button was consumed, ignored\n"); + consumed.erase (c); + } +} + +void +CC121::button_release_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose ("button release event for ID %1 release ? %2\n", (int) tb->controller_number, (tb->value ? "yes" : "no"))); + + ButtonID id (ButtonID (tb->controller_number)); + Button& button (get_button (id)); + + buttons_down.erase (id); + button.timeout_connection.disconnect (); + + ButtonState bs (ButtonState (0)); + + switch (id) { + case FaderTouch: + fader_is_touched = false; + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + framepos_t now = session->engine().sample_time(); + gain->stop_touch (true, now); + } + } + break; + default: + break; + } + + if (bs) { + button_state = ButtonState (button_state&~bs); + DEBUG_TRACE (DEBUG::CC121, string_compose ("reset button state to %1 using %2\n", button_state, (int) bs)); + } + + if (button.uses_flash()) { + button.set_led_state (_output_port, (int)tb->value); + } + + set::iterator c = consumed.find (id); + + if (c == consumed.end()) { + button.invoke (button_state, false); + } else { + DEBUG_TRACE (DEBUG::CC121, "button was consumed, ignored\n"); + consumed.erase (c); + } +} + +void +CC121::encoder_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb) +{ + DEBUG_TRACE (DEBUG::CC121, "encoder handler"); + + /* Extract absolute value*/ + float adj = static_cast(tb->value & ~0x40); + + /* Get direction (negative values start at 0x40)*/ + float sign = (tb->value & 0x40) ? -1.0 : 1.0; + switch(tb->controller_number) { + case 0x10: + /* pan */ + DEBUG_TRACE (DEBUG::CC121, "PAN encoder"); + if (_current_stripable) { + /* Get amount of change (encoder clicks) * (change per click)*/ + /*Create an exponential curve*/ + float curve = sign * pow(adj, (1.0 + 10.0) / 10.0); + adj = curve * (31 / 1000.0); + ardour_pan_azimuth (adj); + } + break; + case 0x20: + /* EQ 1 Q */ + break; + case 0x21: + /* EQ 2 Q */ + break; + case 0x22: + /* EQ 3 Q */ + break; + case 0x23: + /* EQ 4 Q */ + break; + case 0x30: + /* EQ 1 Frequency */ + break; + case 0x31: + /* EQ 2 Frequency */ + break; + case 0x32: + /* EQ 3 Frequency */ + break; + case 0x33: + /* EQ 4 Frequency */ + break; + case 0x3C: + /* AI */ + if (sign < 0.0f) { + if (_jogmode == scroll) { + ScrollTimeline(-0.05); + } + else { + ZoomIn(); + } + } + else { + if (_jogmode == scroll) { + ScrollTimeline(0.05); + } + else { + ZoomOut(); + } + } + break; + case 0x40: + /* EQ 1 Gain */ + break; + case 0x41: + /* EQ 2 Gain */ + break; + case 0x42: + /* EQ 3 Gain */ + break; + case 0x43: + /* EQ 4 Gain */ + break; + case 0x50: + /* Value */ + break; + default: + break; + } +} + +void +CC121::fader_handler (MIDI::Parser &, MIDI::pitchbend_t pb) +{ + DEBUG_TRACE (DEBUG::CC121, "fader handler"); + + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + float val = gain->interface_to_internal (pb/16384.0); + /* even though the cc121 only controls a + single stripable at a time, allow the fader to + modify the group, if appropriate. + */ + _current_stripable->gain_control()->set_value (val, Controllable::UseGroup); + } + } +} + +int +CC121::set_active (bool yn) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose("CC121::set_active init with yn: '%1'\n", yn)); + + if (yn == active()) { + return 0; + } + + if (yn) { + + /* start event loop */ + + BaseUI::run (); + + connect_session_signals (); + + Glib::RefPtr blink_timeout = Glib::TimeoutSource::create (200); // milliseconds + blink_connection = blink_timeout->connect (sigc::mem_fun (*this, &CC121::blink)); + blink_timeout->attach (main_loop()->get_context()); + + Glib::RefPtr heartbeat_timeout = Glib::TimeoutSource::create (800); // milliseconds + heartbeat_connection = heartbeat_timeout->connect (sigc::mem_fun (*this, &CC121::beat)); + heartbeat_timeout->attach (main_loop()->get_context()); + + Glib::RefPtr periodic_timeout = Glib::TimeoutSource::create (100); // milliseconds + periodic_connection = periodic_timeout->connect (sigc::mem_fun (*this, &CC121::periodic)); + periodic_timeout->attach (main_loop()->get_context()); + + } else { + + BaseUI::quit (); + close (); + + } + + ControlProtocol::set_active (yn); + + DEBUG_TRACE (DEBUG::CC121, string_compose("CC121::set_active done with yn: '%1'\n", yn)); + + return 0; +} + +bool +CC121::periodic () +{ + if (!_current_stripable) { + return true; + } + + ARDOUR::AutoState gain_state = _current_stripable->gain_control()->automation_state(); + + if (gain_state == ARDOUR::Touch || gain_state == ARDOUR::Play) { + map_gain (); + } + + return true; +} + +void +CC121::stop_blinking (ButtonID id) +{ + blinkers.remove (id); + get_button (id).set_led_state (_output_port, false); +} + +void +CC121::start_blinking (ButtonID id) +{ + blinkers.push_back (id); + get_button (id).set_led_state (_output_port, true); +} + +bool +CC121::beat () +{ + MIDI::byte buf[8]; + + buf[0] = 0xf0; + buf[1] = 0x43; + buf[2] = 0x10; + buf[3] = 0x3e; + buf[4] = 0x15; + buf[5] = 0x00; + buf[6] = 0x01; + buf[7] = 0xF7; + + _output_port->write (buf, 8, 0); + + return true; +} + +bool +CC121::blink () +{ + blink_state = !blink_state; + + for (Blinkers::iterator b = blinkers.begin(); b != blinkers.end(); b++) { + get_button(*b).set_led_state (_output_port, blink_state); + } + + map_recenable_state (); + + return true; +} + +void +CC121::close () +{ + all_lights_out (); + + stop_midi_handling (); + session_connections.drop_connections (); + port_connection.disconnect (); + blink_connection.disconnect (); + heartbeat_connection.disconnect (); + selection_connection.disconnect (); + stripable_connections.drop_connections (); + +#if 0 + stripable_connections.drop_connections (); +#endif +} + +void +CC121::map_recenable_state () +{ + /* special case for RecEnable because its status can change as a + * confluence of unrelated parameters: (a) session rec-enable state (b) + * rec-enabled tracks. So we don't add the button to the blinkers list, + * we just call this: + * + * * from the blink callback + * * when the session tells us about a status change + * + * We do the last one so that the button changes state promptly rather + * than waiting for the next blink callback. The change in "blinking" + * based on having record-enabled tracks isn't urgent, and that happens + * during the blink callback. + */ + + bool onoff; + + switch (session->record_status()) { + case Session::Disabled: + onoff = false; + break; + case Session::Enabled: + onoff = blink_state; + break; + case Session::Recording: + if (session->have_rec_enabled_track ()) { + onoff = true; + } else { + onoff = blink_state; + } + break; + } + + if (onoff != rec_enable_state) { + get_button(RecEnable).set_led_state (_output_port, onoff); + rec_enable_state = onoff; + } +} + +void +CC121::map_transport_state () +{ + get_button (Loop).set_led_state (_output_port, session->get_play_loop()); + + float ts = session->transport_speed(); + + if (ts == 0) { + stop_blinking (Play); + } else if (fabs (ts) == 1.0) { + stop_blinking (Play); + get_button (Play).set_led_state (_output_port, true); + } else { + start_blinking (Play); + } + + get_button (Stop).set_led_state (_output_port, session->transport_stopped ()); + get_button (Rewind).set_led_state (_output_port, session->transport_speed() < 0.0); + get_button (Ffwd).set_led_state (_output_port, session->transport_speed() > 1.0); + get_button (Jog).set_led_state (_output_port, _jogmode == scroll); +} + +void +CC121::connect_session_signals() +{ + session->RecordStateChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_recenable_state, this), this); + session->TransportStateChange.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_transport_state, this), this); +} + +bool +CC121::midi_input_handler (Glib::IOCondition ioc, boost::shared_ptr port) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose ("something happend on %1\n", boost::shared_ptr(port)->name())); + + if (ioc & ~IO_IN) { + return false; + } + + if (ioc & IO_IN) { + + port->clear (); + DEBUG_TRACE (DEBUG::CC121, string_compose ("data available on %1\n", boost::shared_ptr(port)->name())); + framepos_t now = session->engine().sample_time(); + port->parse (now); + } + + return true; +} + + +XMLNode& +CC121::get_state () +{ + XMLNode& node (ControlProtocol::get_state()); + + XMLNode* child; + + child = new XMLNode (X_("Input")); + child->add_child_nocopy (boost::shared_ptr(_input_port)->get_state()); + node.add_child_nocopy (*child); + + + child = new XMLNode (X_("Output")); + child->add_child_nocopy (boost::shared_ptr(_output_port)->get_state()); + node.add_child_nocopy (*child); + + /* Save action state for Function1..4, Lock, Value, EQnEnable, EQType, + * AllBypass and Footswitch buttons, since these + * are user controlled. We can only save named-action operations, since + * internal functions are just pointers to functions and hard to + * serialize without enumerating them all somewhere. + */ + + node.add_child_nocopy (get_button (Function1).get_state()); + node.add_child_nocopy (get_button (Function2).get_state()); + node.add_child_nocopy (get_button (Function3).get_state()); + node.add_child_nocopy (get_button (Function4).get_state()); + node.add_child_nocopy (get_button (Value).get_state()); + node.add_child_nocopy (get_button (Lock).get_state()); + node.add_child_nocopy (get_button (EQ1Enable).get_state()); + node.add_child_nocopy (get_button (EQ2Enable).get_state()); + node.add_child_nocopy (get_button (EQ3Enable).get_state()); + node.add_child_nocopy (get_button (EQ4Enable).get_state()); + node.add_child_nocopy (get_button (EQType).get_state()); + node.add_child_nocopy (get_button (AllBypass).get_state()); + node.add_child_nocopy (get_button (Footswitch).get_state()); + + return node; +} + +int +CC121::set_state (const XMLNode& node, int version) +{ + XMLNodeList nlist; + XMLNodeConstIterator niter; + XMLNode const* child; + + if (ControlProtocol::set_state (node, version)) { + return -1; + } + + if ((child = node.child (X_("Input"))) != 0) { + XMLNode* portnode = child->child (Port::state_node_name.c_str()); + if (portnode) { + boost::shared_ptr(_input_port)->set_state (*portnode, version); + } + } + + if ((child = node.child (X_("Output"))) != 0) { + XMLNode* portnode = child->child (Port::state_node_name.c_str()); + if (portnode) { + boost::shared_ptr(_output_port)->set_state (*portnode, version); + } + } + + for (XMLNodeList::const_iterator n = node.children().begin(); n != node.children().end(); ++n) { + if ((*n)->name() == X_("Button")) { + XMLProperty const * prop = (*n)->property (X_("id")); + if (!prop) { + continue; + } + int xid = atoi (prop->value()); + ButtonMap::iterator b = buttons.find (ButtonID (xid)); + if (b == buttons.end()) { + continue; + } + b->second.set_state (**n); + } + } + + return 0; +} + +bool +CC121::connection_handler (boost::weak_ptr, std::string name1, boost::weak_ptr, std::string name2, bool yn) +{ + DEBUG_TRACE (DEBUG::CC121, "CC121::connection_handler start\n"); + if (!_input_port || !_output_port) { + return false; + } + + string ni = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (boost::shared_ptr(_input_port)->name()); + string no = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (boost::shared_ptr(_output_port)->name()); + + if (ni == name1 || ni == name2) { + if (yn) { + connection_state |= InputConnected; + } else { + connection_state &= ~InputConnected; + } + } else if (no == name1 || no == name2) { + if (yn) { + connection_state |= OutputConnected; + } else { + connection_state &= ~OutputConnected; + } + } else { + DEBUG_TRACE (DEBUG::CC121, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2)); + /* not our ports */ + return false; + } + + if ((connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) { + + /* XXX this is a horrible hack. Without a short sleep here, + something prevents the device wakeup messages from being + sent and/or the responses from being received. + */ + + g_usleep (100000); + DEBUG_TRACE (DEBUG::CC121, "device now connected for both input and output\n"); + connected (); + + } else { + DEBUG_TRACE (DEBUG::CC121, "Device disconnected (input or output or both) or not yet fully connected\n"); + _device_active = false; + } + + ConnectionChange (); /* emit signal for our GUI */ + + DEBUG_TRACE (DEBUG::CC121, "CC121::connection_handler end\n"); + + return true; /* connection status changed */ +} + +void +CC121::connected () +{ + DEBUG_TRACE (DEBUG::CC121, "connected"); + + _device_active = true; + + start_midi_handling (); + + all_lights_out (); + + /* catch up on state */ + + /* make sure that rec_enable_state is consistent with current device state */ + get_button (RecEnable).set_led_state (_output_port, rec_enable_state); + + map_transport_state (); + map_recenable_state (); +} + +void +CC121::Button::invoke (CC121::ButtonState bs, bool press) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose ("invoke button %1 for %2 state %3%4%5\n", id, (press ? "press":"release"), hex, bs, dec)); + + ToDoMap::iterator x; + + if (press) { + if ((x = on_press.find (bs)) == on_press.end()) { + DEBUG_TRACE (DEBUG::CC121, string_compose ("no press action for button %1 state %2 @ %3 in %4\n", id, bs, this, &on_press)); + return; + } + } else { + if ((x = on_release.find (bs)) == on_release.end()) { + DEBUG_TRACE (DEBUG::CC121, string_compose ("no release action for button %1 state %2 @%3 in %4\n", id, bs, this, &on_release)); + return; + } + } + + switch (x->second.type) { + case NamedAction: + if (!x->second.action_name.empty()) { + fp.access_action (x->second.action_name); + } + break; + case InternalFunction: + if (x->second.function) { + x->second.function (); + } + } +} + +void +CC121::Button::set_action (string const& name, bool when_pressed, CC121::ButtonState bs) +{ + ToDo todo; + + todo.type = NamedAction; + + if (when_pressed) { + if (name.empty()) { + on_press.erase (bs); + } else { + DEBUG_TRACE (DEBUG::CC121, string_compose ("set button %1 to action %2 on press + %3%4%5\n", id, name, bs)); + todo.action_name = name; + on_press[bs] = todo; + } + } else { + if (name.empty()) { + on_release.erase (bs); + } else { + DEBUG_TRACE (DEBUG::CC121, string_compose ("set button %1 to action %2 on release + %3%4%5\n", id, name, bs)); + todo.action_name = name; + on_release[bs] = todo; + } + } +} + +string +CC121::Button::get_action (bool press, CC121::ButtonState bs) +{ + ToDoMap::iterator x; + + if (press) { + if ((x = on_press.find (bs)) == on_press.end()) { + return string(); + } + if (x->second.type != NamedAction) { + return string (); + } + return x->second.action_name; + } else { + if ((x = on_release.find (bs)) == on_release.end()) { + return string(); + } + if (x->second.type != NamedAction) { + return string (); + } + return x->second.action_name; + } +} + +void +CC121::Button::set_action (boost::function f, bool when_pressed, CC121::ButtonState bs) +{ + ToDo todo; + todo.type = InternalFunction; + + if (when_pressed) { + DEBUG_TRACE (DEBUG::CC121, string_compose ("set button %1 (%2) @ %5 to some functor on press + %3 in %4\n", id, name, bs, &on_press, this)); + todo.function = f; + on_press[bs] = todo; + } else { + DEBUG_TRACE (DEBUG::CC121, string_compose ("set button %1 (%2) @ %5 to some functor on release + %3\n", id, name, bs, this)); + todo.function = f; + on_release[bs] = todo; + } +} + +void +CC121::Button::set_led_state (boost::shared_ptr port, bool onoff) +{ + MIDI::byte buf[3]; + DEBUG_TRACE(DEBUG::CC121, "Set Led State\n"); + buf[0] = 0x90; + buf[1] = id; + buf[2] = (onoff ? 0x7F:0x00); + port->write (buf, 3, 0); +} + +int +CC121::Button::set_state (XMLNode const& node) +{ + const XMLProperty* prop = node.property ("id"); + if (!prop) { + return -1; + } + + int xid = atoi (prop->value()); + if (xid != id) { + return -1; + } + + typedef pair state_pair_t; + vector state_pairs; + + state_pairs.push_back (make_pair (string ("plain"), ButtonState (0))); + + for (vector::const_iterator sp = state_pairs.begin(); sp != state_pairs.end(); ++sp) { + string propname; + + propname = sp->first + X_("-press"); + if ((prop = node.property (propname)) != 0) { + set_action (prop->value(), true, sp->second); + } + + propname = sp->first + X_("-release"); + if ((prop = node.property (propname)) != 0) { + set_action (prop->value(), false, sp->second); + } + } + + return 0; +} + +XMLNode& +CC121::Button::get_state () const +{ + XMLNode* node = new XMLNode (X_("Button")); + char buf[16]; + snprintf (buf, sizeof (buf), "%d", id); + + node->add_property (X_("id"), buf); + + ToDoMap::const_iterator x; + ToDo null; + null.type = NamedAction; + + typedef pair state_pair_t; + vector state_pairs; + + state_pairs.push_back (make_pair (string ("plain"), ButtonState (0))); + + for (vector::const_iterator sp = state_pairs.begin(); sp != state_pairs.end(); ++sp) { + if ((x = on_press.find (sp->second)) != on_press.end()) { + if (x->second.type == NamedAction) { + node->add_property (string (sp->first + X_("-press")).c_str(), x->second.action_name); + } + } + + if ((x = on_release.find (sp->second)) != on_release.end()) { + if (x->second.type == NamedAction) { + node->add_property (string (sp->first + X_("-release")).c_str(), x->second.action_name); + } + } + } + + return *node; +} + +void +CC121::gui_track_selection_changed (StripableNotificationListPtr stripables) +{ + boost::shared_ptr r; + + if (!stripables->empty()) { + r = stripables->front().lock(); + } + + set_current_stripable (r); +} + +void +CC121::drop_current_stripable () +{ + if (_current_stripable) { + if (_current_stripable == session->monitor_out()) { + set_current_stripable (session->master_out()); + } else { + set_current_stripable (boost::shared_ptr()); + } + } +} + +void +CC121::set_current_stripable (boost::shared_ptr r) +{ + stripable_connections.drop_connections (); + + _current_stripable = r; + + if (_current_stripable) { + _current_stripable->DropReferences.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::drop_current_stripable, this), this); + + _current_stripable->mute_control()->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_mute, this), this); + _current_stripable->solo_control()->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_solo, this), this); + + boost::shared_ptr t = boost::dynamic_pointer_cast (_current_stripable); + if (t) { + t->rec_enable_control()->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_recenable, this), this); + } + + boost::shared_ptr control = _current_stripable->gain_control (); + if (control) { + control->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_gain, this), this); + control->alist()->automation_state_changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_auto, this), this); + } + + boost::shared_ptr mp = _current_stripable->monitor_control(); + if (mp) { + mp->cut_control()->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_cut, this), this); + } + } + + //ToDo: subscribe to the fader automation modes so we can light the LEDs + + map_stripable_state (); +} + +void +CC121::map_auto () +{ + boost::shared_ptr control = _current_stripable->gain_control (); + const AutoState as = control->automation_state (); + + switch (as) { + case ARDOUR::Play: + get_button (FP_Read).set_led_state (_output_port, true); + get_button (FP_Write).set_led_state (_output_port, false); + get_button (EButton).set_led_state (_output_port, false); + get_button (OpenVST).set_led_state (_output_port, false); + break; + case ARDOUR::Write: + get_button (FP_Read).set_led_state (_output_port, false); + get_button (FP_Write).set_led_state (_output_port, true); + get_button (EButton).set_led_state (_output_port, false); + get_button (OpenVST).set_led_state (_output_port, false); + break; + case ARDOUR::Touch: + get_button (EButton).set_led_state (_output_port, true); + get_button (FP_Read).set_led_state (_output_port, false); + get_button (FP_Write).set_led_state(_output_port, false); + get_button (OpenVST).set_led_state (_output_port, false); + break; + case ARDOUR::Off: + get_button (OpenVST).set_led_state (_output_port, true); + get_button (FP_Read).set_led_state (_output_port, false); + get_button (FP_Write).set_led_state (_output_port, false); + get_button (EButton).set_led_state (_output_port, false); + break; + } +} + +void +CC121::map_cut () +{ + boost::shared_ptr mp = _current_stripable->monitor_control(); + + if (mp) { + bool yn = mp->cut_all (); + if (yn) { + start_blinking (Mute); + } else { + stop_blinking (Mute); + } + } else { + stop_blinking (Mute); + } +} + +void +CC121::map_mute () +{ + if (_current_stripable) { + if (_current_stripable->mute_control()->muted()) { + stop_blinking (Mute); + get_button (Mute).set_led_state (_output_port, true); + } else if (_current_stripable->mute_control()->muted_by_others_soloing () || _current_stripable->mute_control()->muted_by_masters()) { + start_blinking (Mute); + } else { + stop_blinking (Mute); + } + } else { + stop_blinking (Mute); + } +} + +void +CC121::map_solo () +{ + if (_current_stripable) { + get_button (Solo).set_led_state (_output_port, _current_stripable->solo_control()->soloed()); + } else { + get_button (Solo).set_led_state (_output_port, false); + } +} + +void +CC121::map_recenable () +{ + boost::shared_ptr t = boost::dynamic_pointer_cast (_current_stripable); + if (t) { + get_button (Rec).set_led_state (_output_port, t->rec_enable_control()->get_value()); + } else { + get_button (Rec).set_led_state (_output_port, false); + } +} + +void +CC121::map_gain () +{ + if (fader_is_touched) { + /* Do not send fader moves while the user is touching the fader */ + return; + } + + if (!_current_stripable) { + return; + } + + boost::shared_ptr control = _current_stripable->gain_control (); + double val; + + if (!control) { + val = 0.0; + } else { + val = control->internal_to_interface (control->get_value ()); + } + + float fval = (val* 16384.0); + if (fval <0.0) + fval = 0.0; + else if (fval > 16383.0) + fval = 16383.0; + int ival = (int)(fval + 0.5); + + MIDI::byte buf[3]; + + buf[0] = 0xE0; + buf[1] = ival & 0x7F; + buf[2] = (ival >> 7) & 0x7F; + + _output_port->write (buf, 3, 0); +} + +void +CC121::map_stripable_state () +{ + if (!_current_stripable) { + stop_blinking (Mute); + stop_blinking (Solo); + get_button (Rec).set_led_state (_output_port, false); + } else { + map_solo (); + map_recenable (); + map_gain (); + map_auto (); + + if (_current_stripable == session->monitor_out()) { + map_cut (); + } else { + map_mute (); + } + } +} + +list > +CC121::bundles () +{ + list > b; + + if (_input_bundle) { + b.push_back (_input_bundle); + b.push_back (_output_bundle); + } + + return b; +} + +boost::shared_ptr +CC121::output_port() +{ + return _output_port; +} + +boost::shared_ptr +CC121::input_port() +{ + return _input_port; +} + +void +CC121::set_action (ButtonID id, std::string const& action_name, bool on_press, ButtonState bs) +{ + get_button(id).set_action (action_name, on_press, bs); +} + +string +CC121::get_action (ButtonID id, bool press, ButtonState bs) +{ + return get_button(id).get_action (press, bs); +} diff --git a/libs/surfaces/cc121/cc121.h b/libs/surfaces/cc121/cc121.h new file mode 100644 index 0000000000..a5f0363a22 --- /dev/null +++ b/libs/surfaces/cc121/cc121.h @@ -0,0 +1,342 @@ +/* + Copyright (C) 2006 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + Thanks to Rolf Meyerhoff for reverse engineering the CC121 protocol. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef ardour_surface_cc121_h +#define ardour_surface_cc121_h + +#include +#include +#include +#include + +#define ABSTRACT_UI_EXPORTS +#include "pbd/abstract_ui.h" + +#include "ardour/types.h" + +#include "control_protocol/control_protocol.h" + +namespace PBD { + class Controllable; + class ControllableDescriptor; +} + +#include + +//#include "pbd/signals.h" + + +//#include "midi_byte_array.h" +#include "types.h" + +#include "glibmm/main.h" + +namespace MIDI { + class Parser; + class Port; +} + + +namespace ARDOUR { + class AsyncMIDIPort; + class Bundle; + class Port; + class Session; + class MidiPort; +} + + +class MIDIControllable; +class MIDIFunction; +class MIDIAction; + +namespace ArdourSurface { + +struct CC121Request : public BaseUI::BaseRequestObject { +public: + CC121Request () {} + ~CC121Request () {} +}; + +class CC121 : public ARDOUR::ControlProtocol, public AbstractUI { + public: + CC121 (ARDOUR::Session&); + virtual ~CC121(); + + int set_active (bool yn); + + /* we probe for a device when our ports are connected. Before that, + there's no way to know if the device exists or not. + */ + static bool probe() { return true; } + static void* request_factory (uint32_t); + + XMLNode& get_state (); + int set_state (const XMLNode&, int version); + + bool has_editor () const { return true; } + void* get_gui () const; + void tear_down_gui (); + + + /* Note: because the CC121 speaks an inherently duplex protocol, + we do not implement get/set_feedback() since this aspect of + support for the protocol is not optional. + */ + + void do_request (CC121Request*); + int stop (); + + void thread_init (); + + PBD::Signal0 ConnectionChange; + + boost::shared_ptr input_port(); + boost::shared_ptr output_port(); + + enum ButtonID { + Rec = 0x00, + Solo = 0x08, + Mute = 0x10, + Left = 0x30, + Right = 0x31, + EButton = 0x33, + Function1 = 0x36, + Function2 = 0x37, + Function3 = 0x38, + Function4 = 0x39, + Value = 0x3A, + Footswitch = 0x3B, + FP_Read = 0x4A, + FP_Write = 0x4B, + Loop = 0x56, + ToStart = 0x58, + ToEnd = 0x5A, + Rewind = 0x5B, + Ffwd = 0x5C, + Stop = 0x5D, + Play = 0x5E, + RecEnable = 0x5F, + FaderTouch = 0x68, + EQ1Enable = 0x70, + EQ2Enable = 0x71, + EQ3Enable = 0x72, + EQ4Enable = 0x73, + EQType = 0x74, + AllBypass = 0x75, + Jog = 0x76, + Lock = 0x77, + InputMonitor = 0x78, + OpenVST = 0x79, + Output = 22 + }; + + enum ButtonState { + ShiftDown = 0x1, + RewindDown = 0x2, + StopDown = 0x4, + UserDown = 0x8, + LongPress = 0x10 + }; + + void set_action (ButtonID, std::string const& action_name, bool on_press, CC121::ButtonState = ButtonState (0)); + std::string get_action (ButtonID, bool on_press, CC121::ButtonState = ButtonState (0)); + + std::list > bundles (); + + private: + boost::shared_ptr _current_stripable; + boost::weak_ptr pre_master_stripable; + boost::weak_ptr pre_monitor_stripable; + + boost::shared_ptr _input_port; + boost::shared_ptr _output_port; + + // Bundle to represent our input ports + boost::shared_ptr _input_bundle; + // Bundle to represent our output ports + boost::shared_ptr _output_bundle; + + PBD::ScopedConnectionList midi_connections; + + bool midi_input_handler (Glib::IOCondition ioc, boost::shared_ptr port); + + mutable void *gui; + void build_gui (); + + bool connection_handler (boost::weak_ptr, std::string name1, boost::weak_ptr, std::string name2, bool yn); + PBD::ScopedConnection port_connection; + + enum ConnectionState { + InputConnected = 0x1, + OutputConnected = 0x2 + }; + + int connection_state; + void connected (); + bool _device_active; + int fader_msb; + int fader_lsb; + bool fader_is_touched; + enum JogMode { scroll=1, zoom=2 }; + JogMode _jogmode; + + ARDOUR::microseconds_t last_encoder_time; + int last_good_encoder_delta; + int last_encoder_delta, last_last_encoder_delta; + + void button_press_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb); + void button_release_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb); + void fader_handler (MIDI::Parser &, MIDI::pitchbend_t pb); + void encoder_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb); + /* void fader_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb);*/ + + ButtonState button_state; + + friend class Button; + + class Button { + public: + + enum ActionType { + NamedAction, + InternalFunction, + }; + + Button (CC121& f, std::string const& str, ButtonID i) + : fp (f) + , name (str) + , id (i) + , flash (false) + {} + + void set_action (std::string const& action_name, bool on_press, CC121::ButtonState = ButtonState (0)); + void set_action (boost::function function, bool on_press, CC121::ButtonState = ButtonState (0)); + std::string get_action (bool press, CC121::ButtonState bs = ButtonState (0)); + + void set_led_state (boost::shared_ptr, bool onoff); + void invoke (ButtonState bs, bool press); + bool uses_flash () const { return flash; } + void set_flash (bool yn) { flash = yn; } + + XMLNode& get_state () const; + int set_state (XMLNode const&); + + sigc::connection timeout_connection; + + private: + CC121& fp; + std::string name; + ButtonID id; + bool flash; + + struct ToDo { + ActionType type; + /* could be a union if boost::function didn't require a + * constructor + */ + std::string action_name; + boost::function function; + }; + + typedef std::map ToDoMap; + ToDoMap on_press; + ToDoMap on_release; + }; + + typedef std::map ButtonMap; + + ButtonMap buttons; + Button& get_button (ButtonID) const; + + std::set buttons_down; + std::set consumed; + + void all_lights_out (); + void close (); + void start_midi_handling (); + void stop_midi_handling (); + + PBD::ScopedConnectionList session_connections; + void connect_session_signals (); + void map_recenable_state (); + void map_transport_state (); + + sigc::connection periodic_connection; + bool periodic (); + + sigc::connection heartbeat_connection; + sigc::connection blink_connection; + typedef std::list Blinkers; + Blinkers blinkers; + bool blink_state; + bool blink (); + bool beat (); + void start_blinking (ButtonID); + void stop_blinking (ButtonID); + + void set_current_stripable (boost::shared_ptr); + void drop_current_stripable (); + void use_master (); + void use_monitor (); + void gui_track_selection_changed (ARDOUR::StripableNotificationListPtr); + PBD::ScopedConnection selection_connection; + PBD::ScopedConnectionList stripable_connections; + + void map_stripable_state (); + void map_solo (); + void map_mute (); + bool rec_enable_state; + void map_recenable (); + void map_gain (); + void map_cut (); + void map_auto (); + + /* operations (defined in operations.cc) */ + + void read (); + void write (); + + void input_monitor (); + void left (); + void right (); + + void touch (); + void off (); + + void undo (); + void redo (); + void solo (); + void mute (); + void jog (); + void rec_enable (); + + void ardour_pan_azimuth (float); + void ardour_pan_width (float); + void mixbus_pan (float); + + void punch (); +}; + +} + +#endif /* ardour_surface_cc121_h */ diff --git a/libs/surfaces/cc121/cc121_interface.cc b/libs/surfaces/cc121/cc121_interface.cc new file mode 100644 index 0000000000..7483b39d35 --- /dev/null +++ b/libs/surfaces/cc121/cc121_interface.cc @@ -0,0 +1,80 @@ +/* + Copyright (C) 2012 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include + +#include "control_protocol/control_protocol.h" +#include "cc121.h" + +using namespace ARDOUR; +using namespace ArdourSurface; + +static ControlProtocol* +new_cc121_midi_protocol (ControlProtocolDescriptor* /*descriptor*/, Session* s) +{ + CC121* fp; + + try { + fp = new CC121 (*s); + } catch (failed_constructor& err) { + return 0; + } + + if (fp->set_active (true)) { + delete fp; + return 0; + } + + return fp; +} + +static void +delete_cc121_midi_protocol (ControlProtocolDescriptor* /*descriptor*/, ControlProtocol* cp) +{ + delete cp; +} + +static bool +probe_cc121_midi_protocol (ControlProtocolDescriptor* /*descriptor*/) +{ + return CC121::probe (); +} + +static void* +cc121_request_buffer_factory (uint32_t num_requests) +{ + return CC121::request_factory (num_requests); +} + +static ControlProtocolDescriptor cc121_midi_descriptor = { + /*name : */ "Steinberg CC121", + /*id : */ "uri://ardour.org/surfaces/cc121:0", + /*ptr : */ 0, + /*module : */ 0, + /*mandatory : */ 0, + /*supports_feedback : */ true, + /*probe : */ probe_cc121_midi_protocol, + /*initialize : */ new_cc121_midi_protocol, + /*destroy : */ delete_cc121_midi_protocol, + /*request_buffer_factory */ cc121_request_buffer_factory +}; + +extern "C" ARDOURSURFACE_API ControlProtocolDescriptor* protocol_descriptor () { return &cc121_midi_descriptor; } + diff --git a/libs/surfaces/cc121/gui.cc b/libs/surfaces/cc121/gui.cc new file mode 100644 index 0000000000..ca06887102 --- /dev/null +++ b/libs/surfaces/cc121/gui.cc @@ -0,0 +1,629 @@ +/* + Copyright (C) 2015 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include +#include +#include + +#include "pbd/unwind.h" +#include "pbd/strsplit.h" +#include "pbd/file_utils.h" + +#include "gtkmm2ext/bindings.h" +#include "gtkmm2ext/gtk_ui.h" +#include "gtkmm2ext/gui_thread.h" +#include "gtkmm2ext/utils.h" + +#include "ardour/audioengine.h" +#include "ardour/filesystem_paths.h" + +#include "cc121.h" +#include "gui.h" + +#include "pbd/i18n.h" + +using namespace PBD; +using namespace ARDOUR; +using namespace ArdourSurface; +using namespace std; +using namespace Gtk; +using namespace Gtkmm2ext; + +void* +CC121::get_gui () const +{ + if (!gui) { + const_cast(this)->build_gui (); + } + static_cast(gui)->show_all(); + return gui; +} + +void +CC121::tear_down_gui () +{ + if (gui) { + Gtk::Widget *w = static_cast(gui)->get_parent(); + if (w) { + w->hide(); + delete w; + } + } + delete static_cast (gui); + gui = 0; +} + +void +CC121::build_gui () +{ + gui = (void*) new CC121GUI (*this); +} + +/*--------------------*/ + +CC121GUI::CC121GUI (CC121& p) + : fp (p) + , table (2, 5) + , action_table (5, 4) + , ignore_active_change (false) +{ + set_border_width (12); + + table.set_row_spacings (4); + table.set_col_spacings (6); + table.set_border_width (12); + table.set_homogeneous (false); + + std::string data_file_path; + string name = "cc121.png"; + Searchpath spath(ARDOUR::ardour_data_search_path()); + spath.add_subdirectory_to_paths ("icons"); + find_file (spath, name, data_file_path); + if (!data_file_path.empty()) { + image.set (data_file_path); + hpacker.pack_start (image, false, false); + } + + Gtk::Label* l; + Gtk::Alignment* align; + int row = 0; + int action_row = 1; + + input_combo.pack_start (midi_port_columns.short_name); + output_combo.pack_start (midi_port_columns.short_name); + + input_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &CC121GUI::active_port_changed), &input_combo, true)); + output_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &CC121GUI::active_port_changed), &output_combo, false)); + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Incoming MIDI on:"))); + l->set_alignment (1.0, 0.5); + table.attach (*l, 0, 1, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0)); + table.attach (input_combo, 1, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0), 0, 0); + row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Outgoing MIDI on:"))); + l->set_alignment (1.0, 0.5); + table.attach (*l, 0, 1, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0)); + table.attach (output_combo, 1, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0), 0, 0); + row++; + + build_available_action_menu (); + + build_user_action_combo (function1_combo, CC121::ButtonState(0), CC121::Function1); + build_user_action_combo (function2_combo, CC121::ButtonState(0), CC121::Function2); + build_user_action_combo (function3_combo, CC121::ButtonState(0), CC121::Function3); + build_user_action_combo (function4_combo, CC121::ButtonState(0), CC121::Function4); + build_user_action_combo (value_combo, CC121::ButtonState(0), CC121::Value); + build_user_action_combo (lock_combo, CC121::ButtonState(0), CC121::Lock); + build_user_action_combo (eq1_combo, CC121::ButtonState(0), CC121::EQ1Enable); + build_user_action_combo (eq2_combo, CC121::ButtonState(0), CC121::EQ2Enable); + build_user_action_combo (eq3_combo, CC121::ButtonState(0), CC121::EQ3Enable); + build_user_action_combo (eq4_combo, CC121::ButtonState(0), CC121::EQ4Enable); + build_user_action_combo (eqtype_combo, CC121::ButtonState(0), CC121::EQType); + build_user_action_combo (allbypass_combo, CC121::ButtonState(0), CC121::AllBypass); + build_foot_action_combo (foot_combo, CC121::ButtonState(0)); + action_table.set_row_spacings (4); + action_table.set_col_spacings (6); + action_table.set_border_width (12); + action_table.set_homogeneous (false); + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Function 1"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (function1_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Function 2"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (function2_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Function 3"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (function3_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Function 4"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (function4_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Value"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (value_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Lock"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (lock_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQ1"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eq1_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQ2"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eq2_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQ3"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eq3_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQ4"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eq4_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQType"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eqtype_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("AllBypass"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (allbypass_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Footswitch"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (foot_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + table.attach (action_table, 0, 5, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + row++; + + hpacker.pack_start (table, true, true); + pack_start (hpacker, false, false); + + /* update the port connection combos */ + + update_port_combos (); + + /* catch future changes to connection state */ + + fp.ConnectionChange.connect (connection_change_connection, invalidator (*this), boost::bind (&CC121GUI::connection_handler, this), gui_context()); +} + +CC121GUI::~CC121GUI () +{ +} + +void +CC121GUI::connection_handler () +{ + /* ignore all changes to combobox active strings here, because we're + updating them to match a new ("external") reality - we were called + because port connections have changed. + */ + + PBD::Unwinder ici (ignore_active_change, true); + + update_port_combos (); +} + +void +CC121GUI::update_port_combos () +{ + vector midi_inputs; + vector midi_outputs; + + ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::MIDI, ARDOUR::PortFlags (ARDOUR::IsOutput|ARDOUR::IsTerminal), midi_inputs); + ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::MIDI, ARDOUR::PortFlags (ARDOUR::IsInput|ARDOUR::IsTerminal), midi_outputs); + + Glib::RefPtr input = build_midi_port_list (midi_inputs, true); + Glib::RefPtr output = build_midi_port_list (midi_outputs, false); + bool input_found = false; + bool output_found = false; + int n; + + input_combo.set_model (input); + output_combo.set_model (output); + + Gtk::TreeModel::Children children = input->children(); + Gtk::TreeModel::Children::iterator i; + i = children.begin(); + ++i; /* skip "Disconnected" */ + + + for (n = 1; i != children.end(); ++i, ++n) { + string port_name = (*i)[midi_port_columns.full_name]; + if (fp.input_port()->connected_to (port_name)) { + input_combo.set_active (n); + input_found = true; + break; + } + } + + if (!input_found) { + input_combo.set_active (0); /* disconnected */ + } + + children = output->children(); + i = children.begin(); + ++i; /* skip "Disconnected" */ + + for (n = 1; i != children.end(); ++i, ++n) { + string port_name = (*i)[midi_port_columns.full_name]; + if (fp.output_port()->connected_to (port_name)) { + output_combo.set_active (n); + output_found = true; + break; + } + } + + if (!output_found) { + output_combo.set_active (0); /* disconnected */ + } +} + +void +CC121GUI::build_available_action_menu () +{ + /* build a model of all available actions (needs to be tree structured + * more) + */ + + available_action_model = TreeStore::create (action_columns); + + vector paths; + vector labels; + vector tooltips; + vector keys; + vector > actions; + + Gtkmm2ext::ActionMap::get_all_actions (paths, labels, tooltips, keys, actions); + + typedef std::map NodeMap; + NodeMap nodes; + NodeMap::iterator r; + + + vector::iterator k; + vector::iterator p; + vector::iterator t; + vector::iterator l; + + available_action_model->clear (); + + TreeIter rowp; + TreeModel::Row parent; + + /* Disabled item (row 0) */ + + rowp = available_action_model->append(); + parent = *(rowp); + parent[action_columns.name] = _("Disabled"); + + for (l = labels.begin(), k = keys.begin(), p = paths.begin(), t = tooltips.begin(); l != labels.end(); ++k, ++p, ++t, ++l) { + + TreeModel::Row row; + vector parts; + + parts.clear (); + + split (*p, parts, '/'); + + if (parts.empty()) { + continue; + } + + //kinda kludgy way to avoid displaying menu items as mappable + if ( parts[1] == _("Main_menu") ) + continue; + if ( parts[1] == _("JACK") ) + continue; + if ( parts[1] == _("redirectmenu") ) + continue; + if ( parts[1] == _("Editor_menus") ) + continue; + if ( parts[1] == _("RegionList") ) + continue; + if ( parts[1] == _("ProcessorMenu") ) + continue; + + if ((r = nodes.find (parts[1])) == nodes.end()) { + + /* top level is missing */ + + TreeIter rowp; + TreeModel::Row parent; + rowp = available_action_model->append(); + nodes[parts[1]] = rowp; + parent = *(rowp); + parent[action_columns.name] = parts[1]; + + row = *(available_action_model->append (parent.children())); + + } else { + + row = *(available_action_model->append ((*r->second)->children())); + + } + + /* add this action */ + + if (l->empty ()) { + row[action_columns.name] = *t; + action_map[*t] = *p; + } else { + row[action_columns.name] = *l; + action_map[*l] = *p; + } + + string path = (*p); + /* ControlProtocol::access_action() is not interested in the + legacy "/" prefix part of a path. + */ + path = path.substr (strlen ("/")); + + row[action_columns.path] = path; + } +} + +void +CC121GUI::action_changed (Gtk::ComboBox* cb, CC121::ButtonID id, CC121::ButtonState bs) +{ + TreeModel::const_iterator row = cb->get_active (); + string action_path = (*row)[action_columns.path]; + + /* release binding */ + fp.set_action (id, action_path, false, bs); +} + +void +CC121GUI::build_action_combo (Gtk::ComboBox& cb, vector > const & actions, CC121::ButtonID id, CC121::ButtonState bs) +{ + Glib::RefPtr model (Gtk::ListStore::create (action_columns)); + TreeIter rowp; + TreeModel::Row row; + string current_action = fp.get_action (id, false, bs); /* lookup release action */ + int active_row = -1; + int n; + vector >::const_iterator i; + + rowp = model->append(); + row = *(rowp); + row[action_columns.name] = _("Disabled"); + row[action_columns.path] = string(); + + if (current_action.empty()) { + active_row = 0; + } + + for (i = actions.begin(), n = 0; i != actions.end(); ++i, ++n) { + rowp = model->append(); + row = *(rowp); + row[action_columns.name] = i->first; + row[action_columns.path] = i->second; + if (current_action == i->second) { + active_row = n+1; + } + } + + cb.set_model (model); + cb.pack_start (action_columns.name); + + if (active_row >= 0) { + cb.set_active (active_row); + } + + cb.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &CC121GUI::action_changed), &cb, id, bs)); +} + +void +CC121GUI::build_foot_action_combo (Gtk::ComboBox& cb, CC121::ButtonState bs) +{ + vector > actions; + + actions.push_back (make_pair (string("Toggle Roll"), string(X_("Transport/ToggleRoll")))); + actions.push_back (make_pair (string("Toggle Rec-Enable"), string(X_("Transport/Record")))); + actions.push_back (make_pair (string("Toggle Roll+Rec"), string(X_("Transport/record-roll")))); + actions.push_back (make_pair (string("Toggle Loop"), string(X_("Transport/Loop")))); + actions.push_back (make_pair (string("Toggle Click"), string(X_("Transport/ToggleClick")))); + + build_action_combo (cb, actions, CC121::Footswitch, bs); +} + +bool +CC121GUI::find_action_in_model (const TreeModel::iterator& iter, std::string const & action_path, TreeModel::iterator* found) +{ + TreeModel::Row row = *iter; + string path = row[action_columns.path]; + + if (path == action_path) { + *found = iter; + return true; + } + + return false; +} + +void +CC121GUI::build_user_action_combo (Gtk::ComboBox& cb, CC121::ButtonState bs, CC121::ButtonID id) +{ + cb.set_model (available_action_model); + cb.pack_start (action_columns.name); + cb.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &CC121GUI::action_changed), &cb, id, bs)); + + /* set the active "row" to the right value for the current button binding */ + + string current_action = fp.get_action (id, false, bs); /* lookup release action */ + + if (current_action.empty()) { + cb.set_active (0); /* "disabled" */ + return; + } + + TreeModel::iterator iter = available_action_model->children().end(); + + available_action_model->foreach_iter (sigc::bind (sigc::mem_fun (*this, &CC121GUI::find_action_in_model), current_action, &iter)); + + if (iter != available_action_model->children().end()) { + cb.set_active (iter); + } else { + cb.set_active (0); + } +} + +Glib::RefPtr +CC121GUI::build_midi_port_list (vector const & ports, bool for_input) +{ + Glib::RefPtr store = ListStore::create (midi_port_columns); + TreeModel::Row row; + + row = *store->append (); + row[midi_port_columns.full_name] = string(); + row[midi_port_columns.short_name] = _("Disconnected"); + + for (vector::const_iterator p = ports.begin(); p != ports.end(); ++p) { + row = *store->append (); + row[midi_port_columns.full_name] = *p; + std::string pn = ARDOUR::AudioEngine::instance()->get_pretty_name_by_name (*p); + if (pn.empty ()) { + pn = (*p).substr ((*p).find (':') + 1); + } + row[midi_port_columns.short_name] = pn; + } + + return store; +} + +void +CC121GUI::active_port_changed (Gtk::ComboBox* combo, bool for_input) +{ + if (ignore_active_change) { + return; + } + + TreeModel::iterator active = combo->get_active (); + string new_port = (*active)[midi_port_columns.full_name]; + + if (new_port.empty()) { + if (for_input) { + fp.input_port()->disconnect_all (); + } else { + fp.output_port()->disconnect_all (); + } + + return; + } + + if (for_input) { + if (!fp.input_port()->connected_to (new_port)) { + fp.input_port()->disconnect_all (); + fp.input_port()->connect (new_port); + } + } else { + if (!fp.output_port()->connected_to (new_port)) { + fp.output_port()->disconnect_all (); + fp.output_port()->connect (new_port); + } + } +} diff --git a/libs/surfaces/cc121/gui.h b/libs/surfaces/cc121/gui.h new file mode 100644 index 0000000000..107a81ed32 --- /dev/null +++ b/libs/surfaces/cc121/gui.h @@ -0,0 +1,116 @@ +/* + Copyright (C) 2015 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef __ardour_cc121_gui_h__ +#define __ardour_cc121_gui_h__ + +#include +#include + +#include +#include +#include +#include +#include + +namespace Gtk { + class CellRendererCombo; + class ListStore; +} + +#include "cc121.h" + +namespace ArdourSurface { + +class CC121GUI : public Gtk::VBox +{ +public: + CC121GUI (CC121&); + ~CC121GUI (); + +private: + CC121& fp; + Gtk::HBox hpacker; + Gtk::Table table; + Gtk::Table action_table; + Gtk::ComboBox input_combo; + Gtk::ComboBox output_combo; + Gtk::Image image; + + Gtk::ComboBox foot_combo; + Gtk::ComboBox function1_combo; + Gtk::ComboBox function2_combo; + Gtk::ComboBox function3_combo; + Gtk::ComboBox function4_combo; + Gtk::ComboBox value_combo; + Gtk::ComboBox lock_combo; + Gtk::ComboBox eq1_combo; + Gtk::ComboBox eq2_combo; + Gtk::ComboBox eq3_combo; + Gtk::ComboBox eq4_combo; + Gtk::ComboBox eqtype_combo; + Gtk::ComboBox allbypass_combo; + + void update_port_combos (); + PBD::ScopedConnection connection_change_connection; + void connection_handler (); + + struct MidiPortColumns : public Gtk::TreeModel::ColumnRecord { + MidiPortColumns() { + add (short_name); + add (full_name); + } + Gtk::TreeModelColumn short_name; + Gtk::TreeModelColumn full_name; + }; + + MidiPortColumns midi_port_columns; + bool ignore_active_change; + + Glib::RefPtr build_midi_port_list (std::vector const & ports, bool for_input); + void active_port_changed (Gtk::ComboBox*,bool for_input); + + struct ActionColumns : public Gtk::TreeModel::ColumnRecord { + ActionColumns() { + add (name); + add (path); + } + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn path; + }; + + ActionColumns action_columns; + Glib::RefPtr available_action_model; + std::map action_map; // map from action names to paths + + void build_action_combo (Gtk::ComboBox& cb, std::vector > const & actions, CC121::ButtonID, CC121::ButtonState); + void build_user_action_combo (Gtk::ComboBox&, CC121::ButtonState, CC121::ButtonID); + void build_foot_action_combo (Gtk::ComboBox&, CC121::ButtonState); + + void build_available_action_menu (); + void action_changed (Gtk::ComboBox*, CC121::ButtonID, CC121::ButtonState); + + bool find_action_in_model (const Gtk::TreeModel::iterator& iter, std::string const & action_path, Gtk::TreeModel::iterator* found); + +}; + +} + +#endif /* __ardour_cc121_gui_h__ */ diff --git a/libs/surfaces/cc121/operations.cc b/libs/surfaces/cc121/operations.cc new file mode 100644 index 0000000000..67aafda777 --- /dev/null +++ b/libs/surfaces/cc121/operations.cc @@ -0,0 +1,340 @@ +/* + Copyright (C) 2015 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + Thanks to Rolf Meyerhoff for reverse engineering the CC121 protocol. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include "ardour/async_midi_port.h" +#include "ardour/monitor_processor.h" +#include "ardour/monitor_control.h" +#include "ardour/pannable.h" +#include "ardour/plugin_insert.h" +#include "ardour/rc_configuration.h" +#include "ardour/record_enable_control.h" +#include "ardour/session.h" +#include "ardour/track.h" +#include "ardour/types.h" + +#include "cc121.h" + +using namespace ARDOUR; +using namespace ArdourSurface; +using namespace PBD; + +/* this value is chosen to given smooth motion from 0..1.0 in about 270 degrees + * of encoder rotation. + */ +static const double encoder_divider = 24.0; + +void +CC121::input_monitor () +{ + if (_current_stripable) { + MonitorChoice choice = _current_stripable->monitoring_control()->monitoring_choice (); + switch(choice) { + case MonitorAuto: + _current_stripable->monitoring_control()->set_value (MonitorInput, PBD::Controllable::NoGroup); + get_button(InputMonitor).set_led_state (_output_port, true); + break; + case MonitorInput: + _current_stripable->monitoring_control()->set_value (MonitorDisk, PBD::Controllable::NoGroup); + get_button(InputMonitor).set_led_state (_output_port, false); + break; + case MonitorDisk: + _current_stripable->monitoring_control()->set_value (MonitorCue, PBD::Controllable::NoGroup); + get_button(InputMonitor).set_led_state (_output_port, false); + break; + case MonitorCue: + _current_stripable->monitoring_control()->set_value (MonitorInput, PBD::Controllable::NoGroup); + get_button(InputMonitor).set_led_state (_output_port, true); + break; + default: + break; + } + } +} + +void +CC121::left () +{ + access_action ("Editor/select-prev-route"); + + //ToDo: bank by 8? + //if ( (button_state & ShiftDown) == ShiftDown ) + +} + +void +CC121::right () +{ + access_action ("Editor/select-next-route"); + + //ToDo: bank by 8? + //if ( (button_state & ShiftDown) == ShiftDown ) +} + + +void +CC121::read () +{ + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + gain->set_automation_state( (ARDOUR::AutoState) ARDOUR::Play ); + } + } +} + +void +CC121::write () +{ + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + gain->set_automation_state( (ARDOUR::AutoState) ARDOUR::Write ); + } + } +} + +void +CC121::touch () +{ + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + gain->set_automation_state( (ARDOUR::AutoState) ARDOUR::Touch ); + } + } +} + +void +CC121::off () +{ + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + gain->set_automation_state( (ARDOUR::AutoState) ARDOUR::Off ); + } + } +} + + + + +void +CC121::undo () +{ + ControlProtocol::Undo (); /* EMIT SIGNAL */ +} + +void +CC121::redo () +{ + ControlProtocol::Redo (); /* EMIT SIGNAL */ +} + +void +CC121::jog() +{ + if (_jogmode == scroll) { + _jogmode = zoom; + } + else { + _jogmode = scroll; + } + get_button (Jog).set_led_state (_output_port, _jogmode == scroll); +} + +void +CC121::mute () +{ + if (!_current_stripable) { + return; + } + + if (_current_stripable == session->monitor_out()) { + boost::shared_ptr mp = _current_stripable->monitor_control(); + mp->set_cut_all (!mp->cut_all()); + return; + } + + _current_stripable->mute_control()->set_value (!_current_stripable->mute_control()->muted(), PBD::Controllable::UseGroup); +} + +void +CC121::solo () +{ + if (!_current_stripable) { + return; + } + _current_stripable->solo_control()->set_value (!_current_stripable->solo_control()->soloed(), PBD::Controllable::UseGroup); +} + +void +CC121::rec_enable () +{ + if (!_current_stripable) { + return; + } + + boost::shared_ptr t = boost::dynamic_pointer_cast(_current_stripable); + + if (!t) { + return; + } + + t->rec_enable_control()->set_value (!t->rec_enable_control()->get_value(), Controllable::UseGroup); +} + +void +CC121::use_master () +{ + boost::shared_ptr r = session->master_out(); + if (r) { + if (_current_stripable == r) { + r = pre_master_stripable.lock(); + set_current_stripable (r); + get_button(Output).set_led_state (_output_port, false); + blinkers.remove (Output); + } else { + if (_current_stripable != session->master_out() && _current_stripable != session->monitor_out()) { + pre_master_stripable = boost::weak_ptr (_current_stripable); + } + set_current_stripable (r); + get_button(Output).set_led_state (_output_port, true); + blinkers.remove (Output); + } + } +} + +void +CC121::use_monitor () +{ + boost::shared_ptr r = session->monitor_out(); + + if (r) { + if (_current_stripable == r) { + r = pre_monitor_stripable.lock(); + set_current_stripable (r); + get_button(Output).set_led_state (_output_port, false); + blinkers.remove (Output); + } else { + if (_current_stripable != session->master_out() && _current_stripable != session->monitor_out()) { + pre_monitor_stripable = boost::weak_ptr (_current_stripable); + } + set_current_stripable (r); + get_button(Output).set_led_state (_output_port, true); + blinkers.push_back (Output); + } + } +} + +void +CC121::ardour_pan_azimuth (float delta) +{ + if (!_current_stripable) { + return; + } + + boost::shared_ptr r = boost::dynamic_pointer_cast (_current_stripable); + + if (!r) { + return; + } + + boost::shared_ptr pannable = r->pannable (); + + if (!pannable) { + return; + } + + boost::shared_ptr azimuth = pannable->pan_azimuth_control; + + if (!azimuth) { + return; + } + + azimuth->set_value (azimuth->interface_to_internal (azimuth->internal_to_interface (azimuth->get_value()) + (delta)), Controllable::NoGroup); +} + + +void +CC121::ardour_pan_width(float delta) +{ + if (!_current_stripable) { + return; + } + + boost::shared_ptr r = boost::dynamic_pointer_cast (_current_stripable); + + if (!r) { + return; + } + + boost::shared_ptr pannable = r->pannable (); + + if (!pannable) { + return; + } + + boost::shared_ptr width = pannable->pan_width_control; + + if (!width) { + return; + } + + width->set_value (width->interface_to_internal (width->internal_to_interface (width->get_value()) + (delta)), Controllable::NoGroup); +} + +void +CC121::mixbus_pan (float delta) +{ +#ifdef MIXBUS + if (!_current_stripable) { + return; + } + boost::shared_ptr r = boost::dynamic_pointer_cast (_current_stripable); + + if (!r) { + return; + } + + + const uint32_t port_channel_post_pan = 2; // gtk2_ardour/mixbus_ports.h + boost::shared_ptr plug = r->ch_post(); + + if (!plug) { + return; + } + + boost::shared_ptr azimuth = boost::dynamic_pointer_cast (plug->control (Evoral::Parameter (ARDOUR::PluginAutomation, 0, port_channel_post_pan))); + + if (!azimuth) { + return; + } + + azimuth->set_value (azimuth->interface_to_internal (azimuth->internal_to_interface (azimuth->get_value()) + (delta)), Controllable::NoGroup); +#endif +} + +void +CC121::punch () +{ + access_action ("Transport/TogglePunch"); +} diff --git a/libs/surfaces/cc121/wscript b/libs/surfaces/cc121/wscript new file mode 100644 index 0000000000..478834aa3a --- /dev/null +++ b/libs/surfaces/cc121/wscript @@ -0,0 +1,34 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import os + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + autowaf.set_options(opt) + +def configure(conf): + autowaf.configure(conf) + +def build(bld): + obj = bld(features = 'cxx cxxshlib') + obj.source = ''' + cc121.cc + gui.cc + cc121_interface.cc + operations.cc + ''' + obj.export_includes = ['.'] + obj.defines = [ 'PACKAGE="ardour_cc121"' ] + obj.defines += [ 'ARDOURSURFACE_DLL_EXPORTS' ] + obj.includes = [ '.', './cc121'] + obj.name = 'libardour_cc121' + obj.target = 'ardour_cc121' + obj.uselib = 'GTKMM GTK GDK XML' + obj.use = 'libardour libardour_cp libgtkmm2ext libpbd' + obj.install_path = os.path.join(bld.env['LIBDIR'], 'surfaces') + +def shutdown(): + autowaf.shutdown() diff --git a/libs/surfaces/wscript b/libs/surfaces/wscript index 0c736733a1..7ab04ef9f8 100644 --- a/libs/surfaces/wscript +++ b/libs/surfaces/wscript @@ -22,6 +22,7 @@ out = 'build' children = [ 'control_protocol', 'faderport', + 'cc121', 'generic_midi', 'mackie', ] @@ -74,6 +75,7 @@ def build(bld): bld.recurse('control_protocol') bld.recurse('generic_midi') bld.recurse('faderport') + bld.recurse('cc121') bld.recurse('mackie') if bld.is_defined ('HAVE_LO'):