From ca44aa833d550cef0b945832f669e0a88a055967 Mon Sep 17 00:00:00 2001 From: Me Here Date: Fri, 29 May 2026 09:41:57 -0500 Subject: [PATCH] PM_K-1 firmware: screen refinements (run-state bg tint, time/bar totals, per-track log, square/circle pads, separated track #) App.py-only (ships over the one-click updater). APP_VERSION -> 0.0.3. - Run/stop is now a background tint (gray running / near-black stopped) instead of STOP text, reclaiming the space. - Running time + bar counter show "of total" when the track has a b length: "1:23 of 2:00" and "bar N of 16" (bar cycles 1..N); total time derived from bars x master-beats-per-bar x 60/bpm. Parser now reads the b token. - Practice log is filtered to the current track (drops the redundant track column). - Pads: squares for the main pulse, circles for subdivisions (was square + hollow outline); fewer vectorio shapes too. - Track number set apart from the title (small + dim, right) so it no longer reads as part of the title. On-device editing (tap instrument -> lane table; tap beat -> cycle state; dirty-name -> confirm save/revert) is deferred to Phase 2, where "save" has a correct destination (an edited built-in saves as a user copy). Co-Authored-By: Claude Opus 4.7 (1M context) --- pico-cp/README.md | 6 +- pico-cp/__pycache__/app.cpython-312.pyc | Bin 53218 -> 54126 bytes pico-cp/app.py | 129 ++++++++++++------------ 3 files changed, 70 insertions(+), 65 deletions(-) diff --git a/pico-cp/README.md b/pico-cp/README.md index 0732b83..4ac4cdb 100644 --- a/pico-cp/README.md +++ b/pico-cp/README.md @@ -62,7 +62,11 @@ The editor also syncs the device clock, so the practice log gets real wall‑clo - **Joystick:** up/down = tempo, left/right = previous/next groove. - **Button A (GP15):** play / stop. **Button B (GP14):** tap tempo. -- **Touchscreen:** the bottom of the screen shows the **practice log** (time · BPM · duration · track) — +- **Screen:** VARASYS logo + MIDI/USB status icons up top; the **background tints gray while running** + (black when stopped). Running time and bar count show **of the segment total** when the track has a + bar length (`b`), e.g. `1:23 of 2:00` and `bar 3 of 16`. Main beats are **squares**, subdivisions + are **circles**, with vertical gridlines lining the beats up across lanes. +- **Touchscreen:** the bottom shows the **practice log for the current track** (time · BPM · duration) — newest first. Plays under 5 s aren't logged. **Tap a row to arm it (turns amber), tap again to delete.** - **RGB LED** flashes the beat (amber accent / cyan normal / violet ghost); the **buzzer** clicks to match. - The log is saved to `/history.json` (next to `programs.json`) in appliance mode and survives power‑cycles. diff --git a/pico-cp/__pycache__/app.cpython-312.pyc b/pico-cp/__pycache__/app.cpython-312.pyc index d8a869830d4483242123439a201b86c96a002841..ef0b6d69aa312e9c4f837bcf53e5d8bb4484c592 100644 GIT binary patch delta 10258 zcma)C3tW>|md{PTFC>HzAmRNAFC|nI5vi}LAk+tnJQWbk1iqj=W50a(z@%u$oq1#gWG7XmZZj#x#~|ZXH16r}^6i#R ztKG{rT12~7A=%oz94k>m3nM+48n<{HxVPcQ{t=KvvYWx$L+b{_-mamP!MvNguw(oB z_8mQNtf#MM&_1b4o0h5R8>xkvy$|%|4^%#U-(clX_)x^)_S5E5=BdPmxAaj1_QA!2 zxdZ#?s@|fZ7)sJom3lRw{-uGM(o$3->2A$JH@DDLmUfK``#B6L{u0UiNG9l_%oybr zWZ$J*GP8r<1J)~V+AUtCM>2EBlcq@(oqzE+q=rn%sDK_Me~usA#6K_-E|)6_Rg}e1r80u@d?J zqCd{dAR5;TdFk>j)AB^7**FFhDs}`KiA~6D zN5VSZK@Q2EXyXbKx${i_3PQf6kF30hkA~jquAi^e$u-D|VMrqAjr+3rY+#pBd->)82CS$dc9$k~R0t-#N_zcQkg2nrhJAh;nWbHg~Fm?ary_{%mlEi$dDWLzl zW*vNs1#68YlWtjiU2}jlnY@aEJ$sr6J-)&~9(ENM{F|bZbI8n#K?+Mk2;_1DIcZmb zJL_klHZM>ns3EsBkdsg^EVXu0Pntej8`y5pYFVI8veT!Et z`cbh`zxRKs)7-DyunU@+R|Oid+oTmL+F zKxPV~=^NINRaDrpe6@&67I!0g6NsrG^R5?btg+ARmfG|VYB=C1SdW#-Cto7u0T z0-x#Lp`UF?BfoN~H%60i@gns1D#TrlB5n@x6S`p2QYvj~P$(OB(W{$m+;5?nZY(KM z-j83h!9`29lI+^LiK?r$?p=G_^*@-Z7jDI5Jg)2I`n5MBW8qiut!U$o=uA&^=1BNt z^gUF$WjU?cvQl|T8Z%ssr*ungxUNq(rHUQQyAgknC;pxf;kR{XMx&L$@ZA z-E`qr3#p<5Tes59LU5|Y;s8uVHs^z(D`Lq(T6Oe+NWCiDUqEf$k@TB^s5r^sFOf-6 z0Zt$SIVYznW%`tuK$*B&iVyJ8OsSMW9ldeXR;Eo>IOOJ8C(A)DEYO~f{+sfo^v?UF zv_PT2OX&e#N|@n->95P;zavcfJ;IXjnVGl{)D4Sws-$@VDT1n5E90C)0~M*vpX#WmH0i&Fn(90ymc*(w~@$T(`idt ztQ2#>_HUyJip4;@LE8!{3My+W**?4-g5~?5g}4_9Dn>*@6x)E9Qs}R%lQPgldbxGx z!u8^QY&3@TdB`nA;&-)S+Y-nUn!Ro566PJfLF>$A1*OHXe|MQNCM$QDrIrpW%UVRt zc3OPd7GXaf*w)ta5J>9|1NXVm+}PF9E|~32)=p~&l-hG3>K~Hb(#2p%p41t8xv`?5 z^yd}~FPKVQek&^ei6Yttn^$vF6>?0|r#Z3VhGCw^FmJTz)spiiZ*@&o?{J%c;1;Y? zhUO{NF4yMm351T044*B@%`CFEB$r!EH8u(2)$Xj|QO@?+PVb*&`wSGeHk zajnlH=L;3Iu---S;)jn}g_`fusn*?|;oI=*kjZ zb2N&EUDVQUQO|zw-I)l!v^ACvFHWGNXN-#v$j-=pAI}sjvcW%ro?;8m;8Wxfy@;H$V?y;(KmLhP}}V`i$GD!{WQ;< zAC-0Q7&n=9pGP$xG^!d%Kan*kJkdfO<~-e0VWqpe_C|H1r@HZjYQg0)*N{ktWDu2! z2wX%zkTF_{$Q5d}EG4I0k6Y>pxk$fhOpg#@96!$RDmrcZ#MkM9rupOj7jGID9FU1FC^ZF(yHVsTK)f2WU9yOhnMIv#Sj@)s(8$(g zy*HRCQ|#(&+$A2tN;h5EnyN&|Zf~Vctvh0m{f{`nph}aR-qjk&Q}ol;(x6NSfw(BA zWv&hM2W=72ym%CKbO8zbem_IeN9d8ZMapp$y@p5ZS$q{+^?}u zJ(8VBzChwP-$jQClTjbOFa9ZVY%3!u@qyjX>A;~1jnDkB@Q3vGhf;GN05&j#KcE73 zBEh-LL91%WdU@DM9d>}=1&4FV9hY=?vw}QIFCWR&AIECt{aZ_`%$t3l+D*Sbl0xEW z(%+QO-}R~ah0x+84gXObbu^el1UQby$ppEekd*Ynf&{AiQ5yZ@fLGECsR zVHQ#VtjK6*cN7M|q-J@y5>BH^A!s&INwZGxfun{VL5_j!oa(HFp?77k=2= z2wS`P3}I1*;4=*7R|>E`99WZ9d;yw?Pab*!=SvYfyV{2eXTg0;h^OSkKb&4nH#gF$|qScPIvcSyyL7l^{9;m1uOw zR`#uQYf>+9?}WS&^2+w9ifuzhy5~xUOGe3XDTB+81k*#!@vfNO2(p6#2t3A!TR<|# z|MeBc#ib(Z-!~10GtjC<3^(HU&gzFyGiO#UO}0*Z8a>=+j)JMpBs`QU;O8rWZmo^& zaS{jM;5Iq=EhAE!n?7(55k3l07h3nYB*Cfmg-rsYRqye^+L1TSnSaiyH#E88jIW7>~3xM zSliv5Hn%8wIwiNz<*Dg%C#kypHU+{139n_CiZ0Q^XXCm3L=2^5aSSKOx(NNto>)5i zFh{uu^nQfmyG-f?l{n>;JP=J50ga0B!53fU?1hq`z{+%gC5Nana9@L}1}VH#;UHp) zq^1L<3G7B|$HQ?-s31;`jy@KZI#-xOKDW1%ho18UC3W{ka}f@n`JE)@TomYgIynaq z9}YMy1Cc>a)>dBSpuc%7k`AN<^N$lj<;%Hyqe2{tZZH@@jTHS=bhl>qEcQ5gQsE4O zt0yHJNaOQkPE}{rjJOD>*Ep0o92_{{q}ytsFC2}Q=Q${OF^Yy4Mix4Pgb+b1#lfst znsaQL0sd$G7jl62@wAMPoF#_}BpR8-r9O#U4o3B;Kw#+ip2NORAc&Yf)Ez+huk*uy z2Fh5FWyowd4B0IQ_xe+0IHRrE=)Z#~UE3W37iPu`I~Mk22;E9w*5{h@p#AMCASS&J zU^sCPGyKUg4GVUkV;PIKcs3W4!5Dl|Pr?f3yB|mHJ8ruR%boz@RjxB{sjezEsdHK6 zh0}dkr|S}J-Hz5yM@J1kx*3PoVTJH-Ux1qq zoeFrg(1>I0eeKsnR}4z0drtL?3hv|;y^5Qr>=)ynkL#6=b@z1-rt~=nw_n#~jD`1d zH#e1D%X&9!ZjDEmaZV4rlzTP!eDK)f^V%`1C#PuARD2zFY2A+A9>s@Cm-P$BI{P{& zLz5tqBo8NF56u~~^vZ9B8BS`CYoFB*m5*@GBzVHId-)G5s(Yo;^jA$UnFjeG?m6|a z+7n|MDYzb!eJNui<+ACzZi{=zPM9Qkt-ol&V$wNyQLsgPp#wwg=Q+YXxa=!OIa&Onzkks-7u zGz9;B;p_CFN3#LDk3MRmANNJd=PB_hFpZ z*a*J;PxDU9U-2SrvS_%S4#pksh<5O}YaQX(2TPnWP!_?wK@hvSUMvL;&2+64Nz1FE zavTQu7ZVr>KhM#{oiX&^PZ((JPx2cLvsDA6f0C04 zk+4%|E(&0uMwQ;ifa^oIY@cGF@!gUkCh`Gpwu{*YZo|gxh(3YboAk?{M6Jw)hJdqs zp~R~cEka9|eGW!M++Jl$@*&>SD3cv#ZOHAR%TF%oVQ2VGl*b@^KGfh=Twk%V$am)L z!@3yU&S^*(s(1#=mY|T?%Yb%Y&la(%-7027Q?I7U)@~EcEuDgOKMVP$I3K8H2h)Q% zV~$Roj7|DS*0rP2YJok?H9|M>k0|_gBunUDPR1;q(Snz^OV&O6yqvv7u!57Y2wrZF zMX>wd0Gjsi5$zz^j{cZh24m>H!35=6n2!Cb>(ro{kTCkpsn?g>iijMrJfR<~8Y(|k zJHn5Kp9vliPU}wTMyp<} zKVLtQ2Jp~bQ#Tc}W1!%sG5tC1uy({U8a`=UGC)pfq0x@>J8mpp?^(Kjf=n&lG*NV| zMHZ%ix_w`|~PP2Uu zKV_^|fK5Yi^d+!q&L4X)*x-TGXKDBy87xP;@I{V%hW7rns$0|`B>igeHZSVFkFkf66dbkb|ncu#^lPKL+L}( zaMo1nGH{(I(gp--x~_>I6<+N)-*KtvolTcFO^BCE-BmS{`L*u)22XwiSV8)!x~aq^ z^ug1MA(dn%-dK`TvmbN~iM?EGv6f=5+Cn|(Mvnmh}LPgPkE>`yRH^H7q z{rH2w3}bUZKfaBr5~oo${|_bk?{%1x(3-_{s0MUaMo7L#5-(~@j~WxLc5Arregjoz zw_sz5lHrxcF=^a+!8uWNt?o)4*x{9(?gq=`n#LhvByYHNYThbW$*_q;Pk|_}+-3)t zVnywXJ76SHNsm6AKn}P@o*pK-4rl`3t{p2rL-JcB%v-R=8*xndfeHGichGh4**T2R7mbY=I$`&2h%W`%hz$0IDmVM$AQ|bobIBT&Q{ja7Vu#ACq6KAoU20$sY)`9b*}0T* z)oiE|craLuMJyeVNi)3kS3Thp9$Gln(tLkYnPi&bvgl>KJ})RBHLzO(d3f1=8~O$V z77j+0uw`pt*&2wsj>uKGT4xBnGb;rZ)&qP8#mvcp!|`KvEui-hQ7&lUFc1<@Td`IM z4U7rBbST52ZRQw?51rMPBXm|8 zp$5Gm=urO;ZDFaVSrO0yoCM~!(V{N<-vNj59sM_f8*frLl}>&iJf@X)$CgzTtgG6% zuGk1hYRooPZP-|8tb(!E7%zU_9qJq4ItR7*e!^2US}V| z!M?x>PPFX5b9s z9(U66i}`QmU(K5|l=Z256}{yH#9deKuBe`>*gn{(Y zbKFW>FjDnm&6%3XwEP>o>Qi~}CjpkRl<|xU8JCo=XHVo^TYhEvJ=;4r76qqsW!XEVuu+_)$Me5 z?Y+_E^mIAz!p#FIkbf`yFv zwsOSqOzmX!qRH@_X_-i3oN_r{-XV`>CW%HCfD{$L!`0}WpCv{r zu@k%UGB;bTg1MtbXnERYe6=m8978cUa4T+!RwR1t){I;ca=!uMmG5f=ls(&~CD3MW z>FozuDZTZNc^aGyz*DISZpq`vvy>Q7#cyfJcvAZBu+=9>E+Ao-mMnO(NVgsb-asE2 zk0WY2GM*2Q${&qKkr!OwjK9Q_PI~6;^le{Zzt54(Mndcds93T{zKHoP4j78U@aoU^ zlCc4^EIPMgSs@T_kZ5fNys%r`N7H_>NGGO4o9-ZnBDy-?byBDI+%yXAwm4IX+t!h zIh|+XbEj(v;eSA;IR$i{R>`58lE=PselM9;>iAkRolc+~7Z0V&D0d~D4+k!tW?zY` z$JvvKUR~&$y6Hj3wyYXRXM~c}UC;~6w+4ZoOhu7t*Qt_8q^%at@k^BdeTx_%i z$q;61k*h;eiKGL|I+5gIHV(;p968L1l!c-bSZ$!i?=4>T0T$lD{5W#2B4Hcx4sy|0 zk&Pq=37&$PZHVdgr|%__zja-G?~t4eF&qk_W$!=DYssSNB>JE4FNm*ElH@OCj7lLg X!u^o{vHGz3Z!~|nyOplK_UwNH@M35~ delta 9599 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 diff --git a/pico-cp/app.py b/pico-cp/app.py index e6ee32d..d1762cb 100644 --- a/pico-cp/app.py +++ b/pico-cp/app.py @@ -18,7 +18,7 @@ import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart -APP_VERSION = "0.0.2" # firmware version (the A/B updater pushes/compares this) +APP_VERSION = "0.0.3" # firmware version (the A/B updater pushes/compares this) try: import rtc # set from the editor's clock SysEx so the log has real timestamps except ImportError: @@ -91,7 +91,7 @@ MIN_LOG_SEC = 5 # don't log plays shorter t PAD_DIM = (0x10161E, 0x0A3A52, 0x4A3010, 0x2A1D4A) # idle pad: mute / normal / accent / ghost PAD_LIT = (0x39414D, 0x0AB3F7, 0xFF9B2E, 0x967BFF) # playhead pad: mute / normal / accent / ghost C_GRID = 0x1A2330 # faint vertical beat gridlines (beats line up across lanes) -PAD_OUTLINE = 0x33414F # off-beat hollow-square border when idle +C_RUNBG = 0x161D28 # background tint while running (vs near-black when stopped) # WS2812 RGB LED - self-contained via the core neopixel_write module (no external library) class RGB: @@ -195,17 +195,19 @@ PAT = {'X': 2, 'x': 1, 'g': 3, '.': 0, '-': 0, '_': 0} PRIO = {2: 3, 1: 2, 3: 1} def parse_program(s): - bpm = 120; lanes = [] + bpm = 120; lanes = []; bars = 0 for tok in s.strip().split(';'): tok = tok.strip() if not tok: continue if tok[0] == 't' and tok[1:].isdigit(): bpm = int(tok[1:]); continue + if tok[0] == 'b' and tok[1:].isdigit(): # b = segment length in bars (totals + Continue) + bars = int(tok[1:]); continue # (lane sounds like "beep:4" have a ':' -> not matched here) if ':' not in tok: continue lane = _parse_lane(tok) if lane: lanes.append(lane) if not lanes: lanes = [_parse_lane("beep:4")] - return max(30, min(300, bpm)), lanes + return max(30, min(300, bpm)), lanes, bars def _parse_lane(tok): poly = '~' in tok; mute = '!' in tok @@ -337,24 +339,23 @@ class App: self.jx = analogio.AnalogIn(P_JOYX); self.jy = analogio.AnalogIn(P_JOYY) self._joyNext = 0 self._touchDown = False; self._touchSeen = 0 - self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.rgb = (0, 0, 0) + self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.bars = 0; self.rgb = (0, 0, 0) self.programs = load_programs() self.dirty = True - self.pad_pal = displayio.Palette(10) # 0-3 idle levels, 4-7 lit levels, 8 off-beat border, 9 hollow bg + self.pad_pal = displayio.Palette(8) # 0-3 idle levels (mute/normal/accent/ghost), 4-7 the lit playhead for i in range(4): self.pad_pal[i] = PAD_DIM[i]; self.pad_pal[i + 4] = PAD_LIT[i] - self.pad_pal[8] = PAD_OUTLINE; self.pad_pal[9] = C_BG self.lane_pads = []; self.lane_lit = [] self.usb_conn = False; self._m_steps = 0 # USB-connected state; master-lane steps (for the bar counter) self._uiNext = 0.0; self._lastTs = None; self._lastBs = None # throttle the stopwatch/bar redraw - self.ic_midi_pal = None; self.ic_usb_pal = None + self.ic_midi_pal = None; self.ic_usb_pal = None; self.bg_pal = None # practice history - persisted to /history.json (next to programs.json) when we own the filesystem self.can_write = self._probe_write() self.log = self._load_log() self.play_start = None; self.play_bpm = 0; self.play_name = "" self._armed = None; self.log_rows = [] self._build_scene() - self.load(0) - self.draw_log(); self.draw_icons(); self.draw_meters() + self.load(0) # load() also draws the (track-filtered) practice log + self.draw_icons(); self.draw_meters() def _btn(self, pin): d = digitalio.DigitalInOut(pin); d.direction = digitalio.Direction.INPUT; d.pull = digitalio.Pull.UP @@ -363,7 +364,8 @@ class App: # ---------- scene graph ---------- def _build_scene(self): root = displayio.Group(); self.display.root_group = root - root.append(rect(0, 0, WIDTH, HEIGHT, C_BG)) + self.bg_pal = solid(C_BG) # recolored on play/stop (black <-> running gray) + root.append(vectorio.Rectangle(pixel_shader=self.bg_pal, width=WIDTH, height=HEIGHT, x=0, y=0)) # header: VARASYS logo (left, no tagline) + MIDI / USB status icons (right) if LOGO: tg, _p, lw, lh = make_glyph(LOGO, C_CYAN, C_BG); tg.x = 10; tg.y = 9; root.append(tg) @@ -377,14 +379,14 @@ class App: root.append(rect(0, 38, WIDTH, 2, C_PANEL)) # dynamic groups self.g_bpm = displayio.Group(); root.append(self.g_bpm) # big tempo (right) - self.g_run = displayio.Group(); root.append(self.g_run) # RUN / STOP (left) - self.g_time = displayio.Group(); root.append(self.g_time) # stopwatch (m:ss, left) - self.g_bar = displayio.Group(); root.append(self.g_bar) # bar counter (right) - self.g_name = displayio.Group(); root.append(self.g_name) # item index + name + self.g_time = displayio.Group(); root.append(self.g_time) # elapsed [of total] (left) + self.g_bar = displayio.Group(); root.append(self.g_bar) # bar [of total] (left) + self.g_name = displayio.Group(); root.append(self.g_name) # track title + self.g_idx = displayio.Group(); root.append(self.g_idx) # track number (dim, right) - set apart from the title self.g_grid = displayio.Group(); root.append(self.g_grid) # lanes x step pads root.append(rect(0, LOG_TOP - 6, WIDTH, 2, C_PANEL)) # divider above the history log self.g_log = displayio.Group(); root.append(self.g_log) # practice history (tap a row to delete) - # (no on-screen buttons - transport is the joystick + buttons A/B; touch deletes log rows) + # run/stop is shown by the background tint (black=stopped, gray=running); transport = joystick + buttons A/B def _place(self, group, s, x, y, fg, bg, font, right_edge=None): while len(group): group.pop() @@ -401,9 +403,9 @@ class App: def load(self, i): n = len(self.programs); self.idx = i % n self.name, prog = self.programs[self.idx] - self.bpm, self.lanes = parse_program(prog) + self.bpm, self.lanes, self.bars = parse_program(prog) self.master = self.lanes[0] - self._reset_clock(); self.draw_bpm(); self.draw_status(); self.build_grid() + self._reset_clock(); self.draw_bpm(); self.draw_status(); self.build_grid(); self.draw_log() def _step_dur(self, L, step): beat = 60_000_000_000 / self.bpm if L['poly']: # ~ polymeter: fit this lane's whole cycle into lane 1's bar @@ -441,17 +443,18 @@ class App: self.running = not self.running if self.running: self._reset_clock(); self._start_play() else: self.buz.duty_cycle = 0; self.led_off(); self.reset_playheads(); self._log_play() - self.draw_status() + self.draw_runbg(); self.draw_meters() def set_bpm(self, v): v = max(30, min(300, v)) if v != self.bpm: self.bpm = v - self.draw_bpm() + self.draw_bpm(); self.draw_meters() # total time depends on bpm def goto(self, i): was = self.running if was: self.running = False; self._log_play() # close out the track that was playing self.load(i) if was: self.running = True; self._reset_clock(); self._start_play() + self.draw_runbg(); self.draw_meters() def tap(self): now = time.monotonic() if not hasattr(self, '_taps'): self._taps = [] @@ -534,36 +537,42 @@ class App: # ---------- drawing ---------- def draw_bpm(self): self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-12) - def draw_status(self): - self._place(self.g_run, "RUN" if self.running else "STOP", 12, 48, - C_GREEN if self.running else C_MUTE, C_BG, FONT_M) - self._place(self.g_name, "%d/%d %s" % (self.idx+1, len(self.programs), self.name[:18]), - 12, 120, C_TXT, C_BG, FONT_M) + def draw_status(self): # title (bright) + track number set apart (dim, right) + self._place(self.g_name, self.name[:22], 12, 116, C_TXT, C_BG, FONT_M) + self._place(self.g_idx, "%d/%d" % (self.idx + 1, len(self.programs)), 0, 120, + C_DIM, C_BG, FONT_S, right_edge=WIDTH - 12) + def draw_runbg(self): # run/stop indicator: tint the whole background + if self.bg_pal is not None: self.bg_pal[0] = C_RUNBG if self.running else C_BG + self.dirty = True def draw_icons(self): # recolor the MIDI/USB icons by state (tear-free palette swap) if self.ic_midi_pal is not None: _recolor(self.ic_midi_pal, C_GREEN if self.midi_host else C_DIM, C_BG) if self.ic_usb_pal is not None: _recolor(self.ic_usb_pal, C_CYAN if self.usb_conn else C_DIM, C_BG) self.dirty = True - def draw_meters(self): # stopwatch (m:ss) + bar counter; called ~4x/s from run() - if self.running and self.play_start is not None: - el = int(time.monotonic() - self.play_start) - ts = "%d:%02d" % (el // 60, el % 60) - mlen = self.lanes[0]['steps'] if self.lanes else 1 - bs = "bar %d" % (self._m_steps // max(1, mlen) + 1) + def _fmt_t(self, s): # m:ss, or h:mm:ss past an hour + s = int(s) + return "%d:%02d:%02d" % (s // 3600, (s % 3600) // 60, s % 60) if s >= 3600 else "%d:%02d" % (s // 60, s % 60) + def draw_meters(self): # running time [of total] + bar [of total]; ~4x/s from run() + run = self.running and self.play_start is not None + mlen = self.lanes[0]['steps'] if self.lanes else 1 + bpb = (self.lanes[0]['steps'] // max(1, self.lanes[0]['sub'])) if self.lanes else 4 + el = (time.monotonic() - self.play_start) if run else 0 + mbars = self._m_steps // max(1, mlen) # whole master bars elapsed + cur = ("%d" % ((mbars % self.bars + 1) if self.bars else (mbars + 1))) if run else "-" # cycle 1..N + if self.bars: # track has a length (b): show "X of TOTAL" + ts = "%s of %s" % (self._fmt_t(el), self._fmt_t(self.bars * bpb * 60.0 / self.bpm)) + bs = "bar %s of %d" % (cur, self.bars) else: - ts = "0:00"; bs = "bar -" + ts = self._fmt_t(el); bs = "bar %s" % cur if ts != self._lastTs: - self._place(self.g_time, ts, 12, 86, C_TXT, C_BG, FONT_M); self._lastTs = ts + self._place(self.g_time, ts, 12, 52, C_TXT, C_BG, FONT_M); self._lastTs = ts if bs != self._lastBs: - self._place(self.g_bar, bs, 0, 92, C_MUTE, C_BG, FONT_M, right_edge=WIDTH-12); self._lastBs = bs + self._place(self.g_bar, bs, 12, 84, C_MUTE, C_BG, FONT_M); self._lastBs = bs # ---------- pad grid (each lane = a row of step pads; playhead lit as it plays) ---------- def _padbase(self, L, s): return 0 if L['mute'] else L['levels'][s] - def _sq(self, cx, cy, side, ci): # a centred square pad sharing pad_pal - r = vectorio.Rectangle(pixel_shader=self.pad_pal, width=side, height=side, x=cx - side // 2, y=cy - side // 2) - r.color_index = ci; return r def build_grid(self): while len(self.g_grid): self.g_grid.pop() self.lane_pads = []; self.lane_lit = [] @@ -579,37 +588,28 @@ class App: tg, w, h = make_text((L.get('sound', '') or '?')[:7], FONT_S, C_MUTE, C_BG) tg.x = 8; tg.y = cy - h // 2; self.g_grid.append(tg) steps = L['steps']; sub = L['sub']; stepw = max(1, usable // steps) - side = max(5, min(15, stepw - 1, rowh - 6)); inner = max(2, side - 4) + side = max(5, min(15, stepw - 1, rowh - 6)) # square edge for the main pulse + rad = max(2, min(side // 2, stepw // 2 - 1)) # smaller circle for subdivisions pads = [] for s in range(steps): - base = self._padbase(L, s) cxp = px0 + 6 + (s * usable) // steps # proportional -> beats line up across lanes - if s % sub == 0: # main beat -> filled square - sq = self._sq(cxp, cy, side, base); self.g_grid.append(sq) - pads.append(("fill", (sq,), base)) - else: # off-beat -> hollow outline square - out = self._sq(cxp, cy, side, base if base else 8) - ins = self._sq(cxp, cy, inner, 9) # bg fill = the hollow centre - self.g_grid.append(out); self.g_grid.append(ins) - pads.append(("out", (out, ins), base)) + if s % sub == 0: # main beat -> square + p = vectorio.Rectangle(pixel_shader=self.pad_pal, width=side, height=side, + x=cxp - side // 2, y=cy - side // 2) + else: # subdivision -> circle + p = vectorio.Circle(pixel_shader=self.pad_pal, radius=rad, x=cxp, y=cy) + p.color_index = self._padbase(L, s); self.g_grid.append(p); pads.append(p) self.lane_pads.append(pads); self.lane_lit.append(-1) self.dirty = True - def _pad_idle(self, pad): - kind, shapes, base = pad - if kind == "fill": shapes[0].color_index = base - else: shapes[0].color_index = base if base else 8; shapes[1].color_index = 9 # ring + hollow centre - def _pad_lit(self, pad): - kind, shapes, base = pad - for sh in shapes: sh.color_index = base + 4 # fill the square (lit level) regardless of shape def _move_playhead(self, li, step): pads = self.lane_pads[li]; prev = self.lane_lit[li] - if 0 <= prev < len(pads): self._pad_idle(pads[prev]) - if step < len(pads): self._pad_lit(pads[step]) + if 0 <= prev < len(pads): pads[prev].color_index = self._padbase(self.lanes[li], prev) + if step < len(pads): pads[step].color_index = self._padbase(self.lanes[li], step) + 4 self.lane_lit[li] = step; self.dirty = True def reset_playheads(self): for li, pads in enumerate(self.lane_pads): prev = self.lane_lit[li] - if 0 <= prev < len(pads): self._pad_idle(pads[prev]) + if 0 <= prev < len(pads): pads[prev].color_index = self._padbase(self.lanes[li], prev) self.lane_lit[li] = -1 self.dirty = True @@ -648,17 +648,18 @@ class App: g = self.g_log while len(g): g.pop() self.log_rows = [] - hdr, w, h = make_text("PRACTICE LOG", FONT_S, C_MUTE, C_BG); hdr.x = 10; hdr.y = LOG_TOP; g.append(hdr) - if not self.log: - tg, w, h = make_text("plays over 5s show here", FONT_S, C_DIM, C_BG); tg.x = 10; tg.y = LOG_TOP + LOG_ROWH; g.append(tg) + hdr, w, h = make_text("PRACTICE LOG - THIS TRACK", FONT_S, C_MUTE, C_BG); hdr.x = 10; hdr.y = LOG_TOP; g.append(hdr) + rows = [(i, e) for i, e in enumerate(self.log) if e.get("name") == self.name] # current track only + if not rows: + tg, w, h = make_text("no plays over 5s yet", FONT_S, C_DIM, C_BG); tg.x = 10; tg.y = LOG_TOP + LOG_ROWH; g.append(tg) self.dirty = True; return y = LOG_TOP + LOG_ROWH + 2 - for idx in range(min(LOG_ROWS, len(self.log))): - e = self.log[idx]; armed = (idx == self._armed) + for k in range(min(LOG_ROWS, len(rows))): + oi, e = rows[k]; armed = (oi == self._armed) # oi = index into self.log (for delete) dur = "%d:%02d" % (e["dur"] // 60, e["dur"] % 60) - line = "%s%s %3d %5s %s" % ("x " if armed else "", e.get("t", "--:--"), e["bpm"], dur, e["name"][:16]) + line = "%s%s %3d bpm %s" % ("x " if armed else "", e.get("t", "--:--"), e["bpm"], dur) tg, w, h = make_text(line, FONT_S, C_AMBER if armed else C_TXT, C_BG); tg.x = 10; tg.y = y; g.append(tg) - self.log_rows.append((y - 2, y + LOG_ROWH - 2, idx)) + self.log_rows.append((y - 2, y + LOG_ROWH - 2, oi)) y += LOG_ROWH self.dirty = True def _tap_log(self, x, ty):