From e4728b16ea91cfccddf4d991a9797f3fc1102de1 Mon Sep 17 00:00:00 2001 From: Thiemo Tiziani Date: Mon, 19 Oct 2026 00:21:47 +0200 Subject: [PATCH] Builder, upcoming page, mods page for the current mods - builder.html: build C900NXS.UPD in the browser (official 1.31 update in, mods ticked with their dependencies, no-liability and Claude Code confirmations, download). Ports of upd.py, the LZSS packer and the mods' application; the result is checked against the SHA-256 the Python patcher gives and is byte for byte the patcher's file. - builder/data.js from tools/make_builder_data.py: the mods' bytes per combination; firmware parts (UTILITY table, HOT KEYS box, the asset blob's resource table) are made from the user's own update. - index.html: rgb_waveform, extended_bpm, phasemeter, cuecolours, hotcuekeys (experimental), ledsaver, credit; stable/experimental and a green "ran on a player" dot. - upcoming.html: beat jump, stems, uniform Camelot / Open Key, 3-band waveform. - docs.html: the builder, the new mod names, hot cue loading and locks. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + assets/boot-mods.png | Bin 0 -> 6716 bytes assets/boot-stock.png | Bin 0 -> 6527 bytes builder.html | 110 ++++++++ builder/builder.js | 500 +++++++++++++++++++++++++++++++++++++ builder/data.js | 2 + builder/ui.js | 118 +++++++++ docs.html | 150 +++++------ index.html | 167 +++++++------ style.css | 89 +++++++ tools/make_builder_data.py | 234 +++++++++++++++++ upcoming.html | 121 +++++++++ 12 files changed, 1347 insertions(+), 145 deletions(-) create mode 100644 .gitignore create mode 100644 assets/boot-mods.png create mode 100644 assets/boot-stock.png create mode 100644 builder.html create mode 100644 builder/builder.js create mode 100644 builder/data.js create mode 100644 builder/ui.js create mode 100644 tools/make_builder_data.py create mode 100644 upcoming.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0040425 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_test/ diff --git a/assets/boot-mods.png b/assets/boot-mods.png new file mode 100644 index 0000000000000000000000000000000000000000..d42d7c4b267289b197af63dc42ae794a160883c5 GIT binary patch literal 6716 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV0^&A#K6FCDRtf=1_mi+PZ!6Kid%2*+UHA0 z&eDG{|GAb1vv9xxJq1?g83$S3_^_2RTJJbuabv;ZpAFxY?`+6_cJj@dsau=)UidJT zF&btZP#3shzMyNFi`T0CBC2-$QoEU*H}$Q05uCsGx$S#ThEaem1md=t>m7U@pc-oA zdGFeP>#zS~H9aS}7R{aV%bvlZ;b?!sE0dWj$4fIp@A9vh7Z5b<;I+`AxjILp859m! z`AklUY7d({O)zwF!fU0SMN=Ca<5-tF|DSvRdK_!&Bf$j212(0T_W1j?aj%lw{T;V|4Y=LigJtUo|5|UaO&KC@e0KQk6wCaZJT@{%lOPL15d`-zjOB7etTr= ztbNlof9{-O!=$s#%F|A5a>!L<-h_!o^UkzJIz(MJ{2%bj_CU?o5}y53Z=wf zXZgRlZ(ZNKVhQ)lGNC&InfSLD`TqO2=3?li#Y;|IykAr@YqnOTig_Ln(;DXb&%f`V z)VUhi_bt)p|GdTfpFZ0^+5V37f|>t))P5T_tNzUH`!{Xz<2!%0m%Fk=SUyNS`2SYr z(NL+SoASIq*}oIG@Id#Y{iMZvIXAotl>K|xrj6ls!s6Bb&EMirOgQ;%@nxCV=f~bT zDOB&b`|-i1O!(+~mx;d>E2q~~y_sntI51li?tR4&&EKWl%o zyWBOY(_nr5`3Lo?|IJ)VLMP3BqV}}^&XnH&lV<-56|6sd(Kk=!M&sIl^L(PurG;z$ zfB!Jo#QbZ$>QCjL^PfZ)O;j<@FAgr9b-AzT%ln-_@9uL_YW}*a_C@T~eOYU^ujrqo zk+^pQ7i6>-X2C)-8E7{kik>r#G!->%OU64Pj;8@Hb}ts<*!%?s?t+d)kyI z!Ph0Sf3C}T%(u~H#`TD*ubM9>JkmS)UNgS9IIjGB_S6^qJpXyr`Rf%I&zZe{oBV;< zcmBNl$5=jBb-C{I$@hytuRZSnM|r2`vRHAh{{bHkSp0h=Wb-@a%EP0pm(=+Fv^wJV zc=eOwqN{Z~^B+B9+&O=rRqp@T&-d)h?_~;k-wkA9iKwit&b89vi(C5VzN7a7v6K35 z|8Mi^Va>T|e`D94zgZn;ZN+V;3Hsf!WQkaRMn_i2@9Ad_QD)Vdt0UQeS^B;XKlanX ze%I%jUqV+Um-8`(#f9ho-cqai;`zl_hM|fKH^2TnSY&x8`q84-(MeCADITcut;_nk zewRx8u{&>*x)?9zpFX$btM}sx>&vdE*)Q^S4$<0j@1%|V&iY?#PXAVN_&%$>l1;Bv zqHNkZ2iJ9{^jRX_l&GzW(&L+#zTeDPZ(gZtQe@KqZ+ZMzzgCw0yua%Ct4BMh`>m2+ zUUNtC?d>J2+IMk9@4r5|=;qI_Yu^2P_)9pcw&iK0{uP=Uk>Iw{gg~ReZIPwEtZ?Y0kCKExhMzbTyaO zy@*qve)1}x{PxgIHio&nE3&R!+j_Wb*1||V<4Eb~&#!k%-F~|6(DbWQYi=iR zxvng4V}4@at<}@#OkIELy?o4@)P_*rTe}@AS2J|qow?0NeCm@LT?wtP6D@3wWlpzW z`@OFHUi>}X8~=BnjsLW@&?!%{$8UGpob|8X&r*3mX>-*F@sl-MW|U6yd2Lv37r1+s zd{lbrzoL2j&iAj{H*d4w-Rsxbx9xekGk0n1Gvz(ZA=%+YD=zB!+es8`S$*RDme&f! z%QY*1s-#j}ewUwqdjt|TUS?Q**s^)GDvwCUz<43 zyT>2BcXjeR$93snm7QcOwp_h?G49K0y*2mz+oT?)#+AjsEPgBhMlxY)@2ej#3sT=- z3TM(OyVIesJN?7^KgDye^4~o<_ejOfsD_|xU75G^LTrtU=Y98Bu0PL9Zq6My=1|jj zPPtosCd4fLbE{fzj!OFW&!03F$IW~Ht@zZ>D67qt8;Z)z=NiBI8nfijEz>hmy=6w9 z_N|*&zTfNIq?-$-Z|RwE@g(2ub6eu&W$apO?vy%3-{IfDwtM6L%4eJ#PVik0Uz@h5 zy1Vc?|Gw>^hu_sy+C0$yemVTA!e54R;i%kYCVoNBPF7x*tlu_yM#9c#UllzcvtBEY zo!(H*)--=o_mpi{lg}I7dU|)i$2sZs2g0_ec3-{F`{LB2giW6$)o0mACQZMev*XLv zN2>ROSt2UGO6R_NS}o@3xpLn6{L|Zem;d%W{WIg=fvGdUuHbzozvIlL%Tx4qgY}bB zj4mbAU!MH+*4G0;w&%Z|iCmR=>UrA9h0o=3OT+H}U7g;w?$e~}?CP=0wm!VhZmq8o zpZCZ1sr!-8ubOXnT{-R6a8xgS(Kh`T`*Xe5?yp| zX6Iz(7b|F5+sG|{TYXVt;_)@Fr|$oLs=V&GaP+Dt%h&y%HDx>Zh7m|7(ovfzx71$IGMqL+4&I zIsLu;bMEB7Qn_}j^W|mVdX(3EoV0r3Z~yG`lU#4_icimO-|za{=5NuVkcWB)lIqhJ zt=`oeyzT1F_ZGW;rQY81BY6Mxqm!Rp7C7Cs+UWnkY@gFs%hk=p>sM?qF55BbX=4~u zuD$o{t2w!kr>0iEFD<&Nu`<9f@3N@wB(cy{{`@r+YA4Uje^0xA*>Fn^Cs@_1?ytZ%;i4 zeSfa&`>T^`e;2I|3NO;Rwy7#Vc-5}@wE1gp++5uMp?t29i!;c~(_ibVKQ_l}0OVzAIi{T|X(j_x^6a^t0lOVSKs&A8fv|Q(xM+ zt}ekT=eP9Q^q45yc=N}rejZCZmaFJ7vrKo+{3XABI9xxjRe9g&Kg9{4g)P?o8sOc3QEs_O&xB3OYyK8VaQ5)cEoJYwIk!vg(0TW2 z^SX=K`#3i6B`rSt=i;MDH!TXUF8auLSoRwG?~o;W;acLc^;V9-SI<>mk>^#He9wBY>dDR?gYEaUf6gg;rEz(4 z;knA+%EwmaGKM{$_Fb|nx&Gxuj~g4mdKdm$`on$(j@#gU)?;DAL~x9D~)RI-}bh4zg^F( zXMB-A8T~iEyJNpTwB%}T?S+4iED<-Kd$pdM{d{}cs(F9SZ<%ehJI`1-S@xaB|J*a) zRa1Xg-?@~~S`z7AZ}qRS|8&y6r*@qsvFGQir)yeA{Eg}Rw$M&0*ZfrMk#%zm{q5$J z>gVX^n#=zYdHR-PLy^PlX8pYUZ&#;vZoR8@;iH@GmAt$oRZDKairw~BQ!@86>M9vpYBg>Pij*-;MB17{%`j0wVeBNgB}*$4vM|{N#S1R>|IydcQr3_-;nt6Vpi-cUF9_6s`u zN_qSGS8;2;yG55;d9J)t+J4r(TB&-Y{9o7mrMbR)=7iX9=l|tzom&>i!V>X2YNf&b z-%@><`zs1BZjyI=m1llEwp)7F-1?gL7KdKWNWZ!5*2*nAo9-;hJ+2eFa`Ck4((2O* ztM5(xwRq9$TOBL*-G6I$b?w$)dmQsyH;25wzW;xQbX#z3pMc$*H?GT1E$N$h+$jFv zzWbdI;;oj3EdLvK(xG8%^Vfr0YS>TRdw=#_w13&neYF==t}gfMb(_Aqxc%_IYu{gO zkBQ5f+dSvnX_hmS`Tgh43%R>)?Uc`EvsHe{JU3eDza%c_&flZKx}mG9*#Gsp?_TTZ zynFMCz`fo+})hC?f=6yzSlp6 z_Iy46B>L2P-XitT*o_w-hCEZb|Bg**vtjXs_qS}1ZfA*jx~<{w%k*RIRbStpbetyl zGnz?PA+m-}Lf*zIiZXdFlTft9xI3UOvt6$e~{k1lAXvmcLkhIotD@uJzw< zIrXKb67mcz5$t>G3)Wv_*O!0y`085E2fIVmODjL^T{?eN-)g_fkNvnMS110TWnFuz z|M(hHOW8%tZ#PNoIcfd<*Uq}nibl&OZ~a`{H~D1LzK08o1J6mzi`aU8?H6>b-u0Ga zZsq>m<;A3gEJCjD*88=ZbGEE;xc~Iu!LJwocz^mF%-#C>w7mNEnLFj9|A#Hi zs@(GY)slPHcWSj%t$6k4(Ze@~rr&m(=v!{NrvKZgcY-ZXFMivw`oa6Rix=Omk<6G{ z7k9({N6D|9F)G(RUztx+lV4ckW&i({zS#RM^&2!mDgF9ycK!J4`Copl|7>cRcR#B3 zv1JtR?$Y{V{qogEn@)fG&F;VJo%|i1^z-?@i?{7=IGyqJ%{$9e(f+(%r)MO8yb){= zAN=p)vu|tr&7AwoHm`e~a_0BE=9^oK^-HZ*xt?-;96ZPP#lM3)zdEu+{I31?AUj&V z=F@@s=T9!LGrYE8^XF?-H!}A#tz^`HIa&L}b5GBo7nCIfc&B^JFkf+gbB9N$ zhxh!{zws*_sN!5-wLYu%!a<#fuIISb@7CmZL^WYt_8`qDIi{a4*c z75U(Mw)Zb@_*vGQ@9BBZ{&j}=Z|A^M_T7M#c7QFf)HUWuH|GKLgX6=~3&p z{D0lN`Qo|NdS5*&<;#k`-T9SV`Y+??v6KZ|UmO>G_{sd6UEY&-pLb<_d31c)3<0fN zyFdF?Hcz=}ta|U+?!E8XKV7nji`jZ7=vQuj*O@wQjtyl`?`Gezx5&JHVRh-Ah*z7R zr#)UT5qI_dzSIlCIbz9vPs3xk1wYlAY3Kgn(!9zj0fu0JB_DFGIIUjBoZ-!#d61?58u&k8|!Vn|o=? z+1i}lAshc(J#}LKgjD`Zslh#&b6X^o-6P(`Jv{0y@%B^ZUdvmT&E|h&j+B3MVdsL+ zYdz<#iM_J-og43}Um{L2e$A0q6_15d=I~s*Z5IE(c1}q-n{J+MU7xV1|0{#nJ1z%> zdx~kQ?u@a%U%upb#Z9{dR(F4@FNu2n^v3?hkB!_S=XzWGn4FfpCGa6jdvD*I`1kLoUG*>TQ`;LDy7xR_y8SAz`*haYyBDLP9vx?? z-76NT=~(+@(mJaGx%;_FQ`^_)MrMDFoL9PKGP`iddCObZe>I-e__@lqs`t;%RDt-Q zfBpV!8(mI#hrK$|JpJfF{goF#J!)F`#$EJ(Yd;K;LTF9|8iZR%{ zaQ=PmLi^@M5k@hFCC;4n?Q*M%RckpmFg#fJ;#KhPSF0B>bZZo`H{{qac;vi1O;tSC zFU+BVmBDhts&AW9uC3g8>yw0wKmwz~wdz0fwyw{Lx4dr10x~O4b(c+3=B35Cucm$x zWzu0NTH*VAwN-rSoH=Uh&U3OWr+&Km)wO}ufiL0Y`<%x=PP}q&FufsZ-#tZNj9oB+ zv4CUQ$($*Zh2Fb|&ljHSDwx11;C_eak=E1h7hH^D43;y#9-M4=Ib3z-@|vq#WI-ZJ z{1!?Y7nUwewg%aj@Va@qvlH`T2Z) zzi(^)y_~v_d+lvnUVnbF-sZ&stusBxr`PtdT zXS#A+nsIk#y=2t;q;!Dkf$GtS^>(|ep1fAw*de9U5IFyWf0)DieXs9@=h^MtZnwd@ z=H@g17=8}3|9dKXwrbgD39T_%&s+cLl*xXJqHvB43~`Akz6P*<*=v92&!+Z8+wPIwhmvX+t2oQU+9WZVyW24xwc)bU+({=cBg51@!u*uE*f8n zsH?vH;?+V&yH8X7)#k6cEGVhd5PpYM>+zS)8@Ek*as*u1xD$F_c(IoJzxAQ*<7?&X zpA){93Q6iTOusFid7S@!o8e8yw!GI{bV8q3@* zlIAV8)K3julk0zRZTwFici;P`r}0*FE0zf|m1N6bNYB1~ZU3sz)AugW-u!cF^!eO7 rzoOqSV}BE{U1C5I68$W_G4ntF&Bvdn?mqm`1Z1nHtDnm{r-UW|B;ymD literal 0 HcmV?d00001 diff --git a/assets/boot-stock.png b/assets/boot-stock.png new file mode 100644 index 0000000000000000000000000000000000000000..f2a731d5443e84dbf4dd9952237a606199c0023e GIT binary patch literal 6527 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV0^&A#K6FCDRtf=1_nttPZ!6Kid%2*Zp@eV z+@}BF{+r+nY-S4@su?vS4)B;gNHHqVPhjVdU^kyp@c+Pf^*se~&(6NdnbOFZ7(1 zGG$NZ|MFY^V>CP`MHbDS@=csU;lLwzi_%#=lOCIG$$G~g;;*6kIpMX|PQSEAx(osd zvdaRE@|;%(rn+qP+)z3(X6LB`4(pp9FZ_Ql-+O(N(MOIA5()FHJnyNhA7%~ZpSd(f z<#NiiUCg#Vlj?KoPt9_V+B~K57T?U?vv41D+x%>9W z)*1VrY5rU}#fC}enw6)W%3_nV#=HsBist#WMmnr|E%`4pUV7F-Izv9a(o1Yx(b!;eokYD%T zdgc<#Qy1-ep5HJ2vi;7V@=wdFnRA}mPrg`pCc*RHti}7EzOXR=|29^TF^sPw^27eJ zxWcKuL6PRFKjq6gZamQaXg_K3Ud|1-0%iZ+wP|B`ov?UyfAhEa6BAB;TYOn2_W7}Q zP72lg?S6c)DHA^W-eux%#md<=HScC!$hlznI{4GoyG(N5&iiHlh&*-c8<+39FA;Jn zlm3LCYW;m=nm5DAc_~-y-_({&UE=v!&$WT|$wQw%EB>w43zEw!snI`R{e#)8+26uF|M~n=|2Zp0<>cAfcTDC!+p<{ZZ*|_k4eKYINZ6b8zB+uXzSQfp zw-&3My2NIG> zpFFANja2g4`=Ysb6sK`~mhxV2rzRV8NA;)jQ}4WUx8GHUcD|eapK9Mh%ue~x1Rb;sN_20oF%RAAJ*1V2Rdi+fBK$UOZ zx@R|zp;PkZRgU;d2|0qeTkWC`~8yV z=f_j}tryo$HhCK$p7?ruS&*Lf{&{Eb-jAts(&^t|^ZKaF-EC+78XmfM>T{02oAht? z`5J%kTxNK!Q)wTN^jhg@yq!Fg&ZgBaRd0PtcAgb@_*Y>4JoVe>>*lH~&lP>!mcDP= zluLFc>nERnlGWaSTW#k28QVlca<5$5y2broT;5vlM|)z@wwP=?yZ(7n&h*_}@nYeh zeEToP<^O!J)A8HYxV=x-)CBLZFW5GnG0ZsQJ=@c@Ogd%hF*EhFPW}us2)U(PSe0h+ zjDKx;EdSp9d!je|_dXl{DfMy79UBSR^0yZEx5S%z-BU|{`#^uv%WFy30*&&H-}&kC zc3Ex4+^s)PXa2IYpY^TE&#E*&TJFZ5WzV-wEt{G5PHsaqb) zKGWLwa@HZT3v=NtKzs(v?Q{+jnH&y6%5-=+WPl6^e1sQm;?7yZ-OsJjSrdm3{L3 z;otOXRSzT`@BI<9v!(v|{MSorU-Z3GId=LM)09miKEb;KzMeQ!aZh=!UA0zaL6J1C z$=NdTtl5=KPqdzHzxVlphUM!_K{1R=lmsHeGXnDeq>n z#j?4T@!{up-&aXLnR3B9YTJa1C;4WdyRx32Z@!b=JFA6f-?2wD-@S3)_A^UFO8d*z zt7g8qmvrZA^ZVSh8?58y_C8F}Hbnd&S)oGrdne*1?pT0hI`ESpsH72zWPWe<_Vf)G-qjoa$)bmqU zPMaA?-b|>!Jo)Rbw+AlSod0?za#rD~rQS*W zCvQK0_s!hvv!UgE3;Qj5)89^JJlH>jY}%_<$td)*%RB~`ps-&Tv*&n z@vot~!-K-2PhS6)H}Bo+&!v`AC!KvY=iEHyT~SB$99^N2J{$i0@*-n<$?QcIVDtwY%qTK0H;2 ze~ZwHzdCEfHeWra{rS_q*H5w*FLAQm=^GxRqC3k(UjE02o=Ndp@9MT+oOf*xn@Ogf z+J3ta8GklxH4a^-Z>b$W`)=f1_OkoG(+u-ta=sn65=rxjS3GbkPx>RT!P~g))y$i3 zPd&K$-dxf5S10%Wat&X)_Q?$0)UWrJnB@LHV;8w`v%CDmeCt%BXMeY^n=X}Qa}9(UNmwf9G+kGZ^G=n4J$v#71V!Ko*sP8tS{=`+LDT_N&M3H{~pa){p-j%_v*Uc z$2a?!$>yrPwtK4{YajU3&-Z*b_Xa=fP_?Pg?NX{f`hUuL{C(Zw$j`0~Q=MV^d zci6N`ALm>$DW9B}KDF`I8lzpizHg16RCuZ_+0N7NottjxV%@g^-u)Nlmgh()_FkT| zUW9w!YWsUxll@F<_Z`oJ8U$FO$N@3~0 zknq-XzuDDetxv?~{JAho_4CL4dD$oL{+qeDRQ2ap<^3K%KV4~fzMAv@i;Ka9Av+6f z;yG=-XKs7W5y~39__lHPth4*?ehk{Splr+Oy@}R6+jjPDem(2N`LJ7`!)gQUWe+HC zTlv1Hz_cXw(u(YpN4~7&<*l6+5Oa)k2FHd?`+u{`SDxQ}uW0}3OUDd1Ka7i8|L)u3 z`6ZQ_Z$4d^m9Fn~DzbL#WtE?o^3Cq6?20-PkuI&?vT4(+E4qQ^@BItk-`Mxm^-TWH zjkA6(+skx3SF|#CvhI&E^V+K=UfO+GZo4gFt3%>nhy7B`wwHHfTI`&D_n>nB=5?JP zKCj&?z5j)2JWB-o#*1mcG73+oSQlQ+dCc5qy@vnWN{_kgL(c8`m+|~$#$&BHI#;Kg zXvc<{%m2)I{31(`yYH1&^*fd|w%)(P`6Xq(EiNtnzUEl1+}o?OuH9StO7#2HE!!ra zNWQf4b(QBi^E}2|Zw0bnH*;**wEcB+_IG|g{~+H}MS6;R!>eG$yx-4#bYn!;|oW4nQx>2jTPMP!6-dlcJ z{pG4>4A1zU3u)!MH%z@d&;AC_k9p<#`}fM^$gaQg!sPWeJFn;yMRzlGlSB5`e^~YU z`Z@I@F9leCZ|*%k=lV3O(6`F7ul847`6^tr>EQW)F@nC4miC2vzAxVU;-bgrf=%L+ z8@IWZ&kM0H>$;N8|NfTblCFAbUk!nTiO0idZ7c6{iM(ZBuWyvGEm*n!*5dsc!S?#` z{D#l+>?>Z*+PCWNg7To7g0G5RR_(j_U}gPZne>Y>Yps7&ZaEkpTekMHRLz#H-~awH zST)-^h`Y%e?dWYek#{o^YiP^hS>i0-;$S9JCFI3oAsTE|9+e4 zzYKl%zVI}g_L9$5zb?O(|6!s3Z1(%f9@oF#$t;?-srl;ppBeJ676h7Gknvz?8He{xX zFP;4D_|Nd$%3n&jj|-ozfA#vwrcYYl0g zcH-^uT>Wd+i*v2!E(y7{>+o6kYNhIp@_$|Lm*x7}%nz}@%KmrxTr=zQOiXKP*M`h^ zSIc{RNBka()oKd1j`|rx;zq%GxyUQ`Zb+gIo>-+y_nDZ?EC8JnlakJZRs;8`Sf91YE zvHOJ&+LwE+?EAfUQX@k)`|IYFHSDMEy+8Xd+P^IF{=SSU&yttBE&hCS=i$Wq)74+K zr;D#Hp76MCW<%zue=9Ffo^^Zc)S|e}o{P>G>Mj1}^6KWPyYp?Y25rqOfAGFy%WEl< zD|20CcbBzw+uT#LS67eacX@v~X#dMEUTL)gjA3ziel}N|Ztr`=*A@AH_T*XXo_0&T zo`3TCl>25U=9=Ulb!!ivIk{%9bcfHjnawrP-$mku6Qt-^u?uu>5FPU+k&&@jT_wl&3F7xffj|mvRZg$>t=+Kys{TKSTgxA3cjx%(=*b7RXZqdx^rYI`-d8$&uG;arVn$+z|LKU%w%1?c0grySkq&`S)g) zYu@ABPh~4Be%?z8yqur4Z~L->pvcC3r~fveUikC*lj+OFxTa6H_scUbw%_(o*Xin~ zjPq9mcdsuF}%=*t-m&# z>p#5wQoPe+-Q+F#I9+s=%ANanaIfvxqJ7W5BrVV^x%5p_u~1{ zg8b#mzu5&7_O^uY4c}^45%u$v)m@7nKPR2}m1j;%Vhw{p!rm)I zOaI2p&&hea=hdZoQ@+=!r=Qog4_n?Jmea$w%0XU*Yrfrv`VSMYJh(ljynOrD)OOB= zL7yM~H=Vh}XP)Lqv%7C=?I%n>_V#G#XWgao-tEtwZ>n43HX3>+iJO9~w)9;Jx)cW1O@$+r3FE~)Y z`Lkbg^jq_=Xb!^%iq|+coM>B}87ldB`lk8~Icul>+)d?bTPbhXye-{+b-^ls#Ul?*W-M)s8^&Z)}$@!V{5Ygu~xW?uK* zSAYL2dcLJx(yVLO--L9{w|g!2?74k&;@`iU?W%uepSs?_(7opY)9q(@-KW!5-n|$U z_2@WD?Ou^UO~=|Nlk}_#JMF8=3Vra$f0{#q7c%=Phqt|J8Uh>{Dp<7pb4# zsRHpq|N8yeHoBbf4!d=vdHT_V`YSJfdepS=jl1ao)_zw0-`(B}4t#d@;q~9#Wcr&K zxEbWW?AvGhe@EEF~YsoL)V}f1N&)4nq-xcU`*nad$I&rZor8{g}D5 zW}8I^19yY7fuCBmrRK-FlCPQ#i**ifwdpRBS*(V1uyz zP8J56C%?bH_pJN%ayj4s_r<+gU;XdZf3p4Bdj4zi$5}EvSs3>0n6bWC=G))j-{0Tg zZ!ct%`|tZ<<$sTa7zz|FUVT(9_p90^oMQvShPa=T{?)NWFevP5T5*sm!GHRPX^W@1 z%545IdG_^H;jZ?f^${hl4BQRVr=&zhhy1IUS$RHVCG$ejjV>Gq&e;SB)#=Y!rKMGW ztif$l*NFti0}obDTci!j7>v{Z&wJC%y3vJ$!CAM;DP;r0jYyYGv%XfYnDo+>MK8Nv zNg#pogVD9e3ez|k71poVcGpUQNr&N6(5q=#*0LR?0tt)^3I~`Jc6%plOg{fK zFSL*?;PL`!VHRpxK52i4OW$+7SNuKeN<~pWPQS{11Yx N^mO%eS?83{1OUiqx6l9p literal 0 HcmV?d00001 diff --git a/builder.html b/builder.html new file mode 100644 index 0000000..7687409 --- /dev/null +++ b/builder.html @@ -0,0 +1,110 @@ + + + + + + CDJ-900NXS Mods Builder + + + + + + + + + + + + +
+
+ CDJ-900NXS Mods + + +
+
+ +
+
Builder
+

Build your update

+

Upload the official CDJ-900NXS 1.31 update, tick the mods and download your C900NXS.UPD. Everything runs in your browser: the file never leaves your computer.

+ + +
+
1
+
+

The official update

+

Pioneer DJ's C900NXS.UPD for firmware 1.31. The builder checks its SHA-256 and refuses any other file.

+ +
+
+ + +
+
2
+
+

The mods

+

Ticking a mod also ticks what it needs; unticking one unticks what needs it. Tick none for the stock firmware (relabelled so the player takes it).

+
+

+
+
+ + +
+
3
+
+

Before you build

+
+ Unofficial firmware. Flashing modified firmware can leave your player unusable. The boot loader and the recovery stay untouched, so an official update can still be run, but there is no guarantee. +
+ + +
+
+ + +
+
4
+
+

Build

+ + + + +
+
+
+ +
+
+
+
Made with Claude Code · Claude Opus 5.5
+

This website, the builder, our mods and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods extended_bpm, phasemeter and rgb_waveform are by arnoldasr.

+
+
+

Disclaimer

+

Unofficial. Not affiliated with or endorsed by Pioneer DJ or AlphaTheta; CDJ and rekordbox are their trademarks. Flashing modified firmware can leave your player unusable. Everything here is provided as is, without warranty, and used at your own risk.

+
+
+
+ + + diff --git a/builder/builder.js b/builder/builder.js new file mode 100644 index 0000000..fcb6696 --- /dev/null +++ b/builder/builder.js @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// CDJ-900NXS mods builder, in the browser: the official 1.31 C900NXS.UPD in, +// a new one with the chosen mods out. Nothing is uploaded or downloaded. +// +// Ports of the patcher's upd.py (update file), the emulator checkout's +// lzss_encode.py (packer) and patch_cdj900nxs.py's asset blob; the mods' +// bytes are in builder/data.js (tools/make_builder_data.py), with the +// SHA-256 of the finished application for every combination. +(function () { + 'use strict'; + var D = window.BUILDER_DATA; + var BASE = 0x08000000; + var APP_START = 0x40000, APP_END = 0x7E0000, RECORD_DATA = 32; + var ADDRESS_BYTES = { 0: 2, 1: 2, 2: 3, 3: 4, 5: 2, 6: 3, 7: 4, 8: 3, 9: 2 }; + var LABEL = '1.32'; + var UPSTREAM = ['extended_bpm', 'phasemeter', 'rgb_waveform']; + var OURS = ['cuecolours', 'hotcuekeys', 'ledsaver', 'credit']; + + function fail(msg) { throw new Error(msg); } + + // ---------- bytes ---------- + function hex(u8) { + var s = ''; + for (var i = 0; i < u8.length; i++) s += (u8[i] < 16 ? '0' : '') + u8[i].toString(16); + return s; + } + function fromHex(s) { + var out = new Uint8Array(s.length >> 1); + for (var i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16); + return out; + } + function fromB64(s) { + var bin = atob(s), out = new Uint8Array(bin.length); + for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; + } + function equal(a, b) { + if (a.length !== b.length) return false; + for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; + } + function u16(b, o) { return b[o] | b[o + 1] << 8; } + function u32(b, o) { return (b[o] | b[o + 1] << 8 | b[o + 2] << 16 | b[o + 3] << 24) >>> 0; } + function put16(b, o, v) { b[o] = v & 255; b[o + 1] = v >> 8 & 255; } + function put32(b, o, v) { b[o] = v & 255; b[o + 1] = v >> 8 & 255; b[o + 2] = v >> 16 & 255; b[o + 3] = v >>> 24 & 255; } + function grow(b, size) { + if (b.length >= size) return b; + var out = new Uint8Array(size); + out.set(b); + return out; + } + function latin1(u8) { + var s = ''; + for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]); + return s; + } + function ascii(s) { + var out = new Uint8Array(s.length); + for (var i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); + return out; + } + function sha256(u8) { + return crypto.subtle.digest('SHA-256', u8).then(function (h) { return hex(new Uint8Array(h)); }); + } + + // CRC-16/XMODEM (Python's binascii.crc_hqx with 0) + var CRC_TABLE = (function () { + var t = new Uint16Array(256); + for (var i = 0; i < 256; i++) { + var c = i << 8; + for (var k = 0; k < 8; k++) c = c & 0x8000 ? (c << 1 ^ 0x1021) & 0xFFFF : c << 1 & 0xFFFF; + t[i] = c; + } + return t; + })(); + function crc16(u8, start, end) { + var c = 0; + for (var i = start; i < end; i++) c = (c << 8 & 0xFFFF) ^ CRC_TABLE[(c >> 8 ^ u8[i]) & 255]; + return c; + } + + // ---------- the update file (upd.py) ---------- + var HEADER = /^CDJ-900NXS (DRIV|MAIN|PANL)Ver(\d\.\d\d)/; + + function parseSection(blob) { + var n = blob.length; + if (n < 36) fail('a section is truncated'); + if (crc16(blob, 0, n - 2) !== u16(blob, n - 2)) fail('a section fails its CRC'); + var header = blob.subarray(0, 32), m = HEADER.exec(latin1(header)); + if (!m) fail('unexpected section header'); + var sec = { blob: blob, header: header, name: m[1], version: m[2], records: [] }; + var end = n - 2; + if (blob[end - 2] !== 13 || blob[end - 1] !== 10) fail(sec.name + ': the S-records do not end in a line break'); + var pos = 32, line = 0; + while (pos < end) { + var e = pos; + while (!(blob[e] === 13 && blob[e + 1] === 10)) e++; + line++; + if (blob[pos] !== 0x53 || e - pos < 4 || (e - pos) % 2) fail(sec.name + ': S-record ' + line + ' is malformed'); + var kind = blob[pos + 1] - 48; + if (!(kind in ADDRESS_BYTES)) fail(sec.name + ': S-record ' + line + ' has type ' + kind); + var len = (e - pos - 2) >> 1, raw = new Uint8Array(len), sum = 0; + for (var i = 0; i < len; i++) { + var h = blob[pos + 2 + i * 2], l = blob[pos + 3 + i * 2]; + var v = (h <= 57 ? h - 48 : h - 55) << 4 | (l <= 57 ? l - 48 : l - 55); + raw[i] = v; sum += v; + } + var width = ADDRESS_BYTES[kind]; + if (len !== raw[0] + 1 || len < width + 2 || (sum & 255) !== 255) fail(sec.name + ': S-record ' + line + ' fails its length or checksum'); + var addr = 0; + for (var k = 0; k < width; k++) addr = addr * 256 + raw[1 + k]; + sec.records.push({ kind: kind, address: addr, data: raw.subarray(1 + width, len - 1) }); + pos = e + 2; + } + if (sec.records[0].kind !== 0 || [7, 8, 9].indexOf(sec.records[sec.records.length - 1].kind) < 0) fail(sec.name + ': no S-record header or end'); + return sec; + } + function dataRecords(sec) { return sec.records.filter(function (r) { return r.kind >= 1 && r.kind <= 3; }); } + function flashOf(sec, size) { + size = size || APP_END; + var img = new Uint8Array(size).fill(0xFF); + dataRecords(sec).forEach(function (r) { + if (r.address + r.data.length > size) fail(sec.name + ': a record reaches past 0x' + size.toString(16)); + img.set(r.data, r.address); + }); + return img; + } + function parseUpd(raw) { + var pos = 0, sizes = []; + for (var s = 0; s < 3; s++) { + var e = pos; + while (e < raw.length && !(raw[e] === 13 && raw[e + 1] === 10)) e++; + var t = latin1(raw.subarray(pos, e)); + if (!/^\d+$/.test(t)) fail('the file does not start with three section lengths'); + sizes.push(parseInt(t, 10)); + pos = e + 2; + } + if (pos + sizes[0] + sizes[1] + sizes[2] !== raw.length) fail('the section lengths do not add up to the file'); + var secs = sizes.map(function (n) { var b = raw.subarray(pos, pos + n); pos += n; return parseSection(b); }); + if (secs.map(function (x) { return x.name; }).join() !== 'DRIV,MAIN,PANL') fail('the sections are not DRIV, MAIN, PANL'); + return secs; + } + + function unpackLzss(stream) { + var ring = new Uint8Array(4096).fill(0x20); + ring.fill(0, 4078); + var r = 4078, out = new Uint8Array(Math.max(1 << 20, stream.length * 3)), o = 0, pos = 0, n = stream.length; + function room(k) { if (o + k > out.length) { var b = new Uint8Array(out.length * 2); b.set(out); out = b; } } + while (pos < n) { + var flags = stream[pos++]; + for (var bit = 0; bit < 8; bit++) { + if (pos >= n) break; + if (flags >> bit & 1) { + room(1); + var b1 = stream[pos++]; + out[o++] = b1; ring[r] = b1; r = r + 1 & 0xFFF; + } else { + if (pos + 2 > n) fail('the packed application ends inside a match'); + var lo = stream[pos], hi = stream[pos + 1]; + pos += 2; + var at = lo | (hi & 0xF0) << 4, count = (hi & 0x0F) + 3; + room(count); + for (var k = 0; k < count; k++) { + var b2 = ring[at + k & 0xFFF]; + out[o++] = b2; ring[r] = b2; r = r + 1 & 0xFFF; + } + } + } + } + return out.slice(0, o); + } + function appRegion(flash) { + var size = u32(flash, APP_START), end = APP_START + 4 + size; + if (end + 2 > APP_END) fail('the packed application runs past 0x' + APP_END.toString(16)); + var sum = 0; + for (var i = APP_START; i < end; i++) sum += flash[i]; + if (u16(flash, end) !== (sum & 0xFFFF)) fail('the packed application fails its additive checksum'); + return { app: unpackLzss(flash.subarray(APP_START + 4, end)), region: flash.subarray(APP_START, end + 2), + stream: flash.subarray(APP_START + 4, end) }; + } + + // The official update, checked to be exactly the 1.31 file. + function readStock(raw) { + return sha256(raw).then(function (h) { + if (h !== D.stockUpdSha256) fail('This is not the original CDJ-900NXS 1.31 update file (C900NXS.UPD). Its SHA-256 is ' + h + '.'); + var secs = parseUpd(raw), main = secs[1]; + if (main.version !== '1.31' || main.header[31] !== 0x30) fail('MAIN is not 1.31 in application-only mode'); + main.records.forEach(function (r) { if ([0, 2, 7].indexOf(r.kind) < 0) fail('MAIN has unexpected record types'); }); + var flash = flashOf(main), reg = appRegion(flash); + return sha256(reg.app).then(function (ah) { + if (reg.app.length !== D.appSize || ah !== D.stockAppSha256) fail('the 1.31 application inside does not match'); + var boot = dataRecords(main).filter(function (r) { return r.address < APP_START; }); + boot.forEach(function (r) { if (r.address + r.data.length > APP_START) fail('a stock record crosses into the application'); }); + return { raw: raw, sections: secs, flash: flash, app: reg.app, stream: reg.stream, boot: boot }; + }); + }); + } + + function encodeRecord(r) { + var width = ADDRESS_BYTES[r.kind], body = new Uint8Array(width + r.data.length + 2), sum = 0; + body[0] = width + r.data.length + 1; + for (var k = 0; k < width; k++) body[1 + k] = Math.floor(r.address / Math.pow(256, width - 1 - k)) & 255; + body.set(r.data, 1 + width); + for (var i = 0; i < body.length - 1; i++) sum += body[i]; + body[body.length - 1] = ~sum & 255; + return 'S' + r.kind + hex(body).toUpperCase() + '\r\n'; + } + function relabel(header, version) { + var s = latin1(header), m = HEADER.exec(s); + var old = m[0], neu = old.slice(0, -4) + version; + var out = s.replace(old, neu); + if (out.slice(0, old.length - 4) !== s.slice(0, old.length - 4) || out.slice(old.length) !== s.slice(old.length)) fail('relabelling would change more than the version'); + return ascii(out); + } + function encodeSection(header, records) { + var parts = [header]; + var text = ''; + for (var i = 0; i < records.length; i++) { + text += encodeRecord(records[i]); + if (text.length > 1 << 20) { parts.push(ascii(text)); text = ''; } + } + parts.push(ascii(text)); + var n = parts.reduce(function (a, p) { return a + p.length; }, 0), body = new Uint8Array(n + 2), o = 0; + parts.forEach(function (p) { body.set(p, o); o += p.length; }); + put16(body, n, crc16(body, 0, n)); + return body; + } + function join(blobs) { + var head = ascii(blobs.map(function (b) { return b.length + '\r\n'; }).join('')); + var n = head.length + blobs.reduce(function (a, b) { return a + b.length; }, 0), out = new Uint8Array(n), o = head.length; + out.set(head); + blobs.forEach(function (b) { out.set(b, o); o += b.length; }); + return out; + } + function packRegion(stream) { + var n = 4 + stream.length + 2, pad = (RECORD_DATA - n % RECORD_DATA) % RECORD_DATA; + var out = new Uint8Array(n + pad).fill(0xFF); + put32(out, 0, stream.length); + out.set(stream, 4); + var sum = 0; + for (var i = 0; i < 4 + stream.length; i++) sum += out[i]; + put16(out, 4 + stream.length, sum & 0xFFFF); + return out; + } + function buildUpd(stock, stream) { + var main = stock.sections[1], reg = packRegion(stream); + if (APP_START + reg.length > APP_END) fail('the packed application needs ' + reg.length + ' bytes, the flash has ' + (APP_END - APP_START)); + var records = [main.records[0]].concat(stock.boot); + for (var off = 0; off < reg.length; off += RECORD_DATA) records.push({ kind: 2, address: APP_START + off, data: reg.subarray(off, off + RECORD_DATA) }); + records.push(main.records[main.records.length - 1]); + var blob = encodeSection(relabel(main.header, LABEL), records); + return join([stock.sections[0].blob, blob, stock.sections[2].blob]); + } + function sameRecord(a, b) { return a.kind === b.kind && a.address === b.address && equal(a.data, b.data); } + // Every check on the finished file, as upd.verify(). + function verify(raw, stock, app) { + var done = [], secs = parseUpd(raw); + done.push('section lengths, CRC-16 of every section, every S-record checksum'); + if (!equal(secs[0].blob, stock.sections[0].blob) || !equal(secs[2].blob, stock.sections[2].blob)) fail('DRIV or PANL differs from stock'); + done.push('DRIV and PANL byte for byte as stock'); + var main = secs[1], smain = stock.sections[1]; + if (!equal(main.header, relabel(smain.header, LABEL))) fail('the MAIN header differs beyond its version label'); + if (main.header[31] !== 0x30) fail('the MAIN header no longer selects application-only mode'); + done.push('MAIN header as stock but for the label ' + LABEL + '; application-only mode kept'); + if (!sameRecord(main.records[0], smain.records[0]) || !sameRecord(main.records[main.records.length - 1], smain.records[smain.records.length - 1])) fail('the S0 header or S7 entry record changed'); + done.push('S0 header record and S7 entry record as stock'); + var low = dataRecords(main).filter(function (r) { return r.address < APP_START; }); + if (low.length !== stock.boot.length || low.some(function (r, i) { return !sameRecord(r, stock.boot[i]); })) fail('the boot loader or recovery records changed'); + var flash = flashOf(main); + if (!equal(flash.subarray(0, APP_START), stock.flash.subarray(0, APP_START))) fail('the boot loader or recovery bytes changed'); + done.push('boot loader and recovery (flash 0x0-0x3FFFF): every record and byte as stock'); + if (dataRecords(main).some(function (r) { return r.address < APP_START && APP_START < r.address + r.data.length; })) fail('a record crosses into the boot area'); + done.push('no record reaches past 0x7E0000 or into the boot area'); + var reg = appRegion(flash); + done.push('packed application: additive checksum'); + if (!equal(reg.app, app)) fail('the update does not unpack to the patched application'); + done.push('unpacked with the boot loader\'s LZSS: exactly the patched application'); + done.push('flash used by the application: ' + reg.region.length + ' of ' + (APP_END - APP_START) + ' bytes'); + return done; + } + + // ---------- LZSS packer (lzss_encode.py), run in a worker ---------- + function lzssPack(data) { + var RING = 4096, START = RING - 18, MIN = 3, MAX = 18, DEPTH = 48; + var n = data.length, total = START + n; + function cb(i) { return i < START ? 0x20 : data[i - START]; } + var head = new Int32Array(1 << 24).fill(-1), prev = new Int32Array(RING); + function key(p) { return cb(p) << 16 | cb(p + 1) << 8 | cb(p + 2); } + function insert(p) { + if (p + 3 <= total) { var k = key(p); prev[p & (RING - 1)] = head[k]; head[k] = p; } + } + for (var p0 = 0; p0 < START; p0++) insert(p0); + var out = new Uint8Array(n + (n >> 3) + 64), o = 0; + var flagAt = o++, mask = 1, items = 0; + out[flagAt] = 0; + var k2 = 0; + while (k2 < n) { + var a = START + k2, limit = Math.min(MAX, total - a), bestLen = 0, bestPos = 0; + if (limit >= MIN) { + var lo = a - RING, p = head[key(a)], checked = 0; + while (p >= 0 && p >= lo) { + var len = 0; + while (len < limit && cb(p + len) === cb(a + len)) len++; + if (len > bestLen) { + bestLen = len; bestPos = p; + if (bestLen >= limit) break; + } + if (++checked >= DEPTH) break; + p = prev[p & (RING - 1)]; + } + } + var step; + if (bestLen >= MIN) { + step = bestLen; + var off = bestPos & (RING - 1); + out[o++] = off & 0xFF; + out[o++] = (off >> 4 & 0xF0) | (step - MIN); + } else { + step = 1; + out[flagAt] |= mask; + out[o++] = cb(a); + } + for (var t = 0; t < step; t++) insert(a + t); + k2 += step; + items++; + mask = mask << 1 & 0xFF; + if (mask === 0) { + mask = 1; + if (k2 < n) { flagAt = o++; out[flagAt] = 0; } + } + } + return out.slice(0, o); + } + function packInWorker(app) { + return new Promise(function (resolve, reject) { + var src = lzssPack.toString() + '\nonmessage = function (e) { var o = lzssPack(new Uint8Array(e.data)); postMessage(o.buffer, [o.buffer]); };'; + var w; + try { + w = new Worker(URL.createObjectURL(new Blob([src], { type: 'text/javascript' }))); + } catch (e) { + setTimeout(function () { try { resolve(lzssPack(app)); } catch (x) { reject(x); } }, 30); + return; + } + w.onmessage = function (e) { w.terminate(); resolve(new Uint8Array(e.data)); }; + w.onerror = function (e) { w.terminate(); reject(new Error('packing failed: ' + e.message)); }; + var copy = app.slice(); + w.postMessage(copy.buffer, [copy.buffer]); + }); + } + + // ---------- arnoldasr's asset blob (patch_cdj900nxs.assets) ---------- + var TABLE = 0x08100000, DESCRIPTOR = 0x2C, ORIGINAL_COUNT = 0x2A1, TEMPLATE = 0x1C8, DATA_BASE = 0x08FC2D00; + var WIDTH = 200, HEIGHT = 24, MARKER_WIDTH = 8, BAR = 5, KEY = 0xF81F; + var ORANGE_LINE = [120, 65, 20], ORANGE_MARK = [255, 140, 0], BLUE_LINE = [40, 90, 120], BLUE_MARK = [60, 170, 255]; + var WAVE_COLOURS = [ + 448, 457, 47, 458, 449, 111, 495, 466, 175, 467, 487, 239, 303, 479, 367, 459, 456, 431, + 468, 465, 475, 476, 474, 39, 477, 31, 471, 46, 469, 45, 23, 460, 109, 484, 110, 485, 103, + 478, 483, 493, 494, 486, 95, 15, 450, 167, 492, 173, 174, 231, 115, 470, 238, 44, 237, 159, + 295, 87, 108, 302, 51, 301, 491, 482, 359, 223, 366, 365, 236, 300, 179, 423, 461, 287, 430, + 429, 172, 364, 473, 428, 351, 114, 151, 463, 299, 363, 415, 215, 427, 178, 490, 50, 279, 235, + 79, 462, 451, 343, 511, 298, 362, 400, 52, 116, 426, 234, 407, 7, 143, 180, 207, 113, 233, + 243, 481, 57, 271, 452, 417, 177]; + var WHITENESS = [[150, 20, 20], [220, 40, 30], [255, 110, 20], [255, 190, 30], [170, 230, 60], + [40, 210, 170], [60, 160, 255], [225, 240, 255]]; + function rgb565(c) { return (c[0] >> 3) << 11 | (c[1] >> 2) << 5 | c[2] >> 3; } + function lineImage(colour) { + var px = new Array(WIDTH * HEIGHT).fill(KEY), c = rgb565(colour); + for (var x = 2; x < WIDTH - 2; x++) px[12 * WIDTH + x] = c; + for (var i = 0; i < 5; i++) { + var xx = 2 + Math.floor(i * (WIDTH - 5) / 4); + for (var y = 7; y < 18; y++) px[y * WIDTH + xx] = c; + } + return px; + } + function markerImage(colour) { + var px = [], c = rgb565(colour); + for (var y = 0; y < HEIGHT; y++) for (var x = 0; x < MARKER_WIDTH; x++) px.push(y >= 3 && y <= 21 && x < BAR ? c : KEY); + return px; + } + function rgb9(code) { return [6, 3, 0].map(function (s) { return Math.floor(((code >> s) & 7) * 255 / 7); }); } + function assets(app) { + var images = [[WIDTH, lineImage(ORANGE_LINE)], [WIDTH, lineImage(BLUE_LINE)], + [MARKER_WIDTH, markerImage(ORANGE_MARK)], [MARKER_WIDTH, markerImage(BLUE_MARK)]]; + var t0 = TABLE - BASE, count = ORIGINAL_COUNT + images.length, tableBytes = count * DESCRIPTOR; + var pixelsBase = DATA_BASE + ((tableBytes + 31) & ~31); + var template = app.subarray(t0 + TEMPLATE * DESCRIPTOR, t0 + (TEMPLATE + 1) * DESCRIPTOR); + var pixLen = 0; + images.forEach(function (im) { var l = im[1].length * 2; pixLen += l + (32 - l % 32) % 32; }); + var blobLen = (pixelsBase - DATA_BASE) + pixLen, palette = 128 * 2, lut = 512; + var blob = new Uint8Array(blobLen + palette + lut); + blob.set(app.subarray(t0, t0 + ORIGINAL_COUNT * DESCRIPTOR)); + var pix = pixelsBase - DATA_BASE, pl = 0; + images.forEach(function (im, i) { + var e = ORIGINAL_COUNT * DESCRIPTOR + i * DESCRIPTOR; + blob.set(template, e); + put16(blob, e + 4, im[0]); put16(blob, e + 6, HEIGHT); + put32(blob, e + 0x1C, (ORIGINAL_COUNT + i) * DESCRIPTOR); + put32(blob, e + 0x20, pixelsBase + pl - TABLE); + im[1].forEach(function (v, j) { put16(blob, pix + pl + j * 2, v); }); + pl += im[1].length * 2; + pl += (32 - pl % 32) % 32; + }); + var paletteAt = DATA_BASE + blobLen; + var colours = WHITENESS.concat(WAVE_COLOURS.map(rgb9)); + colours.forEach(function (c, i) { put16(blob, blobLen + i * 2, rgb565(c)); }); + for (var code = 0; code < 512; code++) { + var r = code >> 6 & 7, g = code >> 3 & 7, b = code & 7, best = 0, bd = Infinity; + for (var i = 0; i < WAVE_COLOURS.length; i++) { + var w = WAVE_COLOURS[i], d = Math.pow((w >> 6 & 7) - r, 2) + Math.pow((w >> 3 & 7) - g, 2) + Math.pow((w & 7) - b, 2); + if (d < bd) { bd = d; best = i; } + } + blob[blobLen + palette + code] = 8 + best; + } + return { blob: blob, phaseEnd: blobLen, paletteAt: paletteAt }; + } + + // ---------- the mods' bytes (builder/data.js) ---------- + function boxPixels(stock, res, bg, ink) { + var B = D.box, rec = B.resTable + res * 44, w = u16(stock, rec + 4), h = u16(stock, rec + 6); + var off = B.resTable + u32(stock, rec + 0x20), px = new Uint16Array(w * h); + for (var i = 0; i < w * h; i++) px[i] = u16(stock, off + i * 2); + for (var y = 3; y < 13; y++) for (var x = 3; x < w - 3; x++) px[y * w + x] = bg; + // as box_pixels: width = sum(space for ' ', else glyph width + gap) - gap + var text = B.text, width = -B.gap, c; + for (c = 0; c < text.length; c++) width += text[c] === ' ' ? B.space : B.glyphs[text[c]][0].length + B.gap; + var x0 = Math.floor((w - width) / 2), y0 = 4; + for (c = 0; c < text.length; c++) { + if (text[c] === ' ') { x0 += B.space; continue; } + var g = B.glyphs[text[c]]; + for (var gy = 0; gy < g.length; gy++) for (var gx = 0; gx < g[gy].length; gx++) if (g[gy][gx] === 'X') px[(y0 + gy) * w + x0 + gx] = ink; + x0 += g[0].length + B.gap; + } + var out = new Uint8Array(w * h * 2); + for (i = 0; i < w * h; i++) put16(out, i * 2, px[i]); + return out; + } + // One variant of data.js: the application grown to its size, then its + // operations: our bytes, a copy from the user's own application, the HOT + // KEYS box or arnoldasr's asset blob, both made from the user's application. + function applyVariant(app, stock, v) { + var out = grow(app.slice(), v.size), blob = null; + v.ops.forEach(function (o) { + var off = o[1] - BASE; + if (o[0] === 'data') out.set(fromB64(D.chunks[o[2]]), off); + else if (o[0] === 'copy') out.set(stock.subarray(o[2] - BASE, o[2] - BASE + o[3]), off); + else if (o[0] === 'box') out.set(boxPixels(stock, o[2], o[3], o[4]), off); + else if (o[0] === 'assets') { blob = blob || assets(stock).blob; out.set(blob.subarray(0, o[2]), off); } + else fail('unknown operation ' + o[0]); + }); + return out; + } + + // ---------- the whole build ---------- + function patchApp(stock, names, log) { + var up = UPSTREAM.filter(function (n) { return names.indexOf(n) >= 0; }); + var ours = OURS.filter(function (n) { return names.indexOf(n) >= 0; }); + var app = stock; + if (up.length) { + app = applyVariant(app, stock, D.upstream[up.join(',')]); + log('arnoldasr\'s mods applied (' + up.join(', ') + ')'); + } + if (ours.length) { + app = applyVariant(app, stock, D.ours[ours.join(',')]); + log('our mods applied (' + ours.join(', ') + ')'); + } + var key = up.concat(ours).join(','); + return sha256(app).then(function (h) { + if (h !== D.expect[key]) fail('the patched application is not the one the patcher makes (SHA-256 ' + h + ')'); + log('the patched application is exactly the patcher\'s (SHA-256 checked)'); + return app; + }); + } + + function build(raw, names, log) { + var stock; + log('checking the update file...'); + return readStock(raw).then(function (s) { + stock = s; + log('the official CDJ-900NXS 1.31 update: SHA-256 checked, application unpacked and checked'); + if (!names.length) return { app: stock.app, stream: stock.stream }; + return patchApp(stock.app, names, log).then(function (app) { + log('packing the application (LZSS), this takes a while...'); + var t = Date.now(); + return packInWorker(app).then(function (stream) { + log('packed ' + app.length + ' -> ' + stream.length + ' bytes in ' + Math.round((Date.now() - t) / 1000) + ' s'); + return { app: app, stream: stream }; + }); + }); + }).then(function (r) { + log('writing the update file...'); + var out = buildUpd(stock, r.stream); + var checks = verify(out, stock, r.app); + return Promise.all([sha256(out), sha256(r.app)]).then(function (h) { + return { upd: out, checks: checks, updSha: h[0], appSha: h[1] }; + }); + }); + } + + window.CDJBuilder = { build: build, UPSTREAM: UPSTREAM, OURS: OURS, lzssPack: lzssPack, unpackLzss: unpackLzss }; +})(); diff --git a/builder/data.js b/builder/data.js new file mode 100644 index 0000000..f0a54dc --- /dev/null +++ b/builder/data.js @@ -0,0 +1,2 @@ +// Generated by tools/make_builder_data.py -- do not edit. +window.BUILDER_DATA = {"stockUpdSha256":"e96bca7d1ac983985992a8e5fa7813e1ebf04bef514e0c6cfd0a8375f667f7f9","stockAppSha256":"1ac3e30cde292865413950fe159663b3958e85939ed0a5d0122f796809a5f8ef","appSize":16519056,"dataBase":150744320,"upstream":{"extended_bpm":{"size":16576352,"ops":[["data",145914824,"45b88bcd5afea839"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150739456,"786195378d06baff"],["data",150739521,"503c09b6b0baefb3"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"7010683be462baa0"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"172e933902cb3738"],["data",150742016,"2853d86b697a9dfa"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,49760]]},"phasemeter":{"size":16576352,"ops":[["data",135069542,"845677fae2e67916"],["data",145914824,"45b88bcd5afea839"],["data",145941230,"6922e93e3827642c"],["data",145986008,"84014a74358fe2ac"],["data",145987228,"84014a74358fe2ac"],["data",145988312,"84014a74358fe2ac"],["data",146168552,"84014a74358fe2ac"],["data",146172320,"84014a74358fe2ac"],["data",146173504,"84014a74358fe2ac"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147246288,"060f2c4b6cd4c5e4"],["data",150411792,"845677fae2e67916"],["data",150427952,"845677fae2e67916"],["data",150446592,"845677fae2e67916"],["data",150464904,"845677fae2e67916"],["data",150483144,"845677fae2e67916"],["data",150501528,"845677fae2e67916"],["data",150519852,"845677fae2e67916"],["data",150538228,"845677fae2e67916"],["data",150556534,"845677fae2e67916"],["data",150572646,"845677fae2e67916"],["data",150587026,"845677fae2e67916"],["data",150601380,"845677fae2e67916"],["data",150619274,"845677fae2e67916"],["data",150637628,"845677fae2e67916"],["data",150655862,"845677fae2e67916"],["data",150673666,"845677fae2e67916"],["data",150692276,"845677fae2e67916"],["data",150709976,"845677fae2e67916"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150739456,"786195378d06baff"],["data",150739521,"503c09b6b0baefb3"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"7010683be462baa0"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"172e933902cb3738"],["data",150742016,"acf759afff056d54"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,49760]]},"rgb_waveform":{"size":16577120,"ops":[["data",144311836,"ca71ec9290d6ed48"],["data",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",147243844,"d77596e8499df246"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150738944,"91aaac306bd43135"],["data",150739456,"786195378d06baff"],["data",150739521,"d5326732238d3e24"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"f9699e90e078917d"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"bb010f4579136412"],["data",150742016,"9bed08a38f1020b6"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]},"extended_bpm,phasemeter":{"size":16576352,"ops":[["data",135069542,"845677fae2e67916"],["data",145914824,"45b88bcd5afea839"],["data",145941230,"6922e93e3827642c"],["data",145986008,"84014a74358fe2ac"],["data",145987228,"84014a74358fe2ac"],["data",145988312,"84014a74358fe2ac"],["data",146168552,"84014a74358fe2ac"],["data",146172320,"84014a74358fe2ac"],["data",146173504,"84014a74358fe2ac"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147246288,"060f2c4b6cd4c5e4"],["data",150411792,"845677fae2e67916"],["data",150427952,"845677fae2e67916"],["data",150446592,"845677fae2e67916"],["data",150464904,"845677fae2e67916"],["data",150483144,"845677fae2e67916"],["data",150501528,"845677fae2e67916"],["data",150519852,"845677fae2e67916"],["data",150538228,"845677fae2e67916"],["data",150556534,"845677fae2e67916"],["data",150572646,"845677fae2e67916"],["data",150587026,"845677fae2e67916"],["data",150601380,"845677fae2e67916"],["data",150619274,"845677fae2e67916"],["data",150637628,"845677fae2e67916"],["data",150655862,"845677fae2e67916"],["data",150673666,"845677fae2e67916"],["data",150692276,"845677fae2e67916"],["data",150709976,"845677fae2e67916"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150739456,"786195378d06baff"],["data",150739521,"503c09b6b0baefb3"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"7010683be462baa0"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"172e933902cb3738"],["data",150742016,"9e7532da7f56183c"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,49760]]},"extended_bpm,rgb_waveform":{"size":16577120,"ops":[["data",144311836,"ca71ec9290d6ed48"],["data",145914824,"45b88bcd5afea839"],["data",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",147243844,"d77596e8499df246"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150738944,"91aaac306bd43135"],["data",150739456,"786195378d06baff"],["data",150739521,"d5326732238d3e24"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"f9699e90e078917d"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"bb010f4579136412"],["data",150742016,"2853d86b697a9dfa"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]},"phasemeter,rgb_waveform":{"size":16577120,"ops":[["data",135069542,"845677fae2e67916"],["data",144311836,"ca71ec9290d6ed48"],["data",145914824,"45b88bcd5afea839"],["data",145941230,"6922e93e3827642c"],["data",145986008,"84014a74358fe2ac"],["data",145987228,"84014a74358fe2ac"],["data",145988312,"84014a74358fe2ac"],["data",146168552,"84014a74358fe2ac"],["data",146172320,"84014a74358fe2ac"],["data",146173504,"84014a74358fe2ac"],["data",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147243844,"d77596e8499df246"],["data",147246288,"060f2c4b6cd4c5e4"],["data",150411792,"845677fae2e67916"],["data",150427952,"845677fae2e67916"],["data",150446592,"845677fae2e67916"],["data",150464904,"845677fae2e67916"],["data",150483144,"845677fae2e67916"],["data",150501528,"845677fae2e67916"],["data",150519852,"845677fae2e67916"],["data",150538228,"845677fae2e67916"],["data",150556534,"845677fae2e67916"],["data",150572646,"845677fae2e67916"],["data",150587026,"845677fae2e67916"],["data",150601380,"845677fae2e67916"],["data",150619274,"845677fae2e67916"],["data",150637628,"845677fae2e67916"],["data",150655862,"845677fae2e67916"],["data",150673666,"845677fae2e67916"],["data",150692276,"845677fae2e67916"],["data",150709976,"845677fae2e67916"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150738944,"91aaac306bd43135"],["data",150739456,"786195378d06baff"],["data",150739521,"d5326732238d3e24"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"f9699e90e078917d"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"bb010f4579136412"],["data",150742016,"acf759afff056d54"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]},"extended_bpm,phasemeter,rgb_waveform":{"size":16577120,"ops":[["data",135069542,"845677fae2e67916"],["data",144311836,"ca71ec9290d6ed48"],["data",145914824,"45b88bcd5afea839"],["data",145941230,"6922e93e3827642c"],["data",145986008,"84014a74358fe2ac"],["data",145987228,"84014a74358fe2ac"],["data",145988312,"84014a74358fe2ac"],["data",146168552,"84014a74358fe2ac"],["data",146172320,"84014a74358fe2ac"],["data",146173504,"84014a74358fe2ac"],["data",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147243844,"d77596e8499df246"],["data",147246288,"060f2c4b6cd4c5e4"],["data",150411792,"845677fae2e67916"],["data",150427952,"845677fae2e67916"],["data",150446592,"845677fae2e67916"],["data",150464904,"845677fae2e67916"],["data",150483144,"845677fae2e67916"],["data",150501528,"845677fae2e67916"],["data",150519852,"845677fae2e67916"],["data",150538228,"845677fae2e67916"],["data",150556534,"845677fae2e67916"],["data",150572646,"845677fae2e67916"],["data",150587026,"845677fae2e67916"],["data",150601380,"845677fae2e67916"],["data",150619274,"845677fae2e67916"],["data",150637628,"845677fae2e67916"],["data",150655862,"845677fae2e67916"],["data",150673666,"845677fae2e67916"],["data",150692276,"845677fae2e67916"],["data",150709976,"845677fae2e67916"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150738944,"91aaac306bd43135"],["data",150739456,"786195378d06baff"],["data",150739521,"d5326732238d3e24"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"f9699e90e078917d"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"bb010f4579136412"],["data",150742016,"9e7532da7f56183c"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]}},"ours":{"cuecolours":{"size":16599808,"ops":[["data",144150352,"ac638ec83c8cae54"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",150796800,"6db452c680a7f8c7"]]},"ledsaver":{"size":16612352,"ops":[["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",147246160,"f299791cddd3d666"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"1f38751fbe9c8e00"],["copy",150828024,147245276,884],["data",150828908,"3a11950e2934091a"]]},"credit":{"size":16578880,"ops":[["data",146656652,"f445e63209e0be24"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"]]},"cuecolours,hotcuekeys":{"size":16608256,"ops":[["box",150817984,424,63488,0],["box",150820640,425,0,16904],["data",134948828,"8815309ed1a1d33f"],["data",144150352,"ac638ec83c8cae54"],["data",144257356,"03c9e94b28441a1f"],["data",144647420,"9266839af92b27a2"],["data",144648700,"8d0d8900ac2cf436"],["data",145816836,"90b1b75e0586814d"],["data",145945476,"5f89ea7b823bb508"],["data",146154152,"b40832cbfea8a56a"],["data",146166692,"aedd01b3bf32874f"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146602956,"030cb5345274e947"],["data",146647420,"7728b2e2f6f14165"],["data",146648232,"7728b2e2f6f14165"],["data",146649044,"7728b2e2f6f14165"],["data",146649196,"7728b2e2f6f14165"],["data",147246160,"f299791cddd3d666"],["data",150796800,"6db452c680a7f8c7"],["data",150804512,"1b99b878a8b033a3"],["copy",150809552,147245276,884],["data",150810436,"cbf33f29584fb8c8"],["data",150810640,"3854155ee65e476b"],["data",150823379,"4f72ba82f5e21909"]]},"cuecolours,ledsaver":{"size":16612352,"ops":[["data",144150352,"ac638ec83c8cae54"],["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",147246160,"f299791cddd3d666"],["data",150796800,"6db452c680a7f8c7"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"1f38751fbe9c8e00"],["copy",150828024,147245276,884],["data",150828908,"3a11950e2934091a"]]},"cuecolours,credit":{"size":16599808,"ops":[["data",144150352,"ac638ec83c8cae54"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146656652,"f445e63209e0be24"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150796800,"6db452c680a7f8c7"]]},"ledsaver,credit":{"size":16612352,"ops":[["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",146656652,"f445e63209e0be24"],["data",147246160,"f299791cddd3d666"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"1f38751fbe9c8e00"],["copy",150828024,147245276,884],["data",150828908,"3a11950e2934091a"]]},"cuecolours,hotcuekeys,ledsaver":{"size":16612352,"ops":[["box",150817984,424,63488,0],["box",150820640,425,0,16904],["data",134948828,"8815309ed1a1d33f"],["data",144150352,"ac638ec83c8cae54"],["data",144257356,"03c9e94b28441a1f"],["data",144647420,"9266839af92b27a2"],["data",144648700,"8d0d8900ac2cf436"],["data",145816836,"90b1b75e0586814d"],["data",145945476,"5f89ea7b823bb508"],["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146602956,"030cb5345274e947"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",147246160,"ab897fbdedfa502b"],["data",150796800,"6db452c680a7f8c7"],["data",150804512,"1b99b878a8b033a3"],["copy",150809552,147245276,884],["data",150810436,"cbf33f29584fb8c8"],["data",150810640,"3854155ee65e476b"],["data",150823379,"4f72ba82f5e21909"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"34dfc332e32a32ec"],["copy",150828024,147245276,884],["data",150828908,"cfdb635170a79a20"]]},"cuecolours,hotcuekeys,credit":{"size":16608256,"ops":[["box",150817984,424,63488,0],["box",150820640,425,0,16904],["data",134948828,"8815309ed1a1d33f"],["data",144150352,"ac638ec83c8cae54"],["data",144257356,"03c9e94b28441a1f"],["data",144647420,"9266839af92b27a2"],["data",144648700,"8d0d8900ac2cf436"],["data",145816836,"90b1b75e0586814d"],["data",145945476,"5f89ea7b823bb508"],["data",146154152,"b40832cbfea8a56a"],["data",146166692,"aedd01b3bf32874f"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146602956,"030cb5345274e947"],["data",146647420,"7728b2e2f6f14165"],["data",146648232,"7728b2e2f6f14165"],["data",146649044,"7728b2e2f6f14165"],["data",146649196,"7728b2e2f6f14165"],["data",146656652,"f445e63209e0be24"],["data",147246160,"f299791cddd3d666"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150796800,"6db452c680a7f8c7"],["data",150804512,"1b99b878a8b033a3"],["copy",150809552,147245276,884],["data",150810436,"cbf33f29584fb8c8"],["data",150810640,"3854155ee65e476b"],["data",150823379,"4f72ba82f5e21909"]]},"cuecolours,ledsaver,credit":{"size":16612352,"ops":[["data",144150352,"ac638ec83c8cae54"],["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",146656652,"f445e63209e0be24"],["data",147246160,"f299791cddd3d666"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150796800,"6db452c680a7f8c7"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"1f38751fbe9c8e00"],["copy",150828024,147245276,884],["data",150828908,"3a11950e2934091a"]]},"cuecolours,hotcuekeys,ledsaver,credit":{"size":16612352,"ops":[["box",150817984,424,63488,0],["box",150820640,425,0,16904],["data",134948828,"8815309ed1a1d33f"],["data",144150352,"ac638ec83c8cae54"],["data",144257356,"03c9e94b28441a1f"],["data",144647420,"9266839af92b27a2"],["data",144648700,"8d0d8900ac2cf436"],["data",145816836,"90b1b75e0586814d"],["data",145945476,"5f89ea7b823bb508"],["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146602956,"030cb5345274e947"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",146656652,"f445e63209e0be24"],["data",147246160,"ab897fbdedfa502b"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150796800,"6db452c680a7f8c7"],["data",150804512,"1b99b878a8b033a3"],["copy",150809552,147245276,884],["data",150810436,"cbf33f29584fb8c8"],["data",150810640,"3854155ee65e476b"],["data",150823379,"4f72ba82f5e21909"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"34dfc332e32a32ec"],["copy",150828024,147245276,884],["data",150828908,"cfdb635170a79a20"]]}},"expect":{"cuecolours":"33f565dc1b85c0fb21b96d6474d65c220bfdf7e67edb46adcae5dd052eac5cf0","ledsaver":"a72fdfed0d9096a0f46797d2aed9eaf53f0539673e2b31cf682e71fada1c1f13","credit":"cdbbeedd453c58439790d38d901efdb5be846a036f9a7fd0b8085faa05a7dc52","cuecolours,hotcuekeys":"dc6ae56d092de0e0e77e02a6f10ef849e31411f0d742e020257d411386e73904","cuecolours,ledsaver":"c4515ef4a0f6a584b40b8415f9926a6fc0210a1abfadffa7fba04ca41a2922f5","cuecolours,credit":"4cba769597783c7db286c59bc07cd44bf74cb928388e82f46d8a2634885c9806","ledsaver,credit":"8173b9240f82db6a0066372c6ff030ede8e3288977212f8e998a4c786c3415f8","cuecolours,hotcuekeys,ledsaver":"ab7ba9a1ab392dcb4371403a013a273a182290a7810cdcda10b7519d88a50c67","cuecolours,hotcuekeys,credit":"c924eb719a0ebb7ce26407d179932d7d1b3c31ac5197da0ec18c76c357cb0116","cuecolours,ledsaver,credit":"adb0fca5facf9154c9e480a18305b410aad903081805bbb23f03060cb4d4c13b","cuecolours,hotcuekeys,ledsaver,credit":"2051eb058c7eb470e5184d7b3732f843af26b68b8f55c8720a6374db1c766c38","extended_bpm":"1150cc0099d890fb4b9e09cf19f03464106ffd5fd04b1fa940d5af0b41008c4a","extended_bpm,cuecolours":"1d654429265d128f6f57428f696ef43d891cff7ab7f781ff070fd0f8691d0bb6","extended_bpm,ledsaver":"3ff12eafdb2fa13571e3e00fddd9627711300e6a3fb7240f2e3743b0ecba40cd","extended_bpm,credit":"5ce2c13313f4f29b75d03ae3076aae93230940ba30689c1413eea6665f70335e","extended_bpm,cuecolours,hotcuekeys":"ec97fc75a0f060c583a948d0f1e11a70b90eb77823bc6a3e928a237cb465e24e","extended_bpm,cuecolours,ledsaver":"ebe14934834529003bcaf8bd4811438b0ed3758c8edb82a0c75db37ddbb488fd","extended_bpm,cuecolours,credit":"1de56f1275188d082408d91c5e21cdc8cb6dcc1c98ea99bc424f477b7f58890b","extended_bpm,ledsaver,credit":"1b48a24ef744b2e2ac4f2952ae72c717ef554abab3ce8c46049b714221c31e67","extended_bpm,cuecolours,hotcuekeys,ledsaver":"7887783d81bc355901059f121780569fc61e90a92c7fa5ebe5a6bf0f14ab0183","extended_bpm,cuecolours,hotcuekeys,credit":"8e65445037898be915b09ffb56eb84c83d263505bff1e437981411d8c44abd86","extended_bpm,cuecolours,ledsaver,credit":"237764b0b08e357cf41d0c242397e5823ddb00c06b85ce2e0e6abcd3be656f76","extended_bpm,cuecolours,hotcuekeys,ledsaver,credit":"e4c246d634fca90badbd813b5b9deb701b64b9c87b6ed1fc51c1a9fa8be8f3af","phasemeter":"dca533375e39fed109c5b35aef80bcc036ed4500ee2e67f8f3ea3bb0309cdedb","phasemeter,cuecolours":"7f6599ccb6b94ad8d8b85140fdce6a51a41381e7bf9f05f1a51700bbeacd7513","phasemeter,ledsaver":"13ed14e1750ec1ad52811773f6f5d82c8285e49cc0312f4faa2e52d4c6e69250","phasemeter,credit":"9b6c48e501f784c65d157147fd07c9a3d0585ca5de7b5c6bc6e12dca23f55f59","phasemeter,cuecolours,hotcuekeys":"c830d64d7a455526cccc05eaaae68ad6a191bd3881278d91143bad31d7ffd068","phasemeter,cuecolours,ledsaver":"b5b7570f42355bd5f1348763fec8c7379c37addd34a51469ce46758f25935e29","phasemeter,cuecolours,credit":"d99f6f2423c82ff1e083e618b875ea9b77f0aec740c5ad9441d1b106a3e925ee","phasemeter,ledsaver,credit":"ae6f07e4823803feff9ba5ffc99c5a99bd6fd7528e399c196a1ebeb97cad5227","phasemeter,cuecolours,hotcuekeys,ledsaver":"a30e8e6396e2be96310e99c4ab0beb3f4d9060132e8fda055d9e5a3490a4d598","phasemeter,cuecolours,hotcuekeys,credit":"133f7e8112a58c826da783dda18e84e0cb7cab82696e35e8eea4040a5736b91d","phasemeter,cuecolours,ledsaver,credit":"87611265c7484e9b62aeb09e7db0fb9e0cc10aa7250845a4d4d1fbf0defedfa8","phasemeter,cuecolours,hotcuekeys,ledsaver,credit":"928bd8540edd0cadaaad2ef1adceea0d022f910eae999387608728343f847908","rgb_waveform":"0cd6e156824b70f4c010e49348e9cbf2d26e83e12cd9feba14e26c539e64e6be","rgb_waveform,cuecolours":"90d6810eaf2c3c8200a8e6c9f4a43089310f966bf34c4d81f88fdd68f13eedd2","rgb_waveform,ledsaver":"0e9b068fa49feddd262127ba90d0348f3a83e5797fe38c6ac43e97ef4bcb94ad","rgb_waveform,credit":"90a64a0d2044daf0d2befdeb78e3f0bf09aea77cbbd5b8046a6127356af5fc88","rgb_waveform,cuecolours,hotcuekeys":"8985dd4e90229467727f8622ac095fafec4fe20199336edcb2aefb4add87a186","rgb_waveform,cuecolours,ledsaver":"f61280851486e9ec766cecb15a3b6a3400f0d768a6a90479469400889fabf723","rgb_waveform,cuecolours,credit":"8763afcb40230b67916f64beedf8251d5135c0decdea1413e71062b889d92557","rgb_waveform,ledsaver,credit":"3c9234187a75f8673cd23cf3817e4af9aef34c2cf174eb7d20510902f864c47e","rgb_waveform,cuecolours,hotcuekeys,ledsaver":"8bf5fab6b4b0ce24eb9c517f47b2dd903ed0fde6a878003f912757441ecbbe4e","rgb_waveform,cuecolours,hotcuekeys,credit":"4e23c4ea1c16bf2292669a953e6e244337d88cc695369038049fdf0d5c66f806","rgb_waveform,cuecolours,ledsaver,credit":"58a0dfc9bfdf0b335b529b5adb22e48813cc843bb8a048facd6dd722924e1e19","rgb_waveform,cuecolours,hotcuekeys,ledsaver,credit":"e79c7938bdc422e698b129a0f4f48cc59306cd3bc45eb4254d926ee2d0eaaa38","extended_bpm,phasemeter":"5c663e5fdb53c791a75ee45de1a0875eb83fd247534a916403427cb35e578f14","extended_bpm,phasemeter,cuecolours":"d2cc915162acdc88fe2145442ff10ad64add3e5d0c9289c51c24c831a362c653","extended_bpm,phasemeter,ledsaver":"9ac0a6223d37aa09ed7c71b53d18a3f791646882ac2d5c4d149f0507977875de","extended_bpm,phasemeter,credit":"3eac8591830185176f578076dc7fd2ee233c30bde2fa38a0afb6f7470241c837","extended_bpm,phasemeter,cuecolours,hotcuekeys":"fccd05fea2d648a1345f08c89f7b12c79e146cfd68a3ed789b656b6b12489457","extended_bpm,phasemeter,cuecolours,ledsaver":"cf64b3555cb02dea6b0e129e6f10c7982d203a2319c41ec64b400c1d82f6514b","extended_bpm,phasemeter,cuecolours,credit":"0d46e2a41d5717d533ab2262517587071425b8e2445ea31757831b1f5f0208b5","extended_bpm,phasemeter,ledsaver,credit":"54dfe2b040666cb9b8b808bae55e1f12216a5c75c9c89ee91744ef5bb62b3f5b","extended_bpm,phasemeter,cuecolours,hotcuekeys,ledsaver":"7006769ff35db824897c828f1046877c19901fc084c9059186dca59e98ce27f4","extended_bpm,phasemeter,cuecolours,hotcuekeys,credit":"c0e1fca211b5545b3dfe274d41014a6c91bbf1f4d97925da434cc0fa0a858f52","extended_bpm,phasemeter,cuecolours,ledsaver,credit":"3ae85f0a5c473b3b9a69ada56f22a6a04c825102462eab9b926625053f6c0414","extended_bpm,phasemeter,cuecolours,hotcuekeys,ledsaver,credit":"e03ca29ed09132a698818448ef582739ed05fe1ae4d191e81be74c567586dacd","extended_bpm,rgb_waveform":"6ffa6d0392ba2245cc40d6a44c8268e87f98388496fbbd5fe8da3b60632b8110","extended_bpm,rgb_waveform,cuecolours":"4cf527c7bd6f5ca80724fafddc34332e67bbb4132a6d05f06fc55b33b3abd692","extended_bpm,rgb_waveform,ledsaver":"76d382d74d2a24aa8cc47e755227a550cfcc52e9e74f77904b64b660725519cb","extended_bpm,rgb_waveform,credit":"7cbc6d8b109c11795f9cd164a9e847db6eff16f4253535fa7af252871a498cd8","extended_bpm,rgb_waveform,cuecolours,hotcuekeys":"67588b2b3146350379eb577071e73a61fe3a39549c12529a667ee7cd3dbff6e6","extended_bpm,rgb_waveform,cuecolours,ledsaver":"7c43ec2ad24b1dbdd0b10c6a6b9493554c9de57fbfa8b670e3c01ff3ceb953b3","extended_bpm,rgb_waveform,cuecolours,credit":"ad5e5143a41be8d5ab3756ef0daec8d661a0f7be5437bc1c99e7259122e9f9f1","extended_bpm,rgb_waveform,ledsaver,credit":"29b5e2c1e71d510695fdf28304bbb73a323ca81b167b5d81f059440bd91cd980","extended_bpm,rgb_waveform,cuecolours,hotcuekeys,ledsaver":"6102432499ab0c1193eb6350bc4398018078d8bc49395ce4ca1a6fd06efd457f","extended_bpm,rgb_waveform,cuecolours,hotcuekeys,credit":"ebef3ce31f5d8ccfd0ce76e2cbd598ddefc3d1927c23f5233d08590d38737f3b","extended_bpm,rgb_waveform,cuecolours,ledsaver,credit":"61e9ff151fa07cf130f1b4986394119477cf6812db993935d20353bf8fe5e0b7","extended_bpm,rgb_waveform,cuecolours,hotcuekeys,ledsaver,credit":"fff6958216607602c6eebc865139418a9361df4e6202a03bcfe1e4895466fdd0","phasemeter,rgb_waveform":"afeb3fe6b77de187570ede488048a47213b92cd472bad8400d05d81c0f5ce8a4","phasemeter,rgb_waveform,cuecolours":"7415cf562c6cc94be320cef872f926f2de1c7a70850462b79c7c5162fa33f207","phasemeter,rgb_waveform,ledsaver":"08a3eda0a745ff7cd09eae2799d5a6dd485e45ffa58faccca42261ec7f24eb33","phasemeter,rgb_waveform,credit":"eadf29d05d65e9786d7400ba5d8ca922b85dd89680d65a377cc2ef90730cfd54","phasemeter,rgb_waveform,cuecolours,hotcuekeys":"15b0004c56aae605196c0b9f1d134545e9b9847b0c894da3cb2a199096291374","phasemeter,rgb_waveform,cuecolours,ledsaver":"1ccabf4c87becd24b7ca92941ad11c7ac7dc6e8eaef16cfd754b0ec2645f10b2","phasemeter,rgb_waveform,cuecolours,credit":"ac17dd20347e853167dc6fa1976e51339256b7c895ab40fd1b728dfb8e7db341","phasemeter,rgb_waveform,ledsaver,credit":"3438bf64e79d64495f1702d9f49523ba426a0c424b352ba2e36ad285cc7c7bcc","phasemeter,rgb_waveform,cuecolours,hotcuekeys,ledsaver":"c26a9f75bdc4f1b5e8ae17307eba9e9a729640704860d529c096b0c96ff8db24","phasemeter,rgb_waveform,cuecolours,hotcuekeys,credit":"bccde295951b8672462101a1cc8a546490b55232104b50d027454b30fb28c75d","phasemeter,rgb_waveform,cuecolours,ledsaver,credit":"3556504dc8d5673eb944e649922faff3216722e59b965f2e65b759019e5efc05","phasemeter,rgb_waveform,cuecolours,hotcuekeys,ledsaver,credit":"9bb02d1f06b54cba893da7b31b264aa4ff86bd91165b8c802382ba5c8208d1b6","extended_bpm,phasemeter,rgb_waveform":"cba7f8e4748d4adfbb3409e03c519cd9a56a745314b273ef416e5a1e4786ae5d","extended_bpm,phasemeter,rgb_waveform,cuecolours":"547146d47c986f7540fe5923ecf3222ae0b669c4b9fc2dc5ba4c0ab7b9469472","extended_bpm,phasemeter,rgb_waveform,ledsaver":"962686fec8eaaa3dcb001e38954c50bc7024ea5b92922aa8830febffd2bfd143","extended_bpm,phasemeter,rgb_waveform,credit":"f953ec56ceb305fd690974b207cd8ea05f437a9eabc12652cc70b4f70eb63318","extended_bpm,phasemeter,rgb_waveform,cuecolours,hotcuekeys":"06bfd6289a1aa3d643205fb1a69ff8f872d71426fb0fa10c7acc226a4eb27038","extended_bpm,phasemeter,rgb_waveform,cuecolours,ledsaver":"c2f95202abc426be20cb0004a8c01871d516089aec96d18846eb83d977211275","extended_bpm,phasemeter,rgb_waveform,cuecolours,credit":"8eb1a06c4be3f1629fba26a0ae20487fe6e759281b568bb39ee5affdc73903cd","extended_bpm,phasemeter,rgb_waveform,ledsaver,credit":"48746eefbc2f33f028173b670c9f80d6d7e5c8e48b2acbec8004a52e4b915de7","extended_bpm,phasemeter,rgb_waveform,cuecolours,hotcuekeys,ledsaver":"96cc618e283bc3db37e82ef29df953e9ed9a3b0601285804e2c6c8bfedc14e78","extended_bpm,phasemeter,rgb_waveform,cuecolours,hotcuekeys,credit":"e500e0658b57afa3b605fe8df51b0ff0d26c74586d673f019fe7b9614f05a260","extended_bpm,phasemeter,rgb_waveform,cuecolours,ledsaver,credit":"0346a1e8675372e5fcc7783b8ca6d81bef6903fdee3e2c25548ae6cd63dc8f56","extended_bpm,phasemeter,rgb_waveform,cuecolours,hotcuekeys,ledsaver,credit":"d2694918ff7c61a4dce4717a4a8a1e9e31e25c7cf490b9ca3c43de609017da25"},"box":{"resTable":1048576,"width":88,"height":15,"glyphs":{"H":["X.....X","X.....X","X.....X","XXXXXXX","X.....X","X.....X","X.....X"],"O":[".XXXXX.","X.....X","X.....X","X.....X","X.....X","X.....X",".XXXXX."],"T":["XXXXXXX","...X...","...X...","...X...","...X...","...X...","...X..."],"K":["X....X.","X...X..","X..X...","XXX....","X..X...","X...X..","X....X."],"E":["XXXXXX","X.....","X.....","XXXXX.","X.....","X.....","XXXXXX"],"Y":["X.....X",".X...X.","..X.X..","...X...","...X...","...X...","...X..."],"S":[".XXXXX.","X......","X......",".XXXXX.","......X","......X",".XXXXX."]},"text":"HOT KEYS","gap":3,"space":5},"chunks":{"45b88bcd5afea839":"ACX8","c465a030b70c781e":"ACL8","c3967666e153796f":"ACH8","7f0079b1f754ae7e":"AdErQQkACQAAEPwI","a91a9b3491491b64":"AdYrRgkACQAAEvwI","f9e6d34295c320f0":"ACT8","6d11cfe939a0630a":"hi+WLyJPCdj/4AEYCNELQQkAA2kCKAfREmEYIQGJ/+ABGJNgJk/2aQsA9mgALPwIACD8CKDSTQo=","a4d77ab57ab896dd":"Bi8WLyYvNi9GL1YvZi92LyJPEk8CTwNl42QM0QtBCQAL0QERBk8WTyZP9mf2ZvZl9mT2Y/Zi9mH2YGoFJ+YYRgTREHYE1wXSK0IJAAAU/AgALPwIhIurC1DDAADkPr0I","906ea135b6ba2cef":"Ek8CTw3REjQTiQ3REzUQiwzRFzUNiQzRHDVHBRoAC9EcMAvSJTAKAPPhHUAJ0RIwAIv/4AZPCwAWTwkA//8AAPDY////fwAAECcAAIgTAABZF7fRoIYB","d6db78bde1edbcae":"hi+WL6Yvti/GL9Yv5i8iTxJPAk/sfz7YglAIIAKJg1AIIAGLaKAJAIhbOtESOweJiVADagGIDYkCiAuJA4gJiTXRglQLQQDlM9GDVAtBAOVSoAkAM9ESabNk82XQsQkAo2ADiCkMgl2DVC/eNNrILAOJglSDXS3eMton0QtBAOXxVQBFKtEcNcw102Qj0QtBCQDTZAHlINELQQkAk2Q35fJmyCwDiSXlAEYi0Rw2C04JAADrs2AIQK4FCHD+BsgsAokARhzRHDYLTpNks2AIQK4Fk2QZ0QtBCQADZIhSs2AIQBnRHgESMikFC9ELQQkAAXsD4RI73YsUfwZPFk8mT/Zu9m32bPZr9mr2aQsA9mgALPwIoIYBALSvsggAprII8IurC5RwEBHYVrwIila8COoAAADrAAAA1wAAADRWvAhMF/wIWBf8CGQX/AhEAAAAUAAAAFwAAAAnAAAAKAAAACkAAAAAAAAA6AMAABAn","786195378d06baff":"CdYF52U0CgIJQgFCI2MIQywzAENDYDgwAiUEdSNkEEfwiwsACQAJAM3MzMw=","503c09b6b0baefb3":"4RkAJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRAsAQ2A=","160adecaa849e284":"hi+WL6Yvti/GL9Yv5i8iT/B/Q2hTaWNqc2uCUAggBImDUAggMIlnoAkAg1AIICuLk2RssAkACCAmiaNkZ7AJAAggIYmQOh+JsGABiByLs2EYcRBgAYgXi0DREmIoIhOJI2ADyBCLElMzYAPIDIs81Dw0MjQIixFVUjQFiSNmTDYiNgGLA6AJAADgNqAJADwyNuMA4AIiBHIQQ/uLMd7zZAh0C06zZfNkDHSzZQtOGHUt3gDkC04c5QNsAOQLThzlA22TZMNlN7DyVqNk02UzsPNWB+ACLwDgAR8k3gDkk2XDZgtOAOcA5KNl02YLTgDn8lACGPNQAxgB4BB/Jk/2bvZt9mz2a/Zq9mkLAPZoSCQQiUSEDogNi0NhIHESYhTTIDMHixWFBogEixFQCCABiQsAAeALAADgSVEG4hZgAiUEdRBC+otiJUIWQ2EQcRFgY2EQcQEh/+ALAGOBqEsQEdgAAAC4LLMIZiyzCJgmswhsJwuo","d82da843aae59533":"hi+WLyJPW9hAeFvREmmYKQ6JilAAOQuJAOADGAgYCRhW0ZNkC0Fb5YNkA2UEsJNmJk/2aQsA9miGL5Yvpi+2L8Yv1i/mLyJP8H9DaFNpY2qYKWuJlIQOiGiLk2EgcRJiSNMgM2KLEVAIIF+JFYUDaxVLW4sQ4ic7WImzbAJ8CEzDbVB9UH1A0RJiKCJOiSNgA8hLixJTM2ADyEeL02Q8NDI0Q4sRVVI0QIkjZkw2IjY8izwy02MJQwDgAiIEchBD+4sy3i7V82QLTgh0LNUYdfNkC04MdC7RAOQLQcNlA22ZUdNis2MWYAIiBHIQQ/qL8lRCIvNVURKSFJIVmIVIgViB/+BDgVOBs2ACcAIvAOABHyDRAOSTZdNmC0EA5/JQCBjzUAkYqhgBoAHgAOAQfyZP9m72bfZs9mv2avZpCwD2aAkAAQAAAAAAAADZACQAowIAAP8A//8BAAAAAQAAAAAAAADZADwApAIAAP8A//8BAAAAACz8CJhwEBE0VrwIRB/8CGwnC6ioSxARuCyzCGYsswiYJrMI","7010683be462baa0":"1i/mLyJPEk8CT/x/AdErQQkACQBcPr0I","ebd1070ec574dec1":"Ni9GL1YvZi92L4YvIk8R2IRTAXM0GBDTC0MJAAYvFi8O0RJjNhiCZDA0/+AAi4FQBRgL0RJgBxiEUwFzNBj2YfZgJk/2aPZn9mb2ZfZkCwD2YwkAACz8CIxpwggkjKsL8IurCw==","172e933902cb3738":"hi+WLyJPFNiEWZNgAcgMi4VQBi+HUAYvhFEQOQSL9mAJGPZgAaAIGAh/mhgL0QtBCQAGL4VR/+AAMQSJh1GJUAAxAYn/4AgYBdELQQkA9mAmT/ZpCwD2aAAs/AgQgbwIABb8CA==","2853d86b697a9dfa":"hi+WL6Yvti8iTxbRC0EJAANrFdgV0RJpmCkYiYxQADkCiQDgAhgDGBHRk2QLQV7lA2oP0ZNkC0Er5QNmg2SjZQzXDdELQQkACCAAiZwYC9EJAAkAs2AmT/Zr9mr2aQsA9mgJAEzDvQgALPwIlHAQETRWvAgAJvwIABz8CAAe/Ag=","7c188559c27b7ceb":"hi8iTwjRC0EJAANoB9EA4AIRAxEMEUBxAxEIEQkRChGDYCZPCwD2aD4sswgALPwI","e3f375bd0e18a505":"AQAAAAAAAACKAgQA6gAAAP8A//8BAAAAAQAAAAAAAACKAgQA6wAAAP8A//8B","4617c585a7472b06":"hi+WL6Yvti/GL9Yv5i8iTxJPAk/4f23RQHH/4AURBhFl2IhRGCFTicTgDGAeAigiTolB4AhALgkIcC4KmClHiQPgGEDMcI4LkVwF4RI8P4uTZKNlWdELQbNmFUA4i/9wA20DYQhBCEGcMRRxEWItYv9yA+M2MiuJKD0RTSiL02EEccIxJInTZAhECEScNBR0Q25L0QtBo2UDbONkQHRI0QtBo2XIMANtFU0Ri0jREj0OibNkyDQRRAqL0jQIiShEPrnTZQNuQdFAcQURAqAJAGKgAOA92EB4OtmdWhTgnAu8a5Fckl2UUZ1SoDIpi4JioDImibNi/3ID4zYyIYnGPR+L02PIMzPQAjMaiRlBGCEXiTNkGEQIRAhEEbkTZS7RBTEKAxoAKEMpQDsgCCAIibNi/3InABoDo2I4MqIoIRgCGIJcyCzDiR/RC0HzZPFRw20ITR7SIj26iYJiE2MoM9IztYmBUhNkKDTSNACL2DTSNK2JKETjuNNlFNJAcgYS42EIMQhBHWEfYRTSJwEaABPSLDDw4ixADNhAeAQYCH8GTxZPJk/2bvZt9mz2a/Zq9mkLAPZoCQDcRZQJTH2iCLbvoAjAUBAR/NnACAAs/AgAAAEAAAAIAAAAAQAyAAAAAIA=","163da0f1e1acc40a":"hi+WLyJPEk9f0QtBCQAGL2XYQHhd0QtBCQABiCkJiFGJUhghAYkoIgCLAOmDUZAxAolDsAkAkxiYKQGJoL4JAIhUhlUKsJNmiVSFVQawk2b2YBZPJk/2aQsA9mhIJCuJ1i/mLyJPQ25oJhqJ/+AANReJS9EXBRoNKU1K0Rw97IXQMAWJ02DsgQhw7oEDoAkA41AIIAyL42RA0QtBAeUHoAkA41AIIAOJ42Q80QtBAOUmT/ZuCwD2bQsACQCmL7Yvxi/WL+YvIk8x0RJsyCw0iSrdLNoq2wTu1GVcZS3RC0HDZAggJYkDZCrgTQEdYQXiJjEAiyNhsWM9YzYvFi9JUiYvpi/yURghEon/cRIf8VImZCEf8mM1ZTIvXWWYKQCJ81VIJO6JHNELQQkA6q8JABB/AnsQTs+PCnomT/Zu9m32bPZrCwD2agxTWU2hAqICogKiAsMBxAHFAcYBxwHIAckBygHLAcwByAHJAdAB0QHSAcgByQHNAc4BzwHIKvwIzCr8CNQq/AjoAr0I+NmvCJhwEBE0VrwIAKayCLSvsgjDAAAA2QAAAAAs/Ag=","5e4e42864642a517":"//////////8AAAAAAAAAAAAAAAD//////////wAAAAD/////","845677fae2e67916":"UABIAEEAUwBFACAATQBFAFQARQBSAAAAAA==","6922e93e3827642c":"pQ==","84014a74358fe2ac":"0q+y","371b074706e32cac":"Lfw=","060f2c4b6cd4c5e4":"gCn8","acf759afff056d54":"hi+WL6Yvti8iTxbRC0EJAANrFdgV0RJpmCkYiYxQADkCiQDgAhgDGBHRk2QLQV7lA2oP0ZNkC0Er5QNmg2SjZQzXDdEA4AkACCAAiZwYC9ELQQkAs2AmT/Zr9mr2aQsA9mgJAEzDvQgALPwIlHAQETRWvAgAJvwIABz8CAAe/Ag=","ca71ec9290d6ed48":"ZCL8","52b8fe18a1fed3bf":"ABj8","a2721044eff0d2f6":"Ghj8","620bfdaa346b088f":"fw==","582d807f9ed58432":"YO/8CA==","d77596e8499df246":"Q5FDkUORQ5Gl/KX8opCD4WP85875NZ+W","91aaac306bd43135":"Ik8i0QtBCQABiAOLIdFAcQHiLBEmTwsACQAiTx3RQHEcUAggAokA4AawDBEY0QtBCQAmTwsACQCGL5Yvpi8W0RNYiCgdiRXREmmYKRmJFNAeCqgqFYmTYpwyF3L84AkijDIiYA/TMDALiyFQkDAIi4NiFHKcMgF6JGAAKhBJ+48CevZq9mkLAPZoCQASKb0IVCq9CAAs/AjEBKoLYAd9CxjuAgBSR0I1","d5326732238d3e24":"4RkAJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRAsAQ2CWL6Yvti/GL9Yv5i8iT/R/Q25TbGNpc2r/7eguLonia7grK4kb0QtBs2QIICaJGdELQUjkCCAhiQEfAh8A4RIv42QDZRjWFNELQQHn8VLGEhfRFxIX0RgSs2TyVRDRC0EJAAggCoumL/NUD9XzZgR2k2cL0QtBCQADbQR/02AMfyZP9m72bfZs9mv2agsA9mkJAPI2lwgeeZoIUDyXCPA4lwiYQZcIBCwAAAJPAABQV1Y1RVhU","f9699e90e078917d":"1i/mLyJPEk8CT/x/AdErQQkACQBcPr0IIk9DYUDiQ2MUc1wzEGBQiB6LEYRXiBuLEoRWiBiLE4Q1iBWLE2casAx3AogQixNnFbAQd2IwC4sTZxCwBHdA5EYwBYkcMANnbDdsNzY3A4sQQtuPAXEA4CZPCwAJAHRgDGB0ZVxlGEBbIHRlXGUYQFsgdGVcZRhACwBbIA==","bb010f4579136412":"hi+WLyJPFNiEWZNgAcgMi4VQBi+HUAYvhFEQOQSL9mAJGPZgAaAIGAh/mhgL0QtBCQAGL4VR/+AAMQSJh1GJUAAxAYn/4AgYBdELQQkA9mAmT/ZpCwD2aAAs/AgQgbwIABb8CIYvli+mL7Yvxi/WL+YvIk/0f0NoU2ljanNrM9ELQQkAA2wRTFWLom2ybuguUYnYLU+JLtEWPUyJAOACLwEfg2STZfNm82cOvAR38VRIJECJEUA6i/Jlrb7TZgggNYkDaNNk3DQXdPzgCSQIdCDRC0EJAAggKYkDaeNhk2IF4xZgAiIQQ/uPBHLTYxRgACIQQ/uPAXIY0QDl02OEYAxgAECEZABEXjAcAAAiEEP1jwFy02DcMBdw/OEZIJwwD9ESINEQkisL0QtB42TxVAnRC0EJAMNgDH8mT/Zu9m32bPZr9mr2aQsA9miAhpcIsB4EAOzGpQjC66UIYPD8CFJHQjU=","9bed08a38f1020b6":"hi+WL6Yvti8iTxbRC0EJAANrFdgV0RJpmCkYiYxQADkCiQDgAhgDGBHRk2QLQV7lA2oP0ZNkC0Er5QNmg2SjZQzXDdEA4AkACCAAiZwYC9EJAAkAs2AmT/Zr9mr2aQsA9mgJAEzDvQgALPwIlHAQETRWvAgAJvwIABz8CAAe/Ag=","9e7532da7f56183c":"hi+WL6Yvti8iTxbRC0EJAANrFdgV0RJpmCkYiYxQADkCiQDgAhgDGBHRk2QLQV7lA2oP0ZNkC0Er5QNmg2SjZQzXDdELQQkACCAAiZwYC9ELQQkAs2AmT/Zr9mr2aQsA9mgJAEzDvQgALPwIlHAQETRWvAgAJvwIABz8CAAe/Ag=","ac638ec83c8cae54":"APr8","ac4b9d28c5bfb9dd":"vvz8","03bdb1d5e2d1ccb6":"yP38","38712f352f6a6c0e":"B+0LR9ofagz3VQg=","e7cf46a078fed4fa":"Cw==","aca9c8d7ca83a619":"Fv38","6db452c680a7f8c7":"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","f528282873b9c40b":"IGz9","62ab7ebb0d777c57":"mmz9","28dd5e2b2f412bd3":"+HP9CA==","f299791cddd3d666":"Eg==","4bf5122f344554c5":"AQ==","1f38751fbe9c8e00":"Ik9w0AtACQAGL3DREYQIIAGLwLAJAG3REGAIIAiJbNIiY2zQBjMGiSFTa9AGMwKJAOAeoBKAadIiYhKECCAEiwHgEoAA4AERIhESUyNgODARQAmLBXMyERFQAXBa4zIwAIsA4PGvARFd0igiAYkA4AAi9mAmTwsACQBU0RKECCAoiUYvVi92LxFQCEAIQFXSDDJV1QjnVGAMYGNjDDMkZDBgSSAkZEsgEEf0jwAjY2IW5wDkJGAMYAw0Q2AZQAggAYlMZAF0EEf0i0NgACL2Z/Zl9mQ80StBCQBEUP+IAolC0StBCQCGLyJPQ2g30RBgDGAEFD3RC0EJADTREGEcYRMY/+EUGCZPCwD2aERVRIVdZg1iIzYMiUNSIDUJiVMUKtFTYAAhIk8FsFNkJk8LAAHgCwAA4IYvli8iT/x/Q2gr0RJk82UA5ivRC0EJAAggHIvyaZgpGYmTZADljOYp0QtBbGYF4JSAJ9CUgYNglYEg0RJkk2Uh0QtBCQAIIAWJG9ESZJNlHtELQQkABH8mT/ZpCwD2aAHgEYAc0hzTGtX/5m1mMjIKiSFgDWBgMAaJUDACiyGFAckAIfOvBHILAAkACQB4MrYIjkivCABs/QjYlXwLfJIAALBxCwCoUjILAAAAADRu/QjUc/0IfJy9CKTXEQqg1xEK3NbACFC6wAgU2MAIOIDCCOKDAAAAYH+gAIB/oDAC8AbwDAAA/AAAAM8wAAAwAvAJ8AwAAfwAAADPMAAAMALwCfAMAAH8AAAAzzAAADAC8AnwDAAB/AAAAM8wAAAwAvAJ8AwAAvwAAADPMAAAMALwCfAMAAL8AAAAzzAAADAC8AnwDAAC/AAAAM8wAAAwAvAJ8AwAAvwAAADPMAAAMAHwCfAMAAP8AAAAzzAAADAB8AnwDAAH/AAAAM8wAAAwAfAJ8AwAB/wAAADPMAAAMAHwDPAMAAf8AAAAzzAAADAB8AzwDAAL/AAAAM8wAAAwAfAM8AkAC/wAAADPMAAAMAHwDPAJAAv8AAAAzzAAADAE8AzwCQAL/AAAAM8wAAAwBPAM8AkAH/wAAADPMAAAMATwDPAJAB/8AAAAzzAAADAE8AzwCQAe/AAAAM8wAAAwBPAM8AkALvwAAADPMAAAMATwDPAJAC78AAAAzzAAADAE8AzwCQAu/AAAAM8wAAAwBPAM8AkALfwAAADPMAAAMIDwDPAGAD38AAD/zwAAADCA8AzwBgB9/AAA/88AAAAwgPAM8AYAfPwAAP/PAAAAMIDwDPAGAHj8AAD/zwAAADCA8AzwBgC4/AAA/88AAAAwgPAM8AYAuPwAAP/PAAAAMIDwDPAGALj8AAD/zwAAADAI8AzwBgC0/AAA/88AAAAwCPAM8AYA9PwBAP/PAAAAMAjwDPAGAPT8AQD/zwAAADAI8AzwAwDg/AEA/88AAAAwCPAM8AMA4PwCAP/PAAAAMAjwCfADAOD8AgD/zwAAADAI8AnwAwDg/AIA/88AAAAwCPAJ8AMA0PwCAP/PAAAAMEDwCfADAND8AwD/zwAAADBA8AnwAwDQ/AMA/88AAAAwQPAJ8AMAwPwDAP/PAAAAMEDwCfADAID8AwD/zwAAADBA8AnwAwCA/AMA/88AAAAwQPAJ8AMAgPwDAP/PAAAAMEDwCfADAID8AwD/zwAAADBA8AnwAwBA/AMAAM8AAAMwQPAG8AMAQPwDAADPAAADMEDwBvADAED8AwAAzwAAAzBA8AbwAwAA/AIAAM8AAAMwQPAG8AMAAPwCAADPAAADMEDwBvADAAD8AgAAzwAAAzBA8AbwAwAA/AIAAM8AAAMwQPAG8AMAAPwCAADPAAADMAjwBvADAAD8AwAAzwAAAzAI8AbwAwBA/AMAAM8AAAMwCPAG8AMAQPwDAADPAAADMAjwA/ADAED8AwAAzwAAAzAI8APwAwCA/AMAAM8AAAMwCPAD8AYAgPwDAADPAAADMAjwA/AGAID8AwAAzwAAAzCA8APwBgCA/AMAAM8AAAMwgPAD8AYA0PwDAADPAAADMIDwA/AGAND8AwAAzwAAAzCA8APwBgDQ/AIAAM8AAAMwgPAD8AYA4PwCAADPAAADMIDwA/AGAOD8AgAAzwAAAzCA8APwBgDg/AIAAM8AAAMwgPAD8AYA4PwBAADPAAADMATwA/AJAPD8AQD/zwAAADAE8APwCQD0/AEA/88AAAAwBPAD8AkA9PwAAP/PAAAAMATwA/AJALT8AAD/zwAAADAE8APwCQC4/AAA/88AAAAwBPAD8AkAuPwAAP/PAAAAMATwA/AJALj8AAD/zwAAADAB8APwCQB4/AAA/88AAAAwAfAD8AkAffwAAP/PAAAAMAHwA/AJAH38AAD/zwAAADAB8APwDAAt/AAA/88AAAAwAfAD8AwALvwAAP/PAAAAMAHwBvAMAC78AAD/zwAAADAB8AbwDAAu/AAA/88AAAAwAfAG8AwAHvwAAP/PAAAAMALwBvAMAB/8AAD/zwAAADAC8AbwDAAf/AAA/88AAAAwAvAG8AwAD/wAAP/PAAAAMALwBvAMAAv8AAD/zwAAADAC8AbwDAAL/AAA/88AAAAwAvAG8AwAC/wAAP/PAAAAMALwBvAMAAv8AAD/zwAAAAABAwQFBwgJTABFAEQAIABBAE4ASQBNAEEAVABJAE8ATgAAAA==","3a11950e2934091a":"3HP9CHhkEBECAAAA//////////8BAAAAKpG9CPps/QhEn70ISqC9CCxt/QhYpr0I8qa9CA==","f445e63209e0be24":"IPn8","95951541115495c3":"APn8","e9b0c031f0493d3f":"iQ==","c76a1882879da374":"UGlvbmFhcnIgTW9kcyAxLjAAAAAAAAAAAAAAAAAAAAAB0AsACQAJABH5/Kg=","8815309ed1a1d33f":"aCr9","03c9e94b28441a1f":"KCf9","9266839af92b27a2":"UiT9","8d0d8900ac2cf436":"OiT9","90b1b75e0586814d":"cCf9","5f89ea7b823bb508":"Hiv9","b40832cbfea8a56a":"IBj9","aedd01b3bf32874f":"DCn9","030cb5345274e947":"AdArQAkACQBAIf0I","7728b2e2f6f14165":"0Cv9CA==","1b99b878a8b033a3":"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","cbf33f29584fb8c8":"eC/9CHhkEBECAAAA//////////8BAAAAKpG9CHop/QhEn70ISqC9CKwp/QhYpr0I8qa9CEgATwBUACAAQwBVAEUAIABLAEUAWQBT","3854155ee65e476b":"BAAEAgQEBAYFAAAA/////////////////////////////////////////////////////w==","4f72ba82f5e21909":"AQAAAAAAAAAAAAAAAAAABAAAABMA/zAAAAAAAAEAAAAAAAAAAAAAAAAAAAQAAAAm/14AAAAAAAABAAAAAAAAAAAAAAAAAAAEAAAACADE/wAAAAAAAQAAAAAAAAAAAAAAAAAABAAAADxNAP8AAAAAAAEAAAAAAAAAAAAAAAAAAAQAAAATAP8wAAAAAAABAAAAAAAAAAAAAAAAAAAEAAAAFzP/AAAAAAAAAQAAAAAAAAAAAAAAAAAABAAAACv/ABcAAAAAAAEAAAAAAAAAAAAAAAAAAAQAAAAm/14AAADmB+D6PwYfSOYH4DcC+OD6","ab897fbdedfa502b":"Ew==","34dfc332e32a32ec":"Ik9w0AtACQAGL3DREYQIIAGLwLAJAG3REGAIIAiJbNIiY2zQBjMGiSFTa9AGMwKJAOAeoBKAadIiYhKECCAEiwHgEoAA4AERIhESUyNgODARQAmLBXMyERFQAXBa4zIwAIsA4PGvARFd0igiAYkA4AAi9mAmTwsACQBU0RKECCAoiUYvVi92LxFQCEAIQFXSDDJV1QjnVGAMYGNjDDMkZDBgSSAkZEsgEEf0jwAjY2IW5wDkJGAMYAw0Q2AZQAggAYlMZAF0EEf0i0NgACL2Z/Zl9mQ80StBCQBEUP+IAolC0StBCQCGLyJPQ2g30RBgDGAEFD3RC0EJADTREGEcYRMY/+EUGCZPCwD2aERVRIVdZg1iIzYMiUNSIDUJiVMUKtFTYAAhIk8FsFNkJk8LAAHgCwAA4IYvli8iT/x/Q2gr0RJk82UA5ivRC0EJAAggHIvyaZgpGYmTZADljOYp0QtBbGYF4JSAJ9CUgYNglYEg0RJkk2Uh0QtBCQAIIAWJG9ESZJNlHtELQQkABH8mT/ZpCwD2aAHgEYAc0hzTGtX/5m1mMjIKiSFgDWBgMAaJUDACiyGFAckAIfOvBHILAAkACQAgGP0IDCn9CABs/QjYlXwLfJIAALBxCwCoUjILDRj9CDRu/QjUc/0IfJy9CKTXEQqg1xEK3NbACFC6wAgU2MAIOIDCCOKDAAAAYH+gAIB/oDAC8AbwDAAA/AAAAM8wAAAwAvAJ8AwAAfwAAADPMAAAMALwCfAMAAH8AAAAzzAAADAC8AnwDAAB/AAAAM8wAAAwAvAJ8AwAAvwAAADPMAAAMALwCfAMAAL8AAAAzzAAADAC8AnwDAAC/AAAAM8wAAAwAvAJ8AwAAvwAAADPMAAAMAHwCfAMAAP8AAAAzzAAADAB8AnwDAAH/AAAAM8wAAAwAfAJ8AwAB/wAAADPMAAAMAHwDPAMAAf8AAAAzzAAADAB8AzwDAAL/AAAAM8wAAAwAfAM8AkAC/wAAADPMAAAMAHwDPAJAAv8AAAAzzAAADAE8AzwCQAL/AAAAM8wAAAwBPAM8AkAH/wAAADPMAAAMATwDPAJAB/8AAAAzzAAADAE8AzwCQAe/AAAAM8wAAAwBPAM8AkALvwAAADPMAAAMATwDPAJAC78AAAAzzAAADAE8AzwCQAu/AAAAM8wAAAwBPAM8AkALfwAAADPMAAAMIDwDPAGAD38AAD/zwAAADCA8AzwBgB9/AAA/88AAAAwgPAM8AYAfPwAAP/PAAAAMIDwDPAGAHj8AAD/zwAAADCA8AzwBgC4/AAA/88AAAAwgPAM8AYAuPwAAP/PAAAAMIDwDPAGALj8AAD/zwAAADAI8AzwBgC0/AAA/88AAAAwCPAM8AYA9PwBAP/PAAAAMAjwDPAGAPT8AQD/zwAAADAI8AzwAwDg/AEA/88AAAAwCPAM8AMA4PwCAP/PAAAAMAjwCfADAOD8AgD/zwAAADAI8AnwAwDg/AIA/88AAAAwCPAJ8AMA0PwCAP/PAAAAMEDwCfADAND8AwD/zwAAADBA8AnwAwDQ/AMA/88AAAAwQPAJ8AMAwPwDAP/PAAAAMEDwCfADAID8AwD/zwAAADBA8AnwAwCA/AMA/88AAAAwQPAJ8AMAgPwDAP/PAAAAMEDwCfADAID8AwD/zwAAADBA8AnwAwBA/AMAAM8AAAMwQPAG8AMAQPwDAADPAAADMEDwBvADAED8AwAAzwAAAzBA8AbwAwAA/AIAAM8AAAMwQPAG8AMAAPwCAADPAAADMEDwBvADAAD8AgAAzwAAAzBA8AbwAwAA/AIAAM8AAAMwQPAG8AMAAPwCAADPAAADMAjwBvADAAD8AwAAzwAAAzAI8AbwAwBA/AMAAM8AAAMwCPAG8AMAQPwDAADPAAADMAjwA/ADAED8AwAAzwAAAzAI8APwAwCA/AMAAM8AAAMwCPAD8AYAgPwDAADPAAADMAjwA/AGAID8AwAAzwAAAzCA8APwBgCA/AMAAM8AAAMwgPAD8AYA0PwDAADPAAADMIDwA/AGAND8AwAAzwAAAzCA8APwBgDQ/AIAAM8AAAMwgPAD8AYA4PwCAADPAAADMIDwA/AGAOD8AgAAzwAAAzCA8APwBgDg/AIAAM8AAAMwgPAD8AYA4PwBAADPAAADMATwA/AJAPD8AQD/zwAAADAE8APwCQD0/AEA/88AAAAwBPAD8AkA9PwAAP/PAAAAMATwA/AJALT8AAD/zwAAADAE8APwCQC4/AAA/88AAAAwBPAD8AkAuPwAAP/PAAAAMATwA/AJALj8AAD/zwAAADAB8APwCQB4/AAA/88AAAAwAfAD8AkAffwAAP/PAAAAMAHwA/AJAH38AAD/zwAAADAB8APwDAAt/AAA/88AAAAwAfAD8AwALvwAAP/PAAAAMAHwBvAMAC78AAD/zwAAADAB8AbwDAAu/AAA/88AAAAwAfAG8AwAHvwAAP/PAAAAMALwBvAMAB/8AAD/zwAAADAC8AbwDAAf/AAA/88AAAAwAvAG8AwAD/wAAP/PAAAAMALwBvAMAAv8AAD/zwAAADAC8AbwDAAL/AAA/88AAAAwAvAG8AwAC/wAAP/PAAAAMALwBvAMAAv8AAD/zwAAAAABAwQFBwgJTABFAEQAIABBAE4ASQBNAEEAVABJAE8ATgAAAA==","cfdb635170a79a20":"eC/9CHhkEBECAAAA//////////8BAAAAKpG9CHop/QhEn70ISqC9CKwp/QhYpr0I8qa9CNxz/Qh4ZBARAgAAAP//////////AQAAACqRvQj6bP0IRJ+9CEqgvQgsbf0IWKa9CPKmvQg="}}; diff --git a/builder/ui.js b/builder/ui.js new file mode 100644 index 0000000..e45998b --- /dev/null +++ b/builder/ui.js @@ -0,0 +1,118 @@ +// The builder page: file, mods with their dependencies, the two +// confirmations, build and download (the work is in builder.js). +(function () { + 'use strict'; + var MODS = [ + { id: 'extended_bpm', name: 'Extended BPM', by: 'arnoldasr', state: 'stable', + what: 'BPM with two decimals: the tenths as a small digit above, the hundredths below.' }, + { id: 'phasemeter', name: 'Phase meter', by: 'arnoldasr', state: 'stable', + what: 'Two-line phase meter instead of the four boxes; UTILITY › PHASE METER (was SLIP FLASHING) switches it.' }, + { id: 'rgb_waveform', name: 'RGB waveform', by: 'arnoldasr', state: 'stable', + what: 'The big waveform in RGB colours, the overview as a red-to-white gradient.' }, + { id: 'cuecolours', name: 'Cue colours', by: 'ours', state: 'stable', + what: 'Memory cues in their rekordbox colours; rekordbox hot cues shown as lettered boxes.' }, + { id: 'hotcuekeys', name: 'Hot cue keys', by: 'ours', state: 'experimental', needs: 'cuecolours', + what: 'Hot cues A–C on the BEAT DIVIDE keys: play, gate, set and delete, saved on the USB, loaded in the background.' }, + { id: 'ledsaver', name: 'LED animation', by: 'ours', state: 'stable', + what: 'While the screen saver dims the display, the button LEDs and the jog rings play an animation. UTILITY › LED ANIMATION.' }, + { id: 'credit', name: 'Boot text', by: 'ours', state: 'stable', + what: 'The boot screen says “Pionaarr Mods 1.0” instead of “Ver 1.31”.' } + ]; + var chosen = {}; + MODS.forEach(function (m) { chosen[m.id] = m.state === 'stable'; }); + var file = null; + + function byId(id) { return document.getElementById(id); } + function needsOf(id) { var m = MODS.filter(function (x) { return x.id === id; })[0]; return m && m.needs; } + + function toggle(id, on) { + chosen[id] = on; + var note = ''; + if (on && needsOf(id) && !chosen[needsOf(id)]) { chosen[needsOf(id)] = true; note = 'Ticked ' + needsOf(id) + ' too: ' + id + ' needs it.'; } + if (!on) MODS.forEach(function (m) { + if (m.needs === id && chosen[m.id]) { chosen[m.id] = false; note = 'Unticked ' + m.id + ' too: it needs ' + id + '.'; } + }); + byId('pick-note').textContent = note; + render(); + } + + function render() { + var box = byId('picks'); + box.innerHTML = ''; + MODS.forEach(function (m) { + var row = document.createElement('label'); + row.className = 'pick' + (chosen[m.id] ? ' on' : ''); + row.innerHTML = + '' + + '' + m.name + ' ' + m.id + '' + + '' + m.what + '' + + '' + (m.by === 'ours' ? 'ours · made with Claude Code' : 'by arnoldasr') + + (m.needs ? ' · needs ' + m.needs + '' : '') + '' + + '' + m.state + ''; + row.querySelector('input').addEventListener('change', function (e) { toggle(m.id, e.target.checked); }); + box.appendChild(row); + }); + ready(); + } + + function ready() { + var why = !file ? 'Choose the update file first.' + : !byId('agree-risk').checked || !byId('agree-ai').checked ? 'Confirm both points above to build.' : ''; + byId('build').disabled = !!why; + byId('build-why').textContent = why; + } + + function setFile(f) { + file = f; + byId('file-state').textContent = f ? f.name + ' · ' + (f.size / 1048576).toFixed(1) + ' MB (checked when you build)' : 'No file yet'; + byId('drop').classList.toggle('has', !!f); + byId('result').hidden = true; + ready(); + } + + function log(msg, cls) { + var l = byId('log'); + l.hidden = false; + var p = document.createElement('div'); + if (cls) p.className = cls; + p.textContent = msg; + l.appendChild(p); + l.scrollTop = l.scrollHeight; + } + + var url = null; + function run() { + var names = MODS.map(function (m) { return m.id; }).filter(function (id) { return chosen[id]; }); + byId('build').disabled = true; + byId('result').hidden = true; + byId('log').innerHTML = ''; + log('mods: ' + (names.length ? names.join(', ') : 'none (stock)')); + file.arrayBuffer().then(function (buf) { + return window.CDJBuilder.build(new Uint8Array(buf), names, log); + }).then(function (r) { + log('done: every check passed', 'ok'); + if (url) URL.revokeObjectURL(url); + url = URL.createObjectURL(new Blob([r.upd], { type: 'application/octet-stream' })); + byId('download').href = url; + byId('sum').innerHTML = 'SHA-256 of the file ' + r.updSha + '
of the application ' + r.appSha + ''; + var ul = byId('checks'); + ul.innerHTML = ''; + r.checks.forEach(function (c) { var li = document.createElement('li'); li.textContent = c; ul.appendChild(li); }); + byId('result').hidden = false; + }).catch(function (e) { + log('stopped: ' + e.message, 'bad'); + }).then(ready); + } + + document.addEventListener('DOMContentLoaded', function () { + render(); + byId('file').addEventListener('change', function (e) { setFile(e.target.files[0] || null); }); + var drop = byId('drop'); + ['dragenter', 'dragover'].forEach(function (t) { drop.addEventListener(t, function (e) { e.preventDefault(); drop.classList.add('over'); }); }); + ['dragleave', 'drop'].forEach(function (t) { drop.addEventListener(t, function (e) { e.preventDefault(); drop.classList.remove('over'); }); }); + drop.addEventListener('drop', function (e) { if (e.dataTransfer.files[0]) setFile(e.dataTransfer.files[0]); }); + byId('agree-risk').addEventListener('change', ready); + byId('agree-ai').addEventListener('change', ready); + byId('build').addEventListener('click', run); + }); +})(); diff --git a/docs.html b/docs.html index 3e2b571..5f017f6 100644 --- a/docs.html +++ b/docs.html @@ -4,7 +4,7 @@ CDJ-900NXS Mods Docs - + @@ -18,6 +18,8 @@ CDJ-900NXS Mods @@ -28,16 +30,19 @@
Documentation

Using the mods

-

Everything you need to build the update, put it on your CDJ-900NXS and use each mod. The player must run the official firmware 1.31; the patcher only accepts that update.

+

Everything you need to build the update, put it on your CDJ-900NXS and use each mod. The player must run the official firmware 1.31; the builder only accepts that update.

Install

    -
  1. Get the official C900NXS.UPD for firmware 1.31 from Pioneer DJ. The patcher checks its SHA-256 and refuses any other file.
  2. -
  3. Run the patcher with Python 3. Tick the mods with the arrow keys and Space, build with Enter. -
    $ python cdj900nxs_patcher.py C900NXS.UPD
    - Ticking a mod also ticks what it needs (utility needs code, hotcuekeys needs cuecolours); unticking one unticks what depends on it (beatjump needs hotcuekeys). credit (ticked at the start) makes the boot screen say Pionope Mods by Givou where it said Ver 1.31; untick it to keep the stock text. -
  4. -
  5. Read the disclaimer and type yes to accept it. Nothing is built without it.
  6. -
  7. The new file is written to out/<date>-<mods>/C900NXS.UPD, with a report.txt listing every check.
  8. -
  9. Copy C900NXS.UPD to the root of a FAT32 USB stick and run the update from the player as you would for an official update.
  10. +
  11. Get the official C900NXS.UPD for firmware 1.31 from Pioneer DJ.
  12. +
  13. Open the builder and choose the file. It checks its SHA-256 and refuses any other file.
  14. +
  15. Tick the mods. Ticking a mod also ticks what it needs (hotcuekeys needs cuecolours); unticking one unticks what needs it.
  16. +
  17. Confirm the two points (no liability, code written with Claude Code) and build. Everything happens in your browser: nothing is uploaded, nothing is downloaded.
  18. +
  19. Download C900NXS.UPD, copy it to the root of a FAT32 USB stick and run the update from the player as you would for an official update.
-
What the patcher guarantees. Only the main application is replaced. The boot loader and recovery loader (flash 0x0–0x3FFFF), the S-record header and entry record, and the DRIV and PANL sections are copied from the original byte for byte and checked again on the finished file. The file is labelled 1.32 so that the player does not skip it as the version it already has; the player still reports 1.31.
-

Without a terminal that can read single keys, name the mods instead and accept the disclaimer with --accept:

-
$ python cdj900nxs_patcher.py C900NXS.UPD --mods code,utility,overview,cuecolours,hotcuekeys --accept
+
What the builder guarantees. Only the main application is replaced. The boot loader and recovery loader (flash 0x0–0x3FFFF), the S-record header and entry record, and the DRIV and PANL sections are copied from the original byte for byte and checked again on the finished file, which is also unpacked again and compared with the patched application. The patched application is checked against the SHA-256 of the one the Python patcher makes for the same mods, so the file is the same byte for byte. The file is labelled 1.32 so that the player does not skip it as the version it already has; the player still reports 1.31.
+ +

Command line

+

The Python patcher builds exactly the same file, byte for byte. Tick the mods with the arrow keys and Space, build with Enter, accept the disclaimer by typing yes:

+
$ python cdj900nxs_patcher.py C900NXS.UPD
+

Or name the mods and accept with --accept:

+
$ python cdj900nxs_patcher.py C900NXS.UPD --mods rgb_waveform,extended_bpm,phasemeter,cuecolours,hotcuekeys,ledsaver,credit --accept

Back to stock

-

Build the original application, relabelled so the player accepts it, and update as above:

-
$ python cdj900nxs_patcher.py C900NXS.UPD --stock
-

Keep this file on a stick before you try any mod.

+

Build with no mod ticked in the builder (or --stock with the patcher): the original application, relabelled so the player accepts it. Update as above. Keep this file on a stick before you try any mod.

-

code arnoldasr

-

Three display changes tested together as one build: the detail waveform in RGB colours, the BPM shown with two decimals (stacked beside the tempo) and a two-line phase meter.

+

rgb_waveform arnoldasr

+

The big waveform in RGB colours from the track's colour waveform data, and the overview's colour table as a red-to-white gradient.

RGB detail waveform
RGB detail waveform.
-

utility arnoldasr

-

UTILITY › SLIP FLASHING becomes PHASE METER; its value chooses which phase meter is shown. Needs code.

-
UTILITY with PHASE METER
UTILITY › PHASE METER.
+

extended_bpm arnoldasr

+

The BPM to two decimals: the tenths as a small digit above, the hundredths in the place of the stock decimal.

-

overview arnoldasr

-

The overview waveform's colour table becomes a red-to-white gradient. Data only, no code.

+

phasemeter arnoldasr

+

A two-line phase meter instead of the four boxes: orange for the master, blue for this deck, each with a marker. UTILITY › SLIP FLASHING becomes PHASE METER: ON shows the lines, OFF the stock boxes. The orange marker needs another player as tempo master.

+
UTILITY with PHASE METER
UTILITY › PHASE METER.

cuecolours ours

When a track is loaded the player asks its database for the cues. This mod also asks for the extended cue list (the one newer players read from rekordbox's export), which carries the colours and the hot cues.

    -
  • Memory cues are drawn in the colour set in rekordbox, on the overview and on the detail waveform. A memory cue without a colour, or one stored on the player, stays red.
  • -
  • Hot cues A–H are drawn as small boxes with their letter, in their rekordbox colour (green when none was set).
  • +
  • Memory cues are drawn in the colour set in rekordbox, on the overview and on the big waveform. A memory cue without a colour, or one stored on the player, stays red.
  • +
  • Hot cues A–H are drawn as small boxes with their letter, in their rekordbox colour.
Overview with coloured memory cues and hot cues A to F
Coloured memory cues and hot cues A–F on the overview.
-

hotcuekeys ours

+

hotcuekeys ours experimental

The CDJ-900NXS has no hot cue pads. This mod lends it three: the top three BEAT DIVIDE keys left of the jog. It needs cuecolours, which reads the hot cues from your USB.

-
UTILITY with HOT CUE KEYS
UTILITY › HOT CUE KEYS, the new last item.
+
UTILITY with HOT CUE KEYS
UTILITY › HOT CUE KEYS.

The hot key mode

    @@ -115,14 +119,22 @@
    –
    1/4
    –
    1/8
    -

    1/4 and 1/8 do nothing in the mode and stay dark: the player has three hot cue buffers, and A–C keep them, so each of the three starts at once.

    -

    With the mode off, or HOT CUE KEYS set to OFF, the keys do what they always did.

    +

    1/4 and 1/8 do nothing in the mode and stay dark: the player has three hot cue buffers, and A–C keep them. With the mode off, or HOT CUE KEYS set to OFF, the keys do what they always did.

    + +

    Loading

    +

    After a track is loaded and the deck stands at its CUE point, hot cues A, B and C are buffered one after another, about a second each, like the CDJ-2000NXS does. Meanwhile:

    +
      +
    • the big waveform stays black and appears once all are ready;
    • +
    • PLAY, CUE and the hot cue keys are locked, so nothing interrupts the loading;
    • +
    • the key of the hot cue being loaded blinks.
    • +
    +

    Then the deck stands at its CUE point again, as before, and each of the three starts at once. This happens whether the hot key mode is on or not, as long as HOT CUE KEYS is ON.

    Play a hot cue

    -

    Press the key briefly. The deck jumps to the hot cue and plays — also when it was paused.

    +

    Press the key briefly. The deck jumps to the hot cue and plays — also from a pause, in CDJ and in VINYL mode.

    Gate

    -

    Press the key and keep holding it. The deck jumps and starts playing at once, exactly as with a short press; nothing waits. When you let go:

    +

    Press the key and keep holding it. The deck jumps and starts playing at once, exactly as with a short press. When you let go:

    • held less than 0.5 s: it simply plays on (a normal hot cue);
    • held 0.5 s or longer: it jumps back to the hot cue and stands there, ready to go again.
    • @@ -132,7 +144,8 @@

      On a key without a hot cue (LED off):

      1. Pause the deck at the place you want.
      2. -
      3. Hold the key for 0.8 s. The hot cue is set there in green, its LED blinks twice and then stays dim, and the box appears on the waveforms.
      4. +
      5. Hold the key for 0.8 s. The hot cue is set there — A in green, B in orange, C in blue — its LED blinks twice and then stays dim, and the box appears on the waveforms.
      6. +
      7. It is buffered at once, so it starts instantly from the first press. The deck then stands at the new hot cue; PLAY plays from there.

      Keys that already have a hot cue are not overwritten — delete the old one first.

      @@ -149,68 +162,61 @@
      off — no hot cue dim — hot cue there - two bright blinks — pressed, set or deleted + blinking — pressed, set, deleted or being loaded

      Good to know

        -
      • Set and deleted hot cues are saved on the USB stick, in the track's rekordbox analysis files, the same way MEMORY and DELETE keep memory cues. Like MEMORY, this works once the track has settled, a few seconds after loading.
      • -
      • A–C start at once. After a track is loaded, once the deck stands at its CUE point for a second in the hot key mode, hot cues A, B and C are loaded silently into the player's three hot cue buffers (about three seconds) and the deck goes back to its CUE point. If you press PLAY or CUE meanwhile, it waits for that (PLAY then plays); a hot cue key pressed meanwhile cuts it short and plays.
      • -
      • A hot cue you set later loads into its own buffer on its first press (about a second) and starts at once after that.
      • -
      • Hot cues D–H from rekordbox are shown on the waveforms but have no key.
      • +
      • Set and deleted hot cues are saved on the USB stick, in the track's rekordbox analysis files with their colours, the same way MEMORY and DELETE keep memory cues; the player shows MEMORY for each. Like MEMORY, this works once the track has settled. Let the last one finish before you load the next track.
      • A hot cue played from a buffer leaves the CUE point where it is. A gate's release stands at the hot cue; PLAY then plays from there.
      • -
      • A key pressed while a hot cue is still loading waits for it (the last key pressed wins).
      • +
      • Hot cues D–H from rekordbox are shown on the waveforms but have no key.
      • No slip hot cues yet: with SLIP on, a hot cue plays as without SLIP.
      -

      beatjump emulator only

      -

      With UTILITY › BEAT JUMP set to 1, 2, 4 or 8 (the item replaces SCREEN SAVER; the screen saver stays on), TRACK SEARCH ⏮ ⏭ jumps that many beats back or forward along the beat grid, keeping the position within the beat. OFF restores TRACK SEARCH.

      -
        -
      • While playing, the jump target is loaded into a buffer of the player that only beat jumps use, so hot cues A–C stay ready. There is a short silence while it loads, but the deck comes back in phase, as if it had played on through the jump: the load time is measured on every jump and allowed for, at the deck's tempo. The CUE point stays.
      • -
      • Paused, the jump is a CALL: the deck and its CUE point move by that many beats.
      • -
      • A press while a jump or a hot cue is still loading is ignored, so loads never overlap.
      • -
      • A jump while a loop plays ends the loop; LOOP IN / OUT work again right after a jump.
      • -
      • Needs hotcuekeys (its engine makes the jumps); the HOT CUE KEYS item may stay OFF.
      • -
      -
      Rebuilt: the first version jumped off the grid on a player and, after quick presses, reported a corrupted file. The new one has only run in the emulator so far; please report how it behaves on a player.
      - - -

      ledsaver emulator only

      -

      UTILITY › LED ANIMATION (the last item) is ON from the first start; set it to OFF to switch it off (the player keeps the choice). When the player's own screen saver dims the display (5 minutes without a track loaded, or 100 minutes paused or at the cue), the LEDs play a loop of about three seconds:

      +

      ledsaver ours

      +

      UTILITY › LED ANIMATION (the last item) is ON from the first start; set it to OFF to switch it off (the player keeps the choice). When the player's own screen saver dims the display (5 minutes without a track loaded, or 100 minutes paused or at the cue), the LEDs play a loop of about four and a half seconds:

      • the BEAT DIVIDE keys fade in a wave from top to bottom and back up,
      • SYNC and MASTER, MASTER TEMPO and DISC EJECT fade in and out against each other,
      • CUE, PLAY, REV, SLIP, the SLIP LOOP bar and RELOOP/EXIT light one after another,
      • the jog's three rings light in turn: inner, middle, outer, middle, inner …
      -

      Any key ends it, as it ends the screen saver. The keys without levels (CUE, PLAY …) can only switch on and off; the rings too.

      +

      Any key ends it, as it ends the screen saver.

      -

      Test status

      +

      credit ours

      +

      The boot screen says Pionaarr Mods 1.0 where it said Ver 1.31. Ticked at the start; untick it to keep the stock text. UTILITY and the LINK screen still show the real version, 1.31.

      +
      Boot screen with Pionaarr Mods 1.0
      The boot screen.
      + +

      Beat jump, stems, a uniform key notation and a 3-band waveform are in the works: see Upcoming.

      + + +

      Status

      - + - - - - - - - - + + + + + + +
      ModByStatus
      ModByStatusPlayer
      codearnoldasrran on a player
      utilityarnoldasrran on a player
      overviewarnoldasrran on a player
      cuecoloursoursran on a player
      hotcuekeysourspartly tested mode, HOT KEYS box and LEDs on a player; the hot cue buffers (A–C at once), gate, set and delete in the emulator
      beatjumpoursemulator only rebuilt on the hot cue engine
      ledsaveroursemulator only LED map from the LED tests on a player
      creditoursemulator only boot screen text (ticked at the start, can be unticked)
      rgb_waveformarnoldasrstableran on a player
      extended_bpmarnoldasrstableran on a player
      phasemeterarnoldasrstableran on a player
      cuecoloursoursstableran on a player
      hotcuekeysoursexperimentalran on a player
      ledsaveroursstableran on a player
      creditoursstableran on a player

      FAQ

      Does this touch the boot loader?

      -

      No. The patcher copies the boot loader and recovery loader from the official file byte for byte and checks them again before writing. Only the main application changes.

      +

      No. The builder copies the boot loader and recovery loader from the official file byte for byte and checks them again before you can download it. Only the main application changes.

      +

      Is my firmware uploaded anywhere?

      +

      No. The builder runs entirely in your browser; everything it needs comes with the page.

      Can I go back?

      -

      Yes, build --stock and run the update. Keep that file ready before flashing mods.

      +

      Yes, build with no mod ticked (or --stock) and run the update. Keep that file ready before flashing mods.

      Do I need rekordbox?

      Cue colours and hot cues come from a USB stick exported with rekordbox. Without them the mods show what the stock player shows.

      Why is the version still 1.31?

      -

      The update file says 1.32 so the player installs it; the application inside still reports 1.31 (in UTILITY and on the LINK screen). Only the boot screen says Pionope Mods by Givou instead of Ver 1.31.

      +

      The update file says 1.32 so the player installs it; the application inside still reports 1.31 (in UTILITY and on the LINK screen). Only the boot screen says Pionaarr Mods 1.0 instead of Ver 1.31.

@@ -218,7 +224,7 @@
Made with Claude Code · Claude Opus 5.5
-

This website, the mods beatjump, cuecolours and hotcuekeys, and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods code, utility and overview are by arnoldasr.

+

This website, the builder, the mods cuecolours, hotcuekeys, ledsaver and credit, and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods rgb_waveform, extended_bpm and phasemeter are by arnoldasr.

Disclaimer

diff --git a/index.html b/index.html index 0f43aef..c9b22bc 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ CDJ-900NXS Mods - + @@ -18,6 +18,8 @@ CDJ-900NXS Mods @@ -31,13 +33,13 @@

New tricks for an old favourite.

A set of firmware mods that bring features of newer players to the CDJ-900NXS: - an RGB waveform, rekordbox cue colours, visible hot cues and - hot cues you can play from the deck itself. Built into the player's own - update file by a patcher that leaves the boot loader untouched. + an RGB waveform, a BPM to two decimals, a phase meter, rekordbox cue colours and + hot cues you can play from the deck itself. Build your own update file + right here in the browser — the boot loader stays untouched.

@@ -56,10 +58,10 @@
-
6
mods, pick the ones you want
-
A–C
hot cues on the BEAT DIVIDE keys, at once
+
7
mods, pick the ones you want
+
A–C
hot cues on the BEAT DIVIDE keys, loaded in the background
0 bytes
changed in the boot loader and recovery
-
1 file
a normal C900NXS.UPD for the player's USB update
+
1 file
a normal C900NXS.UPD, built in your browser
@@ -67,83 +69,82 @@

The mods

-

Each one compared with stock. Three are by arnoldasr, three are ours. Choose any combination in the patcher.

+

Each one compared with stock. Three are by arnoldasr, four are ours. Choose any combination in the builder.

- ran on a player - emulator only - partly tested - known faults + stable + experimental + ran on a player
- +
-

RGB waveform, BPM & phase meter code

+

RGB waveform rgb_waveform

by arnoldasr
-
ran on a player
+
ran on a playerstable
-

The detail waveform in RGB colours, as on newer Pioneer players, instead of plain blue. Plus the BPM with two decimals and a two-line phase meter.

+

The big waveform in RGB colours, as on newer Pioneer players, instead of plain blue — and the whole-track overview at the bottom as a warm red-to-white gradient, so drops and breaks stand out.

Stock blue detail waveform
Stock
-
RGB detail waveform
Modded
+
RGB detail waveform
RGB
-
-
Stock BPM 145.0
BPM · stock
-
BPM with two decimals
BPM · two decimals
+
+
Stock blue overview waveform
Overview · stock
+
Gradient overview waveform
Overview · gradient (with cue colours and hot cues)
    -
  • StockBlue waveform, BPM to one decimal, phase meter as four boxes.
  • -
  • ModdedRGB waveform, BPM to two decimals (stacked), phase meter on two lines.
  • +
  • StockBlue waveform, flat blue overview.
  • +
  • ModdedRGB waveform, red-to-white overview.
- + +
+
+ +
+

Extended BPM extended_bpm

+
by arnoldasr
+
+
ran on a playerstable
+
+

The BPM to two decimals: the tenths move into a small digit above, the hundredths take the place below — handy for matching tempos by ear and by eye.

+
+
Stock BPM 145.0
Stock
+
BPM with two decimals
Two decimals
+
+
    +
  • StockBPM to one decimal.
  • +
  • ModdedBPM to two decimals, stacked.
  • +
+
+ +
-

Phase meter switch utility

-
by arnoldasr · needs code
+

Phase meter phasemeter

+
by arnoldasr
-
ran on a player
+
ran on a playerstable
-

Choose the phase meter in UTILITY. The rarely used SLIP FLASHING item becomes PHASE METER.

+

A two-line phase meter instead of the four boxes: orange for the master, blue for this deck, each with a marker that sweeps along the bar. Switched in UTILITY, where the rarely used SLIP FLASHING item becomes PHASE METER.

-
Stock phase meter: four boxes
Phase meter · stock
-
Two-line phase meter
Phase meter · two lines
+
Stock phase meter: four boxes
Stock
+
Two-line phase meter
Two lines
UTILITY menu with PHASE METER item
UTILITY › PHASE METER
    -
  • StockUTILITY › SLIP FLASHING.
  • -
  • ModdedUTILITY › PHASE METER picks the meter.
  • -
-
- - -
-
- -
-

Overview colour gradient overview

-
by arnoldasr
-
-
ran on a player
-
-

The whole-track overview at the bottom of the screen gets a warm red-to-white colour gradient instead of flat blue, so drops and breaks stand out.

-
-
Stock blue overview waveform
Stock
-
Gradient overview waveform with coloured cues and hot cues
Modded (with cue colours and hot cues)
-
-
    -
  • StockFlat blue overview.
  • -
  • ModdedRed-to-white gradient.
  • +
  • StockFour boxes; UTILITY › SLIP FLASHING.
  • +
  • ModdedTwo lines with markers; UTILITY › PHASE METER picks the meter.
@@ -155,9 +156,9 @@

Cue colours & hot cues shown cuecolours

ours · made with Claude Code
-
ran on a player
+
ran on a playerstable
-

Memory cues take the colour you gave them in rekordbox. The hot cues stored on the USB (A–H) appear as lettered boxes in their rekordbox colour on the overview and the detail waveform — the stock 900NXS ignores them completely.

+

Memory cues take the colour you gave them in rekordbox. The hot cues stored on the USB (A–H) appear as lettered boxes in their rekordbox colour on the overview and the big waveform — the stock 900NXS ignores them completely.

Stock: red memory triangles only
Stock
Hot cue A box on the detail waveform
Hot cue A on the waveform
@@ -177,15 +178,15 @@

Hot cue keys hotcuekeys

ours · made with Claude Code · needs cuecolours
-
partly tested
+
ran on a playerexperimental
-

Play your rekordbox hot cues on the deck. A short press of QUANTIZE switches the hot key mode; the top three BEAT DIVIDE keys become hot cues A–C, ready to start at once. Press to jump and play, hold for a gate, set new ones while paused and delete them with DELETE. The key LEDs show which hot cues exist.

+

Play your rekordbox hot cues on the deck. A short press of QUANTIZE switches the hot key mode; the top three BEAT DIVIDE keys become hot cues A–C. After a track loads they are buffered in the background, so each starts at once — also from a pause, in CDJ and VINYL mode. Hold for a gate, set new ones while paused, delete them with DELETE; changes are saved on the USB with colours (A green, B orange, C blue).

Stock: QUANTIZE label only
Stock
HOT KEYS box beside QUANTIZE
HOT KEYS mode on
-
UTILITY menu with BEAT JUMP and HOT CUE KEYS items
UTILITY › HOT CUE KEYS
+
UTILITY menu with the HOT CUE KEYS item
UTILITY › HOT CUE KEYS
  • StockNo hot cues on the 900NXS; BEAT DIVIDE keys only for loops.
  • @@ -194,41 +195,61 @@ Hot cue guide → - +
    - +
    -

    Beat jump beatjump

    +

    LED animation ledsaver

    ours · made with Claude Code
    -
    emulator only
    +
    ran on a playerstable
    -

    TRACK SEARCH jumps 1, 2, 4 or 8 beats along the beat grid instead of changing the track, set in UTILITY › BEAT JUMP (which replaces SCREEN SAVER).

    +

    When the screen saver dims the display, the deck comes alive: the BEAT DIVIDE keys fade in a wave, SYNC and MASTER pulse against each other, the transport keys light one after another and the jog's three rings run inner, middle, outer and back — a loop of about four and a half seconds. Any key ends it. UTILITY › LED ANIMATION, on from the first start.

      -
    • StockTRACK SEARCH changes the track; UTILITY › SCREEN SAVER.
    • -
    • ModdedTRACK SEARCH jumps beats; UTILITY › BEAT JUMP OFF / 1 / 2 / 4 / 8.
    • +
    • StockThe screen saver only dims the display.
    • +
    • ModdedButton LEDs and jog rings animate while it is dimmed.
    -
    Rebuilt. Jumps while playing go through a buffer of their own and come back in phase; the CUE point and hot cues A–C stay. Needs hotcuekeys. Tested in the emulator only.
    + Read more → +
    + + +
    +
    + +
    +

    Boot text credit

    +
    ours · made with Claude Code
    +
    +
    ran on a playerstable
    +
    +

    The boot screen tells you what you are running: Pionaarr Mods 1.0 instead of Ver 1.31. Leave it out to keep the stock text.

    +
    +
    Stock boot screen: Ver 1.31
    Stock
    +
    Boot screen: Pionaarr Mods 1.0
    Pionaarr Mods 1.0
    +
+

More in the works — beat jump, stems, a uniform key notation and a 3-band waveform: see Upcoming.

-
+
-

One patcher, your choice

-

The patcher takes the official 1.31 update, adds the mods you tick and writes a new C900NXS.UPD. Only the main application changes; the boot loader, the recovery loader and the other sections are copied byte for byte and checked again before the file is written.

+

Build it in your browser

+

The builder takes the official 1.31 update, adds the mods you tick and gives you a new C900NXS.UPD — all on your computer, nothing is uploaded. Only the main application changes; the boot loader, the recovery loader and the other sections are copied byte for byte and checked again before you can download the file.

+ +

Prefer the command line? The Python patcher builds the same file, byte for byte:

# pick the mods in a list $ python cdj900nxs_patcher.py C900NXS.UPD # or name them -$ python cdj900nxs_patcher.py C900NXS.UPD --mods code,utility,overview,cuecolours,hotcuekeys +$ python cdj900nxs_patcher.py C900NXS.UPD --mods rgb_waveform,extended_bpm,phasemeter,cuecolours,hotcuekeys # back to stock $ python cdj900nxs_patcher.py C900NXS.UPD --stock
-

Before it builds, the patcher shows a disclaimer that you must accept. Full install guide →

+

Both ask you to accept a disclaimer first. Full install guide →

@@ -237,7 +258,7 @@
Made with Claude Code · Claude Opus 5.5
-

This website, the mods beatjump, cuecolours and hotcuekeys, and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods code, utility and overview are by arnoldasr.

+

This website, the builder, the mods cuecolours, hotcuekeys, ledsaver and credit, and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods rgb_waveform, extended_bpm and phasemeter are by arnoldasr.

Disclaimer

diff --git a/style.css b/style.css index 3ae0a85..9abe7af 100644 --- a/style.css +++ b/style.css @@ -287,3 +287,92 @@ footer .ai .dot { width: 8px; height: 8px; border-radius: 50%; background: var(- .keys .b { font-size: 1.1rem; } .diff li { grid-template-columns: 1fr; gap: 0; } } + +/* ---- status: stable / experimental / planned / feasibility, and the "ran on a player" dot ---- */ +.state { + font: 700 .66rem/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; + padding: 6px 8px; border-radius: 7px; border: 1px solid currentColor; white-space: nowrap; align-self: flex-start; +} +.state.stable { color: var(--ok); } +.state.experimental { color: var(--warn); } +.state.planned { color: var(--accent-2); } +.state.feasibility { color: #b48cff; } +.player { + display: inline-flex; align-items: center; gap: 6px; font-size: .78rem; color: var(--muted); white-space: nowrap; +} +.player::before { + content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); +} +.player.no::before { background: var(--faint); box-shadow: none; } + +/* ---- builder ---- */ +.builder { padding: 48px 0 72px; max-width: 900px; } +.builder h1 { font-size: clamp(2rem, 4vw, 2.8rem); letter-spacing: -.03em; margin: 10px 0 10px; } +.builder .lead { color: var(--muted); font-size: 1.05rem; margin: 0 0 18px; } +.step { display: grid; grid-template-columns: 44px 1fr; gap: 16px; padding: 26px 0; border-top: 1px solid var(--line); } +.step-n { + width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; + background: var(--accent); color: #fff; font-weight: 800; +} +.step h2 { margin: 2px 0 6px; font-size: 1.25rem; } +.step p { color: var(--muted); margin: 0 0 14px; } +.drop { + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; + border: 2px dashed var(--line); border-radius: 14px; padding: 26px; cursor: pointer; text-align: center; + background: var(--card); transition: border-color .15s ease; +} +.drop:hover, .drop.over { border-color: var(--accent-2); } +.drop.has { border-style: solid; border-color: var(--ok); } +.drop input { position: absolute; width: 1px; height: 1px; opacity: 0; } +.drop-s { color: var(--faint); font-size: .88rem; } +.picks { display: grid; gap: 10px; } +.pick { + display: grid; grid-template-columns: 22px 1fr auto; gap: 12px; align-items: start; + background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 14px; cursor: pointer; +} +.pick.on { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); } +.pick input { margin-top: 4px; width: 17px; height: 17px; accent-color: var(--accent); } +.pick-main { display: flex; flex-direction: column; gap: 3px; } +.pick-name { font-weight: 700; } +.pick-name code { font-size: .78rem; color: var(--muted); margin-left: 4px; } +.pick-what { color: var(--muted); font-size: .93rem; } +.pick-by { color: var(--faint); font-size: .82rem; } +.hint { color: var(--faint); font-size: .88rem; margin-left: 10px; } +p.hint { margin: 10px 0 0; } +.agree { display: flex; gap: 10px; align-items: flex-start; margin: 10px 0; color: var(--muted); cursor: pointer; } +.agree input { margin-top: 5px; width: 17px; height: 17px; accent-color: var(--accent); flex: none; } +.agree strong { color: var(--text); } +.btn[disabled] { opacity: .45; cursor: not-allowed; filter: none; } +button.btn { font: inherit; font-weight: 700; cursor: pointer; } +.log { + margin-top: 16px; background: #07090d; border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; + font: 13px/1.7 var(--mono); color: #c9d1de; max-height: 260px; overflow: auto; +} +.log .ok { color: var(--ok); } +.log .bad { color: var(--bad); } +.result { margin-top: 18px; display: grid; gap: 12px; } +.result .btn { justify-self: start; } +.result .sum { color: var(--muted); font-size: .85rem; word-break: break-all; } +.result details { color: var(--muted); } +.result summary { cursor: pointer; color: var(--text); font-weight: 600; } + +/* ---- upcoming ---- */ +.upcoming { padding: 48px 0 72px; } +.upcoming h1 { font-size: clamp(2rem, 4vw, 2.8rem); letter-spacing: -.03em; margin: 10px 0 10px; } +.upcoming .lead { color: var(--muted); font-size: 1.05rem; margin: 0 0 28px; max-width: 760px; } +.card .meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-left: auto; justify-content: flex-end; } +@media (max-width: 560px) { + .step { grid-template-columns: 1fr; gap: 8px; } + .pick { grid-template-columns: 22px 1fr; } + .pick .state { grid-column: 2; justify-self: start; } + .card .meta { margin-left: 0; justify-content: flex-start; width: 100%; } +} +@media (max-width: 560px) { + .tabs { gap: 2px; padding: 3px; } + .tabs a { padding: 6px 8px; font-size: .8rem; } + .theme-btn { width: 32px; height: 32px; flex: none; } +} +@media (max-width: 360px) { + .tabs a { padding: 5px 6px; font-size: .74rem; } +} diff --git a/tools/make_builder_data.py b/tools/make_builder_data.py new file mode 100644 index 0000000..122a009 --- /dev/null +++ b/tools/make_builder_data.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +"""Writes builder/data.js, the data the web builder (builder.html) applies. + + usage: make_builder_data.py [--repo ../CDJ900NXSVirtual] + +Our mods (cuecolours, hotcuekeys, ledsaver, credit) are made by the emulator +checkout's mods/patch_cdj900nxs.py for every combination the builder offers; +their bytes do not depend on which of arnoldasr's mods are chosen (checked +below). For each combination the bytes our mods change in the application +go into data.js, except what is the player's own firmware: + + copy the UTILITY item table (hotcuekeys and ledsaver extend a copy of it) + is copied from the user's own application in the browser; + box the HOT KEYS box is the QUANTIZE label's image recoloured with + HOT KEYS written on it; the browser makes it from the user's image. + +arnoldasr's mods (credited on the site), as patch_cdj900nxs.py splits them +up, are in data.js the same way: per combination of extended_bpm, phasemeter +and rgb_waveform the bytes they change, except the asset blob at 0x08FC2D00, +which starts with a copy of the firmware's resource table: the browser makes +it from the user's application (the port of patch_cdj900nxs.assets()). +The SHA-256 of the finished application for every combination is in data.js +too; the browser checks its result against it. + +Before data.js is written, the browser's way of applying the data is redone +here in Python for every combination of arnoldasr's mods and ours and +compared byte for byte with patch_cdj900nxs.py's result. +""" +import argparse +import base64 +import hashlib +import itertools +import json +import os +import struct +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +WEB = os.path.dirname(HERE) +OURS = ('cuecolours', 'hotcuekeys', 'ledsaver', 'credit') +NEEDS = {'hotcuekeys': 'cuecolours'} +UT_TABLE_STOCK = 0x08C6C8DC +UT_TABLE_LEN = 17 * 52 +BASE = 0x08000000 +ASSETS = None + + +def sha(b): + return hashlib.sha256(b).hexdigest() + + +def valid(combo): + return all(NEEDS[m] in combo for m in combo if m in NEEDS) + + +def runs(base, final): + """[(address, bytes)] where final differs from base (short equal gaps joined).""" + n = len(final) + base = base + bytes(n - len(base)) + out, i = [], 0 + while i < n: + if base[i] != final[i]: + j = i + while j < n and (base[j] != final[j] or (j + 16 < n and base[j:j + 16] != final[j:j + 16])): + j += 1 + out.append((BASE + i, final[i:j])) + i = j + else: + i += 1 + return out + + +def ops_for(stock, final, boxes): + """The browser's operations for one combination: data, copy, box.""" + table = stock[UT_TABLE_STOCK - BASE:UT_TABLE_STOCK - BASE + UT_TABLE_LEN] + out = [dict(box, op='box') for box in boxes] # whole, outside the runs + masked = bytearray(final) + padded = stock + bytes(len(final) - len(stock)) + for box in boxes: + off = box['addr'] - BASE + masked[off:off + box['len']] = padded[off:off + box['len']] + for addr, data in runs(stock, bytes(masked)): + cuts = [] # (start, end, op) inside this run + k = data.find(table) + while k >= 0: + cuts.append((k, k + UT_TABLE_LEN, {'op': 'copy', 'addr': addr + k, 'src': UT_TABLE_STOCK, + 'len': UT_TABLE_LEN})) + k = data.find(table, k + UT_TABLE_LEN) + cuts.sort(key=lambda c: c[0]) + pos = 0 + for lo, hi, op in cuts: + if lo > pos: + out.append({'op': 'data', 'addr': addr + pos, 'data': data[pos:lo]}) + out.append(op) + pos = hi + if pos < len(data): + out.append({'op': 'data', 'addr': addr + pos, 'data': data[pos:]}) + return out + + +def apply_ops(app, stock, ops, hk, size): + """What builder.js does with the operations (redone here to check them): + the application grown with zeros to `size` first.""" + out = bytearray(app) + bytes(max(0, size - len(app))) + for o in ops: + off = o['addr'] - BASE + if o['op'] == 'data': + out[off:off + len(o['data'])] = o['data'] + elif o['op'] == 'copy': + out[off:off + o['len']] = stock[o['src'] - BASE:o['src'] - BASE + o['len']] + elif o['op'] == 'assets': + out[off:off + o['len']] = ASSETS(stock)[0][:o['len']] + else: + _, _, px = hk.box_pixels(stock, o['res'], o['bg'], o['ink']) + out[off:off + o['len']] = px + return bytes(out) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--repo', default=os.path.join(os.path.dirname(WEB), 'CDJ900NXSVirtual')) + args = ap.parse_args() + sys.path.insert(0, os.path.join(args.repo, 'mods')) + sys.path.insert(0, os.path.join(args.repo, 'mods', 'cdj900nxs')) + import patch_cdj900nxs as p + import hotcuekeys as hk + import cuecolours + with open(os.path.join(args.repo, 'extract', 'cdj900nxs', 'main_unpacked.bin'), 'rb') as f: + stock = f.read()[:p.APP_SIZE] + if sha(stock) != p.APP_SHA256: + raise SystemExit('not the 1.31 application') + + global ASSETS + ASSETS = p.assets + _, labels = hk.routine(hk.DECODER, cuecolours.HOT_TABLE) + w, h, on = hk.box_pixels(stock, hk.RES_QUANTIZE, 0xF800, 0x0000) + boxes = [{'addr': labels['PIX_ON'], 'len': len(on), 'res': hk.RES_QUANTIZE, 'bg': 0xF800, 'ink': 0x0000}, + {'addr': labels['PIX_OFF'], 'len': len(on), 'res': hk.RES_QUANTIZE_OFF, 'bg': 0x0000, 'ink': 0x4208}] + + combos = [c for n in range(1, len(OURS) + 1) for c in itertools.combinations(OURS, n) if valid(c)] + variants, chunks = {}, {} + + def encode(ops): + enc = [] + for o in ops: + if o['op'] == 'data': + key = sha(o['data'])[:16] + chunks[key] = base64.b64encode(o['data']).decode() + enc.append(['data', o['addr'], key]) + elif o['op'] == 'copy': + enc.append(['copy', o['addr'], o['src'], o['len']]) + elif o['op'] == 'assets': + enc.append(['assets', o['addr'], o['len']]) + else: + enc.append(['box', o['addr'], o['res'], o['bg'], o['ink']]) + return enc + + # arnoldasr's mods, split up: per combination their bytes, but the asset + # blob (it starts with a copy of the firmware's resource table) made by + # the browser from the user's own application + upstream, up_apps = {}, {(): stock} + for k in range(1, len(p.UPSTREAM) + 1): + for up in itertools.combinations(p.UPSTREAM, k): + app = p.patch(stock, list(up)) + blob_len = len(app) - (p.DATA_BASE - BASE) + masked = bytearray(app) + masked[p.DATA_BASE - BASE:] = bytes(blob_len) + ops = ops_for(stock, bytes(masked), []) + ops.append({'op': 'assets', 'addr': p.DATA_BASE, 'len': blob_len}) + if apply_ops(stock, stock, ops, hk, len(app)) != app: + raise SystemExit('%s: the operations do not give the patcher result' % ','.join(up)) + upstream[','.join(up)] = {'size': len(app), 'ops': encode(ops)} + up_apps[up] = app + print('%-40s %3d operations' % (','.join(up), len(ops))) + for combo in combos: + final = p.patch(stock, list(combo)) + ops = ops_for(stock, final, boxes if 'hotcuekeys' in combo else []) + if apply_ops(stock, stock, ops, hk, len(final)) != final: + raise SystemExit('%s: the operations do not give the patcher\'s result' % ','.join(combo)) + variants[','.join(combo)] = {'size': len(final), 'ops': encode(ops)} + print('%-40s %3d operations' % (','.join(combo), len(ops))) + + # the browser applies ours on top of whatever of arnoldasr's was applied: + # check every pair, and note the result's SHA-256 for the browser to check + expect = {} + for k in range(0, len(p.UPSTREAM) + 1): + for up in itertools.combinations(p.UPSTREAM, k): + base = up_apps[up] + if up: + expect[','.join(up)] = sha(base) + for combo in combos: + final = p.patch(stock, list(up) + list(combo)) + alone = p.patch(stock, list(combo)) + ops = ops_for(stock, alone, boxes if 'hotcuekeys' in combo else []) + if apply_ops(base, stock, ops, hk, len(alone)) != final: + raise SystemExit('%s + %s: differs from the patcher' % (','.join(up), ','.join(combo))) + expect[','.join(list(up) + list(combo))] = sha(final) + print('checked: every combination of arnoldasr and ours gives the patcher bytes') + + glyphs = {c: rows for c, rows in hk.GLYPHS.items()} + data = { + 'stockUpdSha256': '', # filled from the patcher's upd.py below + 'stockAppSha256': p.APP_SHA256, + 'appSize': p.APP_SIZE, + 'dataBase': p.DATA_BASE, + 'upstream': upstream, + 'ours': variants, + 'expect': expect, + 'box': {'resTable': hk.RES_TABLE, 'width': w, 'height': h, 'glyphs': glyphs, + 'text': 'HOT KEYS', 'gap': 3, 'space': 5}, + 'chunks': chunks, + } + sys.path.insert(0, os.path.join(os.path.dirname(WEB), 'CDJ900NXSPatcher')) + import upd + data['stockUpdSha256'] = upd.STOCK_UPD_SHA256 + for key, b64 in chunks.items(): # no firmware in what we ship + blob = base64.b64decode(b64) + for i in range(0, max(0, len(blob) - 47), 8): + win = blob[i:i + 48] + if len(set(win)) > 2 and stock.find(win) >= 0: + raise SystemExit('chunk %s holds firmware bytes (at %d)' % (key, i)) + out = os.path.join(WEB, 'builder', 'data.js') + os.makedirs(os.path.dirname(out), exist_ok=True) + with open(out, 'w', encoding='utf-8', newline='\n') as f: + f.write('// Generated by tools/make_builder_data.py -- do not edit.\n') + f.write('window.BUILDER_DATA = ') + json.dump(data, f, separators=(',', ':')) + f.write(';\n') + print('wrote %s (%d bytes, %d + %d variants, %d chunks)' % (out, os.path.getsize(out), len(upstream), len(variants), len(chunks))) + + +if __name__ == '__main__': + main() diff --git a/upcoming.html b/upcoming.html new file mode 100644 index 0000000..d142a13 --- /dev/null +++ b/upcoming.html @@ -0,0 +1,121 @@ + + + + + + CDJ-900NXS Mods Upcoming + + + + + + + + + +
+
+ CDJ-900NXS Mods + + +
+
+ +
+
Upcoming
+

What comes next

+

Ideas we are working on or looking into. None of them is in the builder yet. Planned means we know how to do it; feasibility means we are still finding out whether the player can do it at all.

+
+ planned + feasibility +
+ +
+ +
+
+ +
+

Beat jump beatjump

+
ours · made with Claude Code
+
+
emulator onlyplanned
+
+

TRACK SEARCH jumps 1, 2, 4 or 8 beats along the beat grid instead of changing the track, chosen in UTILITY › BEAT JUMP. While playing, the jump goes through a buffer of its own and comes back in phase; the CUE point and the hot cues stay.

+
    +
  • NowBuilt and working in the emulator, on top of the hot cue engine.
  • +
  • NextTesting on a player before it goes into the builder.
  • +
+
+ +
+
+ +
+

Stems

+
ours · idea
+
+
feasibility
+
+

Two stem modes on the two BEAT DIVIDE keys the hot cues leave free: one to take the vocals out, one to take the melody out — like the stem buttons on the newest players.

+
    +
  • QuestionWhether the player's audio DSP has the room and the speed to split a track while it plays, a job it was never designed for.
  • +
  • NextMeasuring what the DSP has to spare, then deciding.
  • +
+
+ +
+
+ +
+

Uniform Camelot / Open Key

+
ours · idea
+
+
planned
+
+

Every track's key in one notation of your choice — Camelot (8A) or Open Key (1m) — chosen in UTILITY, on the main screen and in the browser, whatever notation the USB was exported with.

+
    +
  • NowThe player shows the key as it was exported.
  • +
  • PlannedOne notation everywhere, set on the player.
  • +
+
+ +
+
+ +
+

3-band waveform

+
ours · idea
+
+
feasibility
+
+

The three-band waveform of the newest players: lows, mids and highs drawn as layers in their own colours, so kicks, vocals and hi-hats can be told apart at a glance.

+
    +
  • QuestionWhether the band data rekordbox exports for newer players can be read and drawn fast enough on this one.
  • +
  • NextReading the data on the player, then a renderer.
  • +
+
+ +
+
+ + + + +