From 91e83608b59df0e01aa550bc8e5af3337a1fa53f Mon Sep 17 00:00:00 2001
From: Mamat79
M{ra?quZ=9Ftf+9DHTiv* z((i368jlnIYls#L@S!rUb+b)S-qEM#loN{cbEzNQqzlwE>adf)ToSr9%)+iV`Rtdr z@y@Ez^ Y^^s4w#Xn7i~E4-;uM3h(-458SG7B{)5z6?@%>VCVSl8b zo40Mp`-;w&h8OiP2euupoxvk^f;vp!W#eDd{K^lNk(C(&(48;=67Khbmn9K}Z*9v3z%)7{!w?kCpN zBkRkGc`^!lr!iT%+}ZP-;{K@Gp#1$+aNkUEW2koJ$`Mfxn}x-WMRyKqt5sh5Y%BcK zRAsO#7j95ZI`Gp&u~cgm9%XX@PY30vN5 ur_jBw|~Q5u0aA6H?G9{Rz_-`(ayP=o6<=mo`Q|9Uj!4PPY>taxRI!gV6NJ$y4M zU0@kUFQZ@yeudvi1MagZ$_B 7sEMeWQ>Ak^mIkO%nT9Jk;_f zluGK!Vz1s8!nm(CTi11z*+DSYj-bUP;BvdHL5FDFn4XRB(7T>|>PyM{yKCP+s~C{3 zakW33$D0Klyci%Ld-G#^Wv6xyLSUh}dN;m-# Evii(NDVUUMKriQ++K@sXp@?Y zHGP+x+gn|dxt*Q^*I9kb8P~jzpLfEQ+XNIx6RoP~Pp+Psdyk4!&Hr~7PD z?!v(I@$3sde@E)7JP{Ix(%MgRaax$?khuoiXwY&V )+x(D<1;GeuL+4i0u~^$gZr#G_s)!y~YNSyl2uYJ-rLpGpa1^E5pVCw)4K% zYxgn{HRenqJ~u5>-@XnBy>ZTcWwN^0r0#23W=^d46=((E6m08iZIuy9hoqToHdwR? zS^tbr+~h7)ik
SOBcZVL+dYYZdP?W3C-K8nS%pL nuXr)6pVF|6y-yw@<|prAud>m>@Hpup5>JesTzv1LfY9^@LaU!t z=)Cbhh=+6hC7Rr1Kl(Dv7mu2hm_C%8_MRc>ipgVRp2WvFt0Ih*7Pz-_hz9WU>6Vk5 zSr@E-I-AA!MRY1HywY|L(h9qV)LxfIDmOgUv*#JJM^fo)mRf6a@%4CmGGFzXP5E^Y zT>dtT`(L;sFIB1cPE@?C3gi4X?9~<4b~4<7Q;sUIv0B?;*)V^yqMa*Ei~1?OMv>DZ z-j2EWtfpGCOglPy3H1HRUEHt2B8RQcz8EoT&S*aGV`u_CIAwaBe!Z=)fFi+Hp;DO- zm~pa%H|tdP5^>n OV{Nt;x>8dvO*t zM5z!}!rt}RS)GpO_Ud5*vZv3gGsuya#8H(@kH g$Fh2>hEp z_qtHsR)k0ZWqs=f?&TZ@M{2F{<=Wa4@Le?AzN${xXE36)KeS45x7s*io=%r{!(_ $h-K>(9i{K;>T3=5_1n_rx@23eyNv-y90d ejN0fZ034;JP1h`7=$)U(M@_vJ;3m-lLuzkO6b$#TP zNgDOw$|n$?iI>5Z(XRcHm(cU_#)&=8ZGu4KJux0vr3Dv2!Uvx3%LMA<;Wyz8q`0LC zQN=j5D7}wT>mlEY=A`S` &x4c1CW3I!y0t^$)8e?`ilHR&Krd!s&$hc%Z=Y?lfQTZKwFUuT5G5 z9`gfF_6pk6dAi&b+r`)VZre@E752>4y{se|+@0Ijy%v}orpY88?muN+;zc5u4)|Fq zri<}+xp>wCS<&;sdtS}dswp&=R4(oH-;d}W4DPOY$WQw=F2D2PruI??!Li;f6c>ks z#I_o%{xxOVJGn|Stiueyc9^jWcCgu0i(mZ`duzE~cK!OSvnhTP=Fa@nNjJdlx<3Ul z?Z)0yN>1hK1c75t8i~Hmk~4t4$lN *f=6%l zDKeAVBl{ABUHG=S9tX}X-!H0dOwiRer%%A|d4{EUdMY_q0Gyk58q=*$S1nz?%HwUv zgQl|Wmf1$UY&2?jLaU(8tP)x+W!O8(^#=D<$$&XH)_Ny9zae;8-tDa2N*s~LhrjX% z$E=tO%XnxLU)46QSXI06jfktKPRBYA@h|!_BX_6pVPJPTb#Oj8I<~mkpP}O^nA1mb zD=%fNvau)LmeYsC<4(b5Cp?E0euh3S;SvEC6^KSlm0px>10 P_ zNc_gV>)gVNb*>J97$-YndkfuxBGO$|bERyoAQ|k?@lxz;=7ZJ+D>{B_tx4rvp+;XZ zRz4@mR@_ZQ1H!r>>8khsC;`ysU=lKWa8%e~J=$OA@a;okD0W#YP3*#aZ+bU%aIv9> zvL7F*DhjeqhSu?3_wOX)?|%K??AI2 F NkQn#8YNOUHsCytK#mY ~uu+2GP#fd* zTIE`S>o2W5yJDX)TBI~?Flgl)N-mf&xbzM{Wa5jn4Fc5e?6KjM@6(`YLpK6hk@m{* zFj+3O1L`uqE@?(O^QO;B@^O1`n3J%<6+#ktaaq~DfD2}c0az>9>7w^&sDq^)?)O(A z1jENN?d}BeG5W9s*qRd&(Ud7GFMRDNWqvIV;(8vYnNE_?QP)`*e(8GvqGlh{o9&^k zRDD=WYAEzGaO&sF%nDzD>$E9LqX9^cYqiPvvS~1PU+!J|2ce(ix)9kb8IyTvomhE) zTUMp7rFQK)53TVTlk@Rwrw!cg>Rd~fmHwuC#V;?BV4H7^+*La1*gjd{xAt %k)6@wK$A?!s^?EUW?#sSv1snJt*6BS_hffFH z73Ws=1-BB|?k1Vl{UG~eUl%TCv5i9H7JuZX{^odk4Xaji7p>}*EJh%OipyM%sP=V} zX)lzdKPMK=R&Xi4ij%9R-|A__R`dOBu?82}pnD!pW3nc6Df>KI`T0Bg@SLk e{l0F3y*1>{Y2A0wItz}pT01QL3<1d zLo7n@Fh)8%gy?*A;2tNDeFy8n%>TcK`%~!`ExzBf*DQ>FN#g6T+w{Ut{Rkb}N;J I%IK&614iBCd;kCd diff --git a/src/silemio_control_hub/manuals/Controller-Studio-for-LiveProfessor-Notice-FR.pdf b/src/silemio_control_hub/manuals/Controller-Studio-for-LiveProfessor-Notice-FR.pdf index 8accdb125f843fa107264a049c8793d047cea7d0..3cdd279beff1bad8a511a745e9e136fc84d98b74 100644 GIT binary patch delta 12227 zcmajF%Mya_z9m+jODEZ#%RiOWnWU?7L_kEYf*= d*iD&;R1j|MJiO>fiqH*MInzG+pBv{;&Vj zKm1|*KcD~IzyF6nvVZaA_`fL1UtaI$X`lVY+`ls+8U|q<7HyIVL-S%M^!TKCAAX*g zsBT=v+ma11sbq@qCwlugmOfJZ-&mxt^~UZYg?ntHmN(aN5A;Nz{PT z$ZgP9VvO&Kz1DNH;@a2NcZX(&+jTCCOT=)gCzbWS+gsW5f|?gLSLcn;Aimf5-DDS^ zCk1fFyy6~`OzCQ2r2Av^(?X_WuOW%PB+LOv>asC*O!jH;35u1`q0}NyuDt5?^s~9} z#j>~pYYH}!I(`_hD`M<_zEzNx^~mHarqDkUCIh-ao2-@n$4zF0&(PFX{eG?0b59%5 z9OYQDWRZQ|?6$96vtD`U#xx)7XT3n=4Yt(5gz+A@`w5Ver8P0{!wkxvSb4SE1(n!Z z3HZ_Pb{wrZcIge8_UWDamCobiU0WFjekR^)VJXT)WJiZ2wUsqa-Z$Wt>jAL``&CL4 z-A8$0Un*FgPRdvjr^+OjDottJL4|BzrJ51K7r^EUU5mLepheEDPM)dNWwUkdB)Ki+ z6Pkz5d6pujy6-g;w9x!&)&8_Bl*Ke?O6`MwEXxp9vZ#ZR)LwqTraN-N_fx%agb&*j zwQLP)+H2Mh47~Tiz`8R+A(bB1AK7%~xu4KIpS#^lvegylj?eb<@n(|tL82uFG}7@` zqkGFNKjY#w%Vy9q%P{#{y6>zVe%mVG=psAzOl)x6Vc}VAq1;Z^&kG29GgCFGtGX|^ z{pBs0VwdBQRrbt9ACX~l9uh6YTiuuS?CqC}Ve%kNhFvw)ggIRTs2!G>i)579>;*{+ zqINyM;o+s4?dOGw@@lwWqHz%qubEL;GMPQQl8FyYQm@!KjFbA)+clZxrGXFI!|Q%= zsIXes65pLE(OQ`{$MwZ#SS~E;y2lOFM>5C}9%}>Go;gomPV_|aKuIBdb{!zyohd-< zXu(DcXWNTJx@6g(D+2Dn^;Yd|V&Yy1%pGAmd_mhp##Jk5%gB52YHngf@P+o@vv?r| z&&&Qzxr)?sCTkInZh}#2Atz05rz^D}ZMDOvs;_75+K}1jrF|B;rzq((I|zrN^JWAZ z%=@{+6lB*q?)Kyh-kPtXIO;B;&HY4OYj87vwa=|xjSo_AxI0zPm6UEz%)kDh fW$?;hh@iAoE+@)j8`utaju? zc$H419_POD7(A@r4y8>S=J?Wh4{u0!pEbG5aVw7P2dpk1CZy=-(IOr-1v+(4+r{O( zMD=Jc+rv3vRVdezJr3)yxgCEpYl Bg?H@;W+lC$xN* z7c=y<@+FPO7)_UTb^B3pFM>Y9>aZz-aecFr9}u(?cQ>}dLqTbIm_DauUqA8@TIdLk zyQi}abTo>L_YHW@tG0AYYO6BPKc9lpfha8{?KKzCCZRpM>T7Rbl?M`TKcBzt;s5== z|D*90sDHkPGf}wTz!TimUaj!pslL{4o66bg?%bVHr581p#Ye{}@KMviGhZp)IHUXZ zu>~Pk5`Ug0Mc(uv;h{4C!44X?10xq&PdxvjCCzTn%6$wU4?8{28=|K=fccgELyLo2 zGjRT3%;LUw>wI(ytTna+@Th-2+;!|~`ce1ys|IFMagVWQhm}LKNoN}ry$ pU`#L*E7$Bhhv;ss4yK1&ilQnT(`acAUN?b2t{?*`4zc|E7~V(3ce z( DxT@1CB&C<*zG)l5@Lt z!gCSc3RB2fmK5dl!h0U01GuhDv2d_KVfXOBv;L22i3EKo%FBf|K`FFkBmAA#W3T@y ztqk^eANCS8l*0=X)|cayUolY?3zv74{B+MB*0>h*sJu`mkkdPsv_w0_FEJQ`&+R2R zM0?-gb;N~(s;&8v$E*=>;=B&w`|d946X7O%-&;HBpdI_GX?xr0ML~N#c!JYy^W}0t zVz4`AM2$?ZO$VJ5f|woXC{OWj6xmcTJ($=^=y74G(k|kUwR7!x D^ti8b#+)0#++B9yOx2DT@{^QO3amzwe% z7&f6hR(p44EPJ4dc^J?6!p`#_3cL|~`nWaNFn_HUXW=!!fwp0fU>R)9zvavM5pIqX z@?5<-#xb}9{=HQ0caMz}EmzvijUKowr(<_Jn#uZd*T21v*AzV7iv9kS-RQ{j43D}Q z;+dDQs46`M^3`Ga$u*Ce-pZ8p6v|m$=FhTPD+#E+>LAxdJX7KTT0FuM^j&Oq{Z&fY zU1ehNrDoEn6}|)0;`Us*uIpMB&xKO9W xCHY z5k{|W8a|$T8(oH%1wO1q#+n^Q$<9B3ld~N@B-K!hZ@7n?Ajoe?#-P*ve6i;VIIgrN z(P=D`>!jpO(RjgCUuQ9$VCccPrmiC`tsEFNw(FI4?&XA1ESU#VMQ#ET-DVT#@WE3( zcnnRhs~xsjL=(0U?9hFiJu^a vG}<8$)V5rZy@%+fj=S^xPOLX zVWGGZxO!~D&0deWK8DRVhM@8YZ|3zvLPN`$mRe?WA6rfrz*Rz3&lo;-rq;fi1oaEu z^>R=O(x7X#U($`*%!EZkyt_5XOz*cxO}_~6lgLiy>QTq0iGzJLb~+ELbB!3!XVY+k zzJJi(Ob*NOx;#iu`bDGDOSS+icNlhCG6VOPQ+}#Hue0$1*rpvd6Fvt{>GzYoSx527 zHhSX;LC$ix)?GbMk2ldc{BCC6(6<|(9oP#}sz-6uMtxX;-UI5Zj6yG6vT2#W9J}aM zUfD}(@`fqz671eiYYx$ERV!)cg%!&?>M~#S?Q86crq^h8HV>(Gc{UGY1~7o_-Kou= zQGxZzAu4aI(eMHXC3(vZZyjZagCk&mAG^qPJ5(48&+3n0+X2?!nN`b)6W$X`^qoI( ztEo^x$hX1yI*}tnx~)8n_F;aYcaWc!m7I4D_f~a2`)S5X!RUFFaiM&{R-;N`(nfBj z;;WmEG|8?4ZKH}S#}XBh@fN`kt2q#D_Hw;+j}McJ)04xYbrI^NZ@wTLa6;dA_vy%Z zru8X#==6>oKAx6Shih=xUf)*9hJnTRd%5hk-*|S~@B2sPt$_GUJuk^_gM{jIS_piB zMXu-!b~eLQSe8x{cm+7P2t6O_r_&Uw4J(`Eifm4g+Yi=uTaorDf5W78kNckM4j$}P zs)fkJ7}a*CpAPP?@>}oB53Tcl_?kp=Ug0b>U$?GCCM{3Xlcm$_4D2)BRtL@> cB@X{Pwxx$A@vcfnNn*EkLuP)>D9f@MQC&PA#UpR<+W$Iw+)r1UT{V* r{wVV>@`Za_hRV1!sXYm#J(!t>B^5+ z4(vs?BB4L_=A+?BOu&6e8IgNz7B@xx#qAlDx!$a{xd;k}a`R|DxqWWeKyS5K1;y|O zZ%BJ!HOKOsp5{I25eWP4h@Do$5w$E@@d%q-{e{Wjbvf9j7;{y`Z2g^bl!2CdLJTa4 zraRvq)O^)xjiuR7k8e9tzTR2B_z}*H!_c=wRXU0(Y9-w**frd|&y=26+ca9{sj;k< z)(?d(G8eJUzhe!4p4LHWRAU)?&Y~wCh{4NxYZ{o9h_51cs8zZ#6MiABdvzuYn>tN; z(>Yy{>ny?Np;+JMkEXsj1=JWCwbWiCOE+54Uf2#`-Sib(oKJwQS1$W|c-)7nEu(yR zM^1=g>l0QO)$eM|b8yhjV}8qS_I}Tj&DL}AXwAV!!T<-*D ItL9)A>f0QY=AO 8W>6C&DtdY7ZamqPZ%4u4@_W42jBDGH#Yyaq)Cl z$WyA{L&;8Fr_z#zBBM`Hdv2Hg7(`~15*coQn_sboN!(txYt)!vn}^d8N!Q|QB|Mq* z#dh2lz<@t%1WZeAK5j3~+I8M#2H|75E9L#lLWv56n~jf^V418dS>oZ}iX#fMe$lwB z8_?=i9EU7~X|sV!%(p{yf^c<-JUZ^t9Ee7%Rs&bCTFuVAX1P5X l#W6^y}89j|2yrYHSN#n-rL5qs$hx$z0`-P8rmL0eB`;#Cs zM}2gD1Ik@QrdV*QVl3N)Mp5IZ6k|&6EQ?IvYn F}YZyeaJYKy^&SgyM9xk^-ep7)CB Yd~AoexD|iKr%5uS+PjXwy;ld7<$cwm7`l`^F6)(&2@`R6+vmvlc%!(v*P1+C zRx1m$opG)mCBrR0R$FLTKCPI&SL-@cRy$1BrCQcF0}Qe$a%uIpx;3y8zA083NT0e7 zXGE{)z{9bh+EeGziywEcTexPiw0-E?m6Hu7>?Bi|F(Um86W0lj2Of~*Zwt- z+stV =MZ^yZFH2>DZhRs2mSCQD^D`Uv zbTFgt)#?a|uDhc#B|r?}sT|fR6S=yBLLqa3PF;pTQijFg#wgAKmX$9v07$%YDWL-g z8}~Y)7nfcrOzihYA45w&mues t>;OSLxNoZ>RR=Kd<(I7jS+76<{cN9rRzLX0sKDaNF-WLM-uuC zZH^u_L+K&N&n&qKV(qa$GYWw)(qr|}@m7~^U|Sv_k%&lO5CYz;L @(D-)`0LzWKq$ys{<}ll@Cz zp|oSloyOB8_R(Dw`XLbon^&_`07@jcah^mTuX_z1UfJj_zp->V98>2VS)PrE+4YR- zF1hx!d4-xwQ?ha%B^i4FS|M|`9c}y5 tgSD(frteSPG8Gd+PL=5sks zkwBBicX})r+q0f-93R`(rn?i)g%8-D=#Tt*6)EWrNlfjMT{;DWcu|9&$ZMDgM5mA1 zFCj?GbUEwcHHv=H<)s7M77)_5%0y|z4Z$s0P{b;bD9>69GH^$qeNT^29wF`WHL|e5 z(ulL-MzR{<+VNYL^N;QcWKfEq(5)fl*s>EDrKY)6HG>b5YX<|@sijV^_u=}g{{|)w zG7r${KB-KynxiQ8mZ($c>AoCH?~V<|Fe1m9<0VZ%YCDBGT85mNsO7l{4c;Bk8V^B& zF_!E8YM8<04tQN6Jd&JRXOXF6Z0~I2{76Ch?M-~veSE(B+cflV$e*Rdaers&NWXO9 zg8eL=#o^IJ-g9eh@7|ZLD+Bu3RX@c@0hE1h1y8%&eitpgg2Yc+;0Pn@)85(94>w8R zyNS>_`+a(Igz6_(pf4Z!aBEykmDM?{_{X;|M}BX5wZQw=Z%*nnju{~pOVhN_28i?Z zQN0j~S&u#DQJ0`~Lm1_o%JE~3?PivBo3gk09m>uAMC2oHYmC?Ey5unoKka%(jZd{U z*c~4~I^Dk5J47>b6wJCEnw+!UfjQ3;Jip)-uh$whF--l=+1cK=8v%<9ib4BoP%pZN z3^kJjTNdP177z7Q01j;T9>W8HzbTi-VZ5rwzajaUD8;Bw!&|=9=T%_ypi@{WB&}hz znkr3ab=eiMAAe+cbHP`fMYT3KaYd3ua&oj5I#K4^H)H#SK=;_M5k3;22MRuKyM32A zHno*!S7Dx!Rv Yc@Y8b4$NpVc4Uj %(vnV;d|L8YG~|mH?kTUaa-;62cqIyeSlWV-FbX?lQpt7q3q~U zxHRfh_R!*?4yOeDvNDLAhdS7g7NU85;9{i~?Fsd9C_2Mgr~GBr!g@%~$@f|73mLZE zi(D@Tl6ODXL!)dmHLcG#`NDj{pr`5trGeR*8Io1OccQj`8%*2%_JPP3rQ6!bXI`Yt ze$%6FjvSR*LeyTDn)QRtM=ASJCJ *0a9Hk<49=v_x>8%ovP2 zeqyUpI8A8d>g=G8Iqy8Up0}{HJ*F`(QS=8>RG!7lGy9AWL-IS1^8T@RhB(+6W!=rI zrDK}utoa78J&W*g?Ka+K!D=;5M F3v9E-x?zHf#QP zVi-3!=-X-6mx~3P)Z=V-*71aWluYuY?A8iTmTyb{5;a@+u*eRx+ofC*G=X{yV)8BP z-Dm6pgg`O@9?bD1omH4%b3m>L>=<3*P5!zK2Kbf6MQXHg0YvIIH(l%kC1%)ssn%LW z;nAvIL4!em@$Q2++-b!S-rbm5?J=A#v_VE)_ggzyKbqWZ_88c~yjp3tAd(&MaO$*9 z2Zq}iVY47>ggKT-^t2t7`0N&LrArMBUzofXfzubwr@UEsAiqZttE1~>A6T0`A=ZoK zVN3Z1sN>Cody0#)Y2TgGZf`}}*>&$-ae52hWU0)S$K3fi+m8x^AYbI{@dfcMtf}6; z?&3-Qgj~CW6{V%#pRsG90YCTK#%I}1$I* h0pDbBJ(hN@g{EBNPW3FM`4&yl#t#7CMR^5ji|XJ=@9SjG|JZm4 z@*+=EtYM)oJLi0v;JebS+T_UBnCO8V3fGT5^uok%-CGUn1r(FQCiU`0E;y@~0a990 zP41MGJH~2@-PWJxuuCtu3UnO-!`bxEtH|XYx-F#i+A0t80dV$51LZ*z&E{phNSAu2 zJu74Hde?;0ebauqo9!Vffxu-|JS`z^0=BQaI)*CP@@pz>CfebJ`LQjm!v2|>^zGRw z@Rbj}t~$-fZ+xX9!A4UYc;?7boX S<9tup3Q!Zj$ZGOy)V?y7g)#fWwO5P%dP7woIZJ-)2+3herdUX<0E6&eA8or zKYe X8Npx5$cjw}BK50cqiF=c~BQ`5z tTI2sNBYi^3u;x-lohP}!ovP${V3`0t(8q}b9x~`EPmuJK$e+$V!Wg0 zU_Z11tF;uV;r&B8Ug-!d%hOj__()UZ^pk<@O{z~|px+QJfA5%Q{yV_HD|#7wX0+!B zMPYWu`Jk#LPn)+pFMk>OfV^0>_ekzX1h{=nwM`faLOI_O+h=jv;$E 9u-Y!kn zcr~8f0)9N&Vk =|8POfc%n z?Y;dwP(}6^824BHrURa!7C3*RYFwH?)k`6IF4aA5p4iK0G@&}>>3FG*cM0X_C=h-) z)Mbjh_z4quX?se>Wu<@Lp8a7c73nGTDo+dAc-7x5c3`N#fl1)Q^3nOuKnZ}Eo&b%{ z2R>O%N_-O|2e6bXhr{FTyLNe@<%5j*)bG`s6p#DB?;u?7-6zA2nx0(e$D+U=e1y2J z;i+Z)w&uJ|<|U%HbKcwMUTe<#77HJKWerg@6AG<%CmC!KsLxd&!HfH5fMAvkRf07? zL!GGCiG&(=3S@oKKA%8!DvHOJ>z#uEIc^e{fr4eNPG?wISg-yfRzZM~!`UYNCD-q2 ziK(ZKkDCv|E~^FswPJ*!IeY3}^Cqb<_#P=0%D=M2Tv)a@J3t)2oX5B0r)~L%YFD?a z)QN(9b0^2qj`!9@dOQni?@IRop@ uE4hs2@+1-u>+Q&VtGoFGOdWRu zx9Xo!O;}j9*6R{ouC=h4EgPvjxT3q-VvpdKq}Ln 0;{JPAUQFofA7ZysTiM zIDB;~@F$nsvXtH9GfIboy^G!_=2}ZID{KCCSG|q7ZEfo`(b#+S&rfObL%|HEpylZJ z*&U@un_*wdM5+CPD(tf<{f%h;7xfS2%l +3PGg zt35?c;uqOH476BHN6frb?M7b}XP@$}3T} gYqN=aK$%7jG<&V` zGnDyP&4Ru6pc=-waX9nOdqr)16e%56FP^#eedyQ9J?|AJ?9$f9v_hj&HiyjU<(x@x z 6Wvj!3Pguf1;h|F r==UUU5RNP?=(-<6Y;Rgbxl!P# zZBw(&TTq+CNQ{}cM$RO=cImrhm|sP>g^_Rb*A?2ASyG(*jKhnUL4)2B2R8;Zf7i>+ zS2455`Dxdh0v<9P{}eM2_#x7tjr~X1@U@UAJgm_Z>B)q4Jm0U*ojA_nQq7!( E?@^S4!yz3dco6mpwB+uCH*t(yrn z8;N~|{y}(U>LNI!alL1hM32yA(QT*PWQggr|AK;GC?$`Y$h>Di|Jr4q3m!#xm6681 zmk;#xj?lpBc7sa(c{liQhTIol@v||;i^+N(r(MFnKVy4oKRQdMv=Hn2+JPSSf@T-m zQoxE&)!j68`7sz>tA%b+C45ZWBIC29=LHJe-p1HWvJv+2f^bkC`^I52rM4>M*2@Lb zxYv@w!K8NYQ#n^YNS*)9;-#4TV%Dy<+>NDamzrx>ew&SFKgA5;+`@{h!~y_B_I&=y zB43EJXj&jZX)nIbw7P24k{#^ej+(s=?djxofwoqC;o5RU0ask7qVxTuZVF@5G5V#X ztBy-cyVcm7kKWqv$(=WF+VmF4zi9mv7X3H>?%%Zk__x1SIz?6gk@~0CKi>ZFFTXdM zs&PEe{{340xBPc&-0#}@->=a%n*aMXhURO3zs6SU|MY$T_5b&We`U@(``7=q^T)q# z)vDDad*m45aIOn`q0ayI6J6a4N1i=#+^Md}|9@Bf6*K>M{42gnQ-6UVW7PS}{{^R^ B?85*6 delta 11107 zcmajFOA~_Zw&zzT>2&(+PC7f2GwP}w1W^ (O4jrLuk~B&Kl~~E&;O(JKm73zfBa{E z{O5oCi$DI0KmO$(|K%V5>W_c?<1c^w>p%XhKmO}K{>>l%&HwV(KmX}p;w_wL5`X?r z|MXY&Kb`;k|L{+LP5 `1e&{I?hXPd zU %2v5kOu@wM28_<#Y4U0`58jgE6Kl2rSNv7s+Gag|^e$$X z&23r5?TuJ`wd;vBhc9sTI|kTTn7X2{=w(KSDSbHNsmR8v`vBbAyf7jYW^0$7IwCJ% zUFf*zWmtZdC2u|j`QE04w OQ9tRdEx7IHD-4mu|=XMlLh&5o$ E0b#KB~d&kXbp7y7%0x!17lw_RS&P*1V8>q+U;?rtJrM8KB+Iak7OZ*D- z_UU*#zTFY3P&@&0L5kV0w|3Iy;;f&KkD*pmJKIq*%0d}8;Pu=i^J-t2tnkDvX8oDr zmJXlmzCK&5iNXAu&p#bkL4u-S%(HvvBVE(t3T>Vh-B-eK5U2pJvbXbs3$`vx-8ENH z0Jns?Yi_xQR$^i3njQOVH2|xBrsa9dnRZRiXSdz17@X)^_sNIpDInDb!9eYD9l^Hy z_95wU$re41kJR|`I4l@)^V?J_JckiK+aa^zJ|>{hc@b)9W$t;9d-Dxda>3PLF-EKq zm7bJ?*pJebj>kQsFyg-HdBE#-ADC_L6r=82b}WPjQpuGHPn65c(lypDkJ^K}Jj|25 z)HWCEtb;_WxZ|?QaZ;#uN~gU(UkHPG^xpNtg5F^?Ax?wW%x?CGQ8U_s+ 8ldgQ>uG8^BuAkvo7mL^AHo>YDzrt~iS{;d%vm7R_T&nnmP0DOL_bup#BSmYz zCi`MqzC>KTPT8$?*S>793lqQ5*d)Jp3EZtVr7bJ3?T=V%`R^PAkQQH83Y$vt1o}>K zQLFFKr(4-9Q+(2_2=k}0e8Y2L7F%VHAXaBi0^_7!JvlVQkdQFlH_k+U-E>hByleG$ zghJJ&dq^jJm5QiRe9(K3bm5bD`>^=7liB+P>xdEK9#g=S)ECxkgx@6a&|58%N4;01 zsiKS3qGiJ&N@s1@W@~z_UR|~Pd!KpQ;*dNI!lHxmSD>VYeYg6_nh>}W 2lIx1M}ewC+Em>ZJ#;hmdIRtFyb6bB)C!@Tk4^svKNK zu=*mgREeV3oONM~7q}xK++{fA`~a3_X|o>;y>{HZ4lyh|W2XmQFjv%?O9r<~ePE28 zT`0tNpnROVLkV)n`LrOGqHY9TE$J)Iu5WYuXd4B0S4&%$Gu =Nu?S0@Xuw7g^Z`25!S=dPNf>c(x{*X(h2+xw#i&<`bi zUWCh45fTN%Tk$%d^yO=d?JC1c)PFR1uGo&T_OK?H1kpREtWJajn aPJiH%_IYty(i`@wY5}}`&+URR|}>CS^Z)?^jNd5^`dl5 z*}cF;j$y~B^X|iJPL|zt`s5qV{f?CswY_ilJZJGV9E%#ighfZV)(g;nXBmteto6^F z5r+c}RJ!eBI{q$(;Hc1C2fNkw7P)X%ZYm`dX8@#dg6anMG>0BiZZ_8Apr1bit)kaj z*V;1D?7>E&7PloxM=yiNv5w~AIo@+H0_t>Odly&F%By@QE2A yhfE0O>8x=iQU86Y?!*u06szi>a#)38?f _5+G< znJHMcX!IDWann*C$`tlct@F6_Z%wGmJ7~PRpQ~dc!O@=^>d 1n3qpZS`ruuDJVLuN$6rQ?^`U`B-LPHBktg6O-^HR6#AW| zeIg6T+)X|IkT*B9*lz@gQ+5u1JhYdKW5iBis6XuyiwapIPDSrUR#a#u-!A<~?|6qi zZ!4bwTvWdodq0}fGUrnFVtnlO@ApMK08et(aG_^yTD_m=d1#HW2<5S8)HwFe?YC0X zBklS2>?c>Cp0M|R#A-dUjlR#R+U+tmlF`tuz(_O;nGqbq3rV6*Hd9jxroY1nC*(7J zY*p?JDvK|g9o~=aU}(}9T(2yPGdDOz^~|IlnXH1+XI&_!=%Zg@6b0SewP<&zudUaX zE;aq|L2kT?9s}pa^toGnFfpII^~S^L)h794t--o7jQb(02Jc4+^oEP`JUzB;t14fW z`&IH^xHSXvq0|DNa|gEZ=`bXSPT>v&No6Z~Hsqe6Qt;>h4gKu3K{ozBI(5t7;ZLWO zMq0u95sd@4qVGDVTCW1W*e-6mK5KAximr#H&FQ$rD&-BZCBpEtHrPrp; h!iy>oWw!+4eXUX0K*CISUK1p_o|NB#wsKK#=}tv0vW@FfL`lTIh{d(_uHe z!1YpEb;R=whB@&h0NiU0F8JnSs K7AR zH;nFlHN4j+wyqDZt+rDZ9~7H;o>q4HZ27p<`R^b&6MV&88Gg%NMichX8c=ov7U`0G z7SD(4hPvrLyM1Nl8eln5FWS&b+HPXKn|}ll0)c_e6A*6I?o@+1`_4 qaPF-}V#46W)><8~k>l3iRNVzFXbSb^nMq8#&Wz?R^cl2NhL* z=4!m_5a)j3JgM-tfuB&O7t%godiyz3H=1*)Bu3wPlPDH@art_B6_aNP-_`Sz741f9 zEQsUa_!u-7138}@8U+E2J}nfE>E1KS*xcO3EG*U$l+S!l(w@`97GJuGwTp%AS{c{O z0=*K-R&_r0x5UngIv6T1UwkDThj7)}AII%h+IW2B(0-}%Y%}#`w>7z%Ph=mgXfpd* zN48(FH%ne2Q=PN#Cv(lS!vrJ9$&KAl!kiqhj(lZX5$fmL{7&-kp!t)8`>CF(*-fjT zJdtV6(Lom=V!pt vB`)ewAe%083~ozGlaXtp+8N{)iiRbRmI!PJvK;^$tO0 z&Xl9gQD2C$axqVpVtSlw+L$2kQFB@ %j8+^Bz-r0y2e8xrRn4Hg2b#zE#nYW#x8C`zk|}>1;mKM?+z$ zeyoBTanNcycrD uCNqyv5QEfh+PUH zU5g>Dc_xY{DZ5ocB8s3H$G1N>vg!=0yMDFNnN?;LQS{U^J{(WWG4VP>;9IT&+@d^8 z@4J&ij&tR%?eRm^&F}8=;ci~VQDw{xUPAoc$x^$?WyOQi1d5w^ZQUQg+gLPEb9GV! zWkF)oANSb8k4DqlXaOdyR%AEr5ZJlk#b%=!BW0Sx&W)9Oy2hn$QC`iqOLcWdsm5pl z1Ztb~F(U4aHoMD|DVK%eDYP$R;~_02Xne-M87Ld=uD?n#lOx_-lhg*Qwb^$b8kzL? z$3%G$W4B8FG>}ni`fh3cG*B%5H1H&etx!G6o%Y}XsKb$4S68o^w+K5T9{`#<98~w? z-S#tF4I5(TxQpw^U}v_OX }^}j5<7;eYHImlVCNs>iQeZ zs#jJH9PQc}ly=7*a-D(Q@@x3MO-$q=E!6tS`8u=mt2FVao8|BtGp&*juN B*)as;q*^l-ealWM4kv!?BA{p$G+38l9 zl{aVQmjwSxtV5o8xwaS#!D8i<0mWH3ko&-7cK#ODo~@I7@E9F$jnnd3Ji7(sSnIER zMBel8W;g@tn@RA7(yeLi?z>WNU~J`;w%BjD{firPwPj5KBuY!h0@kL6X4&fEy;Td` zb&1K|Zmg1)wbc`=T+`kfZ*3ijo{=Q< !b8)zxNZhFW zTqe%ELSG7mp#}!k2|qK+WP;SXJNvW^Ox=9J 2eK_zZB;DT;J*>R7PSpVc=#%Ho zSarm6zWaSQfeBW_)stZdh_bF1hZPNck22%EDb2o}er6 }@8?~#SLx1-o)0^wRYJERduKNL^oliIY`%!we@d3UK@PUY1gQ|PpjF~k= zDeKE&xY%sK{hI3bxRcvE4s796QtnHco$|$i7?Z>cne_;%+oLMpxZXAPy)&%1JCW9G z8@w#|Q;O~<^6?=}q@!_gr8%`7O`RPNKFQ%PzS8!>|Nbmb4cY#y`DoGC+YzC XoofwB=+aVz_R=_5=dE_``84#IuoncsmdmR-aMstm(H6^Ii|sYC_r_p8>r|>{ z+$eP#n`2VQk$mh7Tkp@}oQ}~G+ZhNVFFwO3Az*WVyy;}}B;mJ^q*;ePc JHxnfH zQEv2rWO%{ZEE^N6ckQ9D1Z{Mb&fJ|Sf>`flB4pFeKmYs2uij NpScM*R=D*0 9j(MVPNT=pTkE;sak zkiZI{0wY2!6edbg%Au5UmpEs96F(nNyJ}BugVZHx+W~8NylaF~i1Gn@E~wqRRG+#f z9E|5cNUta_36l7up{>JOcLzj!=t~||c$!bGPO=m7bcJtsa!-(`P$r_{G>7S#JJEvk z%T67CdP=ApYf&?RZrH9K244eHoZ;J?5yAP{T6woiH peQrTH?y#J$yE-u=qTNg&wc2@z)vF>EVhDHy@MyjV+tkytu4yTA*o!2MnH z6OM=Y7&l+96X)}G)oP@#3Op4jQ{%?JfRk%QMBdQe>zZiv=~o#-S`Xp9$18YSe}E1j zmZFZsoYE)m8)%Y{2lk?T!tW2^WVZs}>b`Cy-#GjdOy;xdUze}SZL8j>kNGP7z|yM( zW!PY1z`cz)rLLPZ5jrutxrok78(vfh8r>5!au3y#OvnhbZF)#yIbu`$lFImO(R|&3 z-3@wE%;M}dkSiKY7it^tM7OUHF#E`)i;n?l+y+E7cw9&aWHhuivpS1ZUh+b0QBE~$ z-UifQltsXhA0X{#z0uIIQII>SVcZP2UEXCQb6$Z*33*$v>jj(a>^#y@iJueC$5L&Q zXYmf7<)_`+9mi(1`>HD!i27#oy0edzNo7Z+oUWTWx_}Dy&ZW+W5|s<>g*7M@X=gX> zsr7zQy_8O*oMl%9V%~xBc+TQ$39`eNa0OfQqR^f;pJ&0oRayfD-MD?O+{qZDwd;c- z(LLkYyB(C)xYRmAAwLXjrRXc}yS*y9B+u(@3ncmEGA#_3vr1?`NbB8053Ssvwd=_9 zz-K|+QqeZ(4pG8=XiS+Ns$;&~ke9n&L%Vduup;nn_45X5PFKpCUcb0(ZMY3^vy0xu z&u^1PGk=ZIv8HF b{NxhScn&@u`0j0V`O@)a#?MLr(zc_`wu%R3C~d9vII~FP~Ej?s9UDY`Vl<#DyPYn|MUoE z)3WkJzIeNAclNQ0Wm)jx+@QXiSeg_DzwXePFLFb;b6oq7bFTH&E_T~}(88_a<;VWg zCNT~`Z%VQ{Z=L0~U$<>ifs$QuX9%mbu)BqiSWGF! E{OO4lQ}#OT@xIy{4B{b+9hyLMV>A1O`DB+v zTpt;a0e%Bd;1=%GyO#HJ!TAnV*Ee?$$etKqnLqz8=C9K)+rt0GQa-;;EzRK=Cg {!ES_Gd-u<}k&vG1p6XW?w97}55+CBV%&<3}TBzb*EhaUMJ z6;F7FaSnICv3+pqwI*Qq3E1wg(#>ttSdUg4uGg !_pX*r#BU*Ibz4Oly1{yE7`Z84o?7chfSHmuUrjbJ 0chN7 zo6c$kmJ6@Cn CXN;C3t|W-DSd4em+^@<>1)-?#(nmXtnS#G zvF4q4b(1-jzJYQ{U9!Mzhsyb)-JMK3pfD;QZ%>!ELie%xMdO(LxJ`c;q)cHwC8w_@ z+~1Qa^{Zr 4XFqw7vqtlD#EkcXbbGbrJDasW1!#R6(!-c;6V^IasBZgORbrDm zo?7A_NDhUYBi@7MuSisZv#4izf$( ssIH8;rQol^#DtH88+8DN*~cLRI3uHL#9YTe#`yzjY8`AQ(RX15@+ zZOq&!;GTzS4~z#)+1@zU>l;HS)5xwL^a^p{MP^m06coeQV9!7@`gWmVq-X`NWZd3L z-)q=rx6dFw+YfcqEKa5{Jc?+c8<^8t#}3!}aDHZ$F^P`VchzNdulRZG#mQ(${@fwg zak-wGE;Y02#YtK_ZU)y$EUR053s&hxMMgiuRJasguX(Az{&Up+k;W$Zx7+zQSioaJ zV+eC=$#z5v;3*fbO?mjxbXl(0I>c(82De)XiBuMBf506Y1JU!v FY4$YWO*?e2*Q-#IMPA(s>S1oVUEfL6!5n zhSh*k*0so$_m*B&k#!JiIz2tm=(Wkgnttd$i6URs&9%{NQuEijmE52ui?Qc&EDM0& zdNrs9xaG4cOLjLu%8aayo2zh$)1LsJ=0&Ui7 S8BT-2G_ILF8^tP=G%}&s2I$W9I}PTToLAaJ-y7!>7Qm8OTHmQ1 zC(mR_uUK>8CXDCdFGQ~W`vrO;hMbI%LmCEg!aepf~H$oV?+6k(m@_aZz;+r+aHz z&_Auf*x*^Anvq>@vYSxtr({gfT;SAhR9udC;j>dce!ZgWiM>+XEe@9JCZyL^fKG_q zP-|3M1+z7Wh{FjG@{<&w509P*3;yV^#PMEuY#`o4ti$c6)rDH2q0~VFyDxw91Ph25 zVHeN-*;gsn9g4k82|ZX! <%cf}z!GY6JeIQRhff}*Z|1ytv@ z?rzw@p_}E|U>2Q>`9_zDMP|_?_icJtewSar5thR{>C-JKIIB+;lq%<0Sg%i&fiu*1 zm9Exe`rX>Eg%p{Du=d6)1U#G$)%vhR6^44x`f`!sIUl6iZTE@VJ=Rf+o0VugLtm zaDpK-lsjStZ>cFC193RV#X+}QT0gJb!+NHn*clPL&PL?{6m@1z5e5b~wno*v>sb%? zq??~M(14Q_dOWTl_w2r6iH%Y_-S2hfy>@6L(2ykLKVQXWb_2RjuEeHqENx??a#fOU z{BmW!=IipnV!CS>nf1%n3%y#^FIbvM$O4|~KwYU|eOVS>19*~C)cC2~us+ln)<`Rz zM5+BF^~|Kcs%P>QiYM;$HRQmItLg5Ae^d3sl`eI5#upVNKRvgHr@W?`^a3wR3leZJ z#v{=;_-oV(F!a_3w_`#*;M)@g{QjzGVf!F^okNx>&7J=4)K(F_Hft5* zvQ?^_WBmq?#M7EPw{FOVMuz!oSS&)xpkbcOBt)&UH2#+Fz8SdT;dS|#I?U1R^M#B` z)4+;5N1@NE< e>0o(zH>nm^lwa}qy8q KCty_% zxvO;ueaYFoyg$#-c$@pqFCB5&iEvPigqX$n=#BF0V~td`OM0pDm(u05{&hg$&7c3f z@>lcwx9sw#s_adV`oI3oj5UkM0}=I}){ZPo$`0K@Wq9g#snOUH%GnqS_HxU;*SOiX zUp}4XG>;CyL8wNvPIPv1G#DGtD}qb1DlFQEdrghXx5{|LYV{pPGQz8sr^SK&4URxT zx*Ahe1iaoQm(>K>#1k2-F0Tz*Tvd?KM<~UwfCc4RkB2A|X%5%@7pXI4uGF26{6^6_ z#ey$*`m^Z33q(M-V#L?m^^$N?sRy?Yo_FB)eH|S3TFr~TLP*ATmIHw@hYRidw7pt5 zC$8Y&=kCni+Z@;XjiMiBQlX)oN>6+|Jp2kqii{9NIyTD3hisUIvXF?DEGvx0G7%4L zh8v9aAU9R{9D0!G1+3nO<$Yl1Uaz+oNG6b*jE egjXLsa@7RKFPj*W(Eep z3^>^g%>x5J4{hMS6QRCvdyZLXca5KBYIow(d_7&Rp7Ya_aSFYDc2nH0uO9qDtKUxV zrEZNA8K|Tm&tYeITJYn?rnKPmqdh9aoPb`an9?>kx8d&;&y?Elyj5S_G->l4s!0er z(mv_3%yF}tA89!~r%kTQq0jJ6k-yYhmkQzYUGe=Jn=Hh6ztP;Wt>cmuUOjPc=!4+E&5(OP@BLE)a}Bo|?XQ!HQ(3Q1KF>$ktvRmJR%K+x z>!G@%*eR9$c zF{^8qVY3iTx|9JfK{h$Bx`yucaMU%weuhA^a{QVl+Y&QOGjlyZN_4jMPUiIHU zgKCl#{k!hJ{eM50A{mzb^WTjA`fp_dKZX=R1t&j>LY$%57J~;Q-E2@n#6%(05&r+K X`13aUYyPh(qS5$Ev8Yb_fBAm^& None: except Exception as exc: self.close() raise MidiBackendError(f"envoi MIDI impossible: {exc}") from exc + + +class MidiInputListener: + """Small input-only connection used by Controller Studio's MIDI Learn UI.""" + + def __init__(self, callback: Callable[[Any], None]) -> None: + self.callback = callback + self.input_port: Any | None = None + self.input_name = "" + self._lock = threading.RLock() + + @property + def is_open(self) -> bool: + return bool(self.input_port and not self.input_port.closed) + + def open(self, requested_input: str) -> str: + mido = _mido() + with self._lock: + self.close() + self.input_name = resolve_port(requested_input, list(mido.get_input_names())) + try: + self.input_port = mido.open_input(self.input_name, callback=self.callback) + except Exception as exc: + self.close() + raise MidiBackendError(f"ouverture de l’entrée MIDI impossible: {exc}") from exc + return self.input_name + + def close(self) -> None: + with self._lock: + if self.input_port is not None: + try: + self.input_port.close() + except Exception: + pass + self.input_port = None diff --git a/tests/test_controller_contribution.py b/tests/test_controller_contribution.py index 75f2236..8cfec7c 100644 --- a/tests/test_controller_contribution.py +++ b/tests/test_controller_contribution.py @@ -7,6 +7,7 @@ import pytest from silemio_control_hub.controller_contribution import ( + controller_submission_body, controller_submission_url, validated_controller_payload, ) @@ -65,3 +66,14 @@ def test_submission_url_targets_the_product_repository_and_template(tmp_path: Pa assert parsed.path == "/Mamat79/Controller-Studio-for-LiveProfessor/issues/new" assert query["template"] == ["controller-profile.md"] assert query["title"] == ["Controller profile: ACME & Sons Knob / One"] + + +def test_submission_url_can_prefill_the_complete_validated_profile(tmp_path: Path): + profile, payload = validated_controller_payload(_profile(tmp_path / "profile.json")) + url = controller_submission_url(profile, payload=payload) + body = parse_qs(urlsplit(url).query)["body"][0] + + assert "ACME & Sons Knob / One" in body + assert '"id":"acme.knob"' in body + assert "```json" in body + assert controller_submission_body(profile, "{}").startswith("## Contrôleur") diff --git a/tests/test_controller_registry.py b/tests/test_controller_registry.py index c9bbc4e..8b7cf58 100644 --- a/tests/test_controller_registry.py +++ b/tests/test_controller_registry.py @@ -14,6 +14,7 @@ def test_builtin_profiles_are_valid_and_distinct(): registry = ControllerRegistry() profiles = registry.all() assert {profile.id for profile in profiles} == { + "akai.lpd8-mk2.program-1-mpc", "behringer.x-touch-compact.layer-a", "behringer.x-touch-mini.preset-a", "behringer.x-touch-one.midi", @@ -23,6 +24,7 @@ def test_builtin_profiles_are_valid_and_distinct(): "faderfox.pc4.setup-1", "faderfox.uc4.setup-1-group-1", "generic.midi.16", + "korg.nanokontrol2.cc-factory", "novation.launch-control-xl3.mode-16", } ec4 = registry.get("faderfox.ec4") @@ -64,6 +66,15 @@ def test_builtin_profiles_are_valid_and_distinct(): assert all(control.supports_rotation for control in twister.controls) assert all(control.supports_press for control in twister.controls) + nanokontrol = registry.get("korg.nanokontrol2.cc-factory") + assert nanokontrol.bank_size == 16 + assert sum(control.supports_rotation for control in nanokontrol.controls) == 16 + + lpd8 = registry.get("akai.lpd8-mk2.program-1-mpc") + assert lpd8.bank_size == 16 + assert sum(control.supports_rotation for control in lpd8.controls) == 8 + assert sum(control.supports_press for control in lpd8.controls) == 8 + def test_duplicate_control_ids_are_rejected(tmp_path): profile = { diff --git a/tests/test_controller_studio.py b/tests/test_controller_studio.py new file mode 100644 index 0000000..6c221cc --- /dev/null +++ b/tests/test_controller_studio.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import json +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from silemio_control_hub.controller_studio import ( + controller_profile_payload, + default_controller_payload, + editable_controller_payload, + midi_binding_from_message, + save_user_controller_profile, + suggest_controller_profile_id, + validate_controller_draft, +) +from silemio_control_hub.models import ControllerProfile, ProfileError + + +def test_profile_id_suggestion_is_schema_safe_and_accent_neutral(): + assert suggest_controller_profile_id("Béh/Ringer", "X Touch + Perso") == ( + "beh-ringer.x-touch-perso" + ) + + +def test_starter_profile_is_valid_and_round_trips(): + profile, payload = validate_controller_draft(default_controller_payload()) + assert profile.display_name == "Custom MIDI Controller" + assert profile.bank_size == 8 + assert len(profile.controls) == 8 + assert ControllerProfile.from_dict(controller_profile_payload(profile)) == profile + assert payload["status"] == "community" + + +def test_duplicate_is_personal_and_keeps_control_definitions(): + profile, _ = validate_controller_draft(default_controller_payload()) + duplicate = editable_controller_payload(profile, duplicate=True) + cloned, _ = validate_controller_draft(duplicate) + assert cloned.id == "custom.controller.custom" + assert cloned.status == "community" + assert cloned.controls == profile.controls + + +def test_user_profile_save_is_atomic_and_backs_up_replacement(tmp_path: Path): + payload = default_controller_payload() + first = save_user_controller_profile(payload, directory=tmp_path) + assert first.path.is_file() + assert first.backup_path is None + + payload["model"] = "MIDI Controller Mk II" + with pytest.raises(ProfileError, match="existe déjà"): + save_user_controller_profile(payload, directory=tmp_path) + second = save_user_controller_profile(payload, directory=tmp_path, replace=True) + assert second.backup_path is not None + assert second.profile.profile_version == "1.0.1" + assert json.loads(second.backup_path.read_text(encoding="utf-8"))["model"] == ( + "MIDI Controller" + ) + assert json.loads(second.path.read_text(encoding="utf-8"))["model"] == ( + "MIDI Controller Mk II" + ) + + +@pytest.mark.parametrize( + ("message", "expected"), + [ + ( + SimpleNamespace(type="control_change", channel=0, control=74, value=63), + {"message": "cc", "channel": 1, "number": 74}, + ), + ( + SimpleNamespace(type="note_on", channel=9, note=40, velocity=127), + {"message": "note", "channel": 10, "number": 40}, + ), + ( + SimpleNamespace(type="pitchwheel", channel=2, pitch=42), + {"message": "pitch_bend", "channel": 3}, + ), + ], +) +def test_midi_learn_converts_common_messages(message, expected): + assert midi_binding_from_message(message) == expected + + +def test_midi_learn_ignores_note_on_zero_and_unknown_messages(): + assert ( + midi_binding_from_message( + SimpleNamespace(type="note_on", channel=0, note=40, velocity=0) + ) + is None + ) + assert midi_binding_from_message(SimpleNamespace(type="clock")) is None diff --git a/tests/test_desktop.py b/tests/test_desktop.py index 06b0179..d63ce62 100644 --- a/tests/test_desktop.py +++ b/tests/test_desktop.py @@ -18,6 +18,7 @@ def test_desktop_controller_table_uses_the_registry_without_legacy_dependencies( ) assert {row.profile_id for row in rows} == { + "akai.lpd8-mk2.program-1-mpc", "behringer.x-touch-compact.layer-a", "behringer.x-touch-mini.preset-a", "behringer.x-touch-one.midi", @@ -27,6 +28,7 @@ def test_desktop_controller_table_uses_the_registry_without_legacy_dependencies( "faderfox.pc4.setup-1", "faderfox.uc4.setup-1-group-1", "generic.midi.16", + "korg.nanokontrol2.cc-factory", "novation.launch-control-xl3.mode-16", } assert all(row.controls > 0 for row in rows) diff --git a/tests/test_liveprofessor_controller_export.py b/tests/test_liveprofessor_controller_export.py index 5936a9b..71c2431 100644 --- a/tests/test_liveprofessor_controller_export.py +++ b/tests/test_liveprofessor_controller_export.py @@ -69,6 +69,28 @@ def test_generic_profile_export_contains_only_its_sixteen_rotaries(tmp_path): assert tree.get("OSCOutPort") == 9011 +@pytest.mark.parametrize( + ("profile_id", "expected_counts"), + [ + ("korg.nanokontrol2.cc-factory", (16, 0)), + ("akai.lpd8-mk2.program-1-mpc", (8, 8)), + ], +) +def test_common_controller_profiles_export_expected_liveprofessor_controls( + tmp_path, + profile_id, + expected_counts, +): + destination = tmp_path / f"{profile_id}.ctrl2" + + export_liveprofessor_controller( + ControllerRegistry().get(profile_id), + destination, + ) + + assert _counts(parse_tree(destination.read_bytes())) == expected_counts + + def test_ec4_profile_can_export_the_recommended_sixteen_rotary_unibank(tmp_path): destination = tmp_path / "Faderfox-EC4-UniBank.ctrl2" diff --git a/tests/test_product_identity.py b/tests/test_product_identity.py index 7ca1df4..e48422b 100644 --- a/tests/test_product_identity.py +++ b/tests/test_product_identity.py @@ -15,4 +15,4 @@ def test_current_product_identity_is_liveprofessor_specific(): assert FULL_PRODUCT_NAME == "Controller Studio for LiveProfessor" assert WINDOWS_PRODUCT_FOLDER == "Controller Studio for LiveProfessor" assert WINDOWS_PRODUCT_SLUG == "Controller-Studio-for-LiveProfessor" - assert __version__ == "2026.3" + assert __version__ == "2026.4"