From ecd1d2a1892c0cecf3f140a17a7a9e79919e0ff7 Mon Sep 17 00:00:00 2001 From: Me Here Date: Fri, 29 May 2026 14:36:03 -0500 Subject: [PATCH] PM_K-1 0.0.12: Song = 4-bar sections, timer resets with the bar, smoother MIDI - Built-in Song playlist: every section is now b4 (~4 bars) so Continue rolls one into the next quickly. - On-screen timer now counts WITHIN the current segment and resets every time the bar counter wraps (new _seg_start, reset at each b boundary + on _reset_clock). The practice-log duration still uses play_start (total). Unified the segment-boundary handling (timer reset + ramp restart + Continue advance) in _on_new_bar. - MIDI stutter: display.refresh() BLOCKS on the SPI stream and was delaying the next beat's note. Cap refresh to ~30Hz and poll the GT911 touch ~30Hz (was every loop) so the scheduler fires notes on time; visuals lag a few ms (imperceptible). Verified in harness: Build(b4,rmp92/4/2) bpm 92->96->reset@bar4, seg_start resets only at the boundary, Continue arms there; edit tests pass; app.mpy builds (C/v6). Co-Authored-By: Claude Opus 4.7 (1M context) --- pico-cp/__pycache__/app.cpython-312.pyc | Bin 79494 -> 79878 bytes pico-cp/app.py | 64 +++++++++++++----------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/pico-cp/__pycache__/app.cpython-312.pyc b/pico-cp/__pycache__/app.cpython-312.pyc index 0d4042bf2c0f7212a3d3d05432e1d4258d6d4cc8..e4a72693dd90541c269b2b63da5f407a67b93f2b 100644 GIT binary patch delta 6557 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 F{|{R{(3JoH delta 6192 zcma)Ad013Ow(oO$qiLG0X<9&#O{GB*5D{ErR50qe4@opJG!5MCNE~<5`Bu_d{IA>d3EZt`26z6Yd?R~S?Ziq zbx)l-_r7yq_0Cir{pQsz=PEJW>qI9}LKi+GsJ2^RRWL^qQ(vCL!%y8I> zA_qq%r@0rnDu=j}hoX%%I>ILt(xn=o`bm&2CB5>Sx}Ty-VV#b_;LncK40)_zbkpWFFhJ`2{y>2_%-j;o(xza`4Hf*2PTP@{$XEP#_T=PckHncRNZN38fs+V( zk@pQfu_H7wmih5$$6H^Tm6bJ(#C1_)r;f{-MdFcCkGGD?o<`2#yjH6Fuo*^5r$5}M ziXF_Px48A8Xl?;21hLVrzDWKJ$t1dESIoHoM^wX^EjcRgXBd)oXeLK?`|GYS{|sb~ zKp2TIipK3u3yNXGN`w4p7L+cwmbm6mlF06J5XB#(nrue{d`oy3JRNG0XQsAKQedX^ z-X}jo^dFd+Y>QUb{v(?I;JJBpdtE$?rzh)%!OPOqx)s7OA5_ByTfF6YbCALJJOoF- zq6MBslaTa9FwrmePlvgZ=0L6hK6L)UNw9!69BhX&dUsX~jU1?>&0m^n>t~65D-;&> zG>^*ZSmGwdXetj4u>T5+icYMLpo{AJ^?l3)UFV88FgNpyOY@6R%QccV)Q=L@8|a%- znAE2s7&HZFppd2>N`(qqaOeWdpb=6WRMULv29(f=pGQFrE&TlKa2G1Rk3Qc-u;YU; zqfJIBBFSq=l=nEY2IECCY5ZUVusLR0Lo znww-}CJAB^_&piA?B*BE?e)O!9WDL7IYJ=)`5OAjYnXyM#LKQ>a-Hm1p{VHkXVFgM zxlfKV2|0rBUkJw;WU~W!7Kv5_e86NY9el2jmh8aC@F5*{F1qGD#;)|^g`(7HbvhPX z^K9f4W4ivyW-V28=>0ZitjXA7+_G?+xx>%6p<+$NmOt)H-IGz9vDI1+7sHe5QyWJ$ zjA~Y$@;l*odFYIefS`?Tv0_ujPIH^t(iUK;n|U!Hv0l~a)8NyT{pIWb>HYN z%zCwLqIP!M=-HQ&=1}ALKMY={XWrWNVHfpbJ5_shAM5J!>$4l@G|ah}_(DhD$W@u^ zr`}g8hK$*%yPyxFx#z=#*&6orL#g3{Njh-8O`r{51=ATdeSNDnZa)@2{}m2mJwT{W zwYEw_hE{3WE}E&OFe1p6W%p)8pKd*Np%0}|?*@g+iBYAoEAeUWXM2G^P&oV3WNnw-P{abL)>Y}4&vA6eWwU7nj;&EWmUJY* zF3>M81^ZfKdYFpBNxUzKI~gA79^1`!Pwg2^>mC)(z}6EaR>{kT0<)5gTEh0?Wl3)FNN0yez1 z`n3(#)z)tf5vzo2{z0PeCf{%UBUh=f>J1$cF{?7yP46(8#WC-W>4+Y%QT3i*ovJ-9 zy)7=iDX?km5mU3!oP0ukIc@@)vaYf2>|YoA*^s?M+M+X>vM)xB?K1ICcA!SB9(6$< z(_sn`=Wd$YZi;U+(d}0+`WyRC2~)I&>8Dtft#L5LLaRMT^OA85*fs10a-03Jc#%7} z;5LMZxHEjqcx!Ny@jRLd)2C%yr^q+%^VbvJEAsVZCpLE0%SwI~q^DY?E#7)D>ly!8$fTTIXNU_6|5oX#H^UMC*ScOM3o?G)SVF8*%ze=u_?uX~vDH#OGQr9(!-g zbq)>ju92^-m9DxmSTh!FVnZGsOawNTdtx4@o7f7RTIu19W+8`nUf$#YI;=-KJaKzB zg1h2x2YXP3JCIv4X7kw|qlt;1@llXp>SXT*0XHAQDyqIUhV{=`x3&QtS#8u~330Nk zv07CnLOhsg;GO<7|EC~jV3nHY{uE5@ccPFMva7@wLz%q(PJlQrl-;o?ynV-y2A(id zpPxgRl98tWY>oK!+n~}b7S35YiTg{D)H2?jsNiSy7 zpOU*4I6*(Sn^?oMB86??Ei8E!quhiX!Yw|-7-S;?uS~qe)S-aim@W8N`Qh4!Jf7wk zany6x$Tvu0xg_5r9Az^sI^MT>xi+$s#D7)Q7ki<&IOu@1T32zr|POOd7nBe+T1ei;N; z>2JTJ>d$nCL{8GtkNW4JClY{wS!D^8`>S}=dfp&Y@~G#N0{a4zk7G?qp2OyLc~p7% zrNzX6HlAP%{59krTDVS|ALW~rOd@Bd{5p#!Ql<3AZ>k1x?|j31Lr}{haC^Ls|DIm^ zH6&yUS_$MmNlED)E~_-aP}f=)j{;Ai*utar7s-1_%8=LQ3p zip3qtesa6M6kQq7mHeL2?aK8Ey9-@zrn^*etxwd>*gc80iQo1~?o=ob&xJC8O7Sc} z3LJc202Aai1`DtjvKm-C?C~tbtubJ1ykGw_lDP=CF#z_T1t7bxXG&eKX~m^ByS2D< z{=CM2DPa+${mk@8HO@k-WC|lESTy7_Bzhf$qZU_4M!{|IT{T#ssPV6A7Tl!Xtmf&? zb6G+?UT8nZ(eoF?6UOZ72U&?FZoc^o8;5J*m|(^rEP;d@%fZlO2cpg!qM$)c@P_c$ zxqW_!_`%{%4x^`g7^(^_bUl~{$Y@1a#cVzIU>;!{@(V266dr}EahEr&SHlT0*&q7r zu}LM|#wyX~4=>I1pclE4R$4)$_cQeZFG+BmZhwCMZ=maasL z5RA)Fik4^~P#}H~4e|O)G(Qj>ofg}oVM63h#_*`{3MIF2s38ugt;FF`yNVr-GIBs1 z9s_Bh7M(E=47(dw#6Z3}s@H`RUCc$JO6HNhdzPc^N>OKlC_f`(ReJHmYs)XXB4$`% zI^@vvXTn&U9@N-mfwusn#1|4+p|`aK(fWZpaeV^#>D*nnf`V>%(?stL^j3jl-3jqp z0)$x3FIFs3&QUA|)_aCB@BA0b{DWeSVx6s9YVi^y20=)TfyFQ(d%mo8VGMbRjpjey z`{3f#ycfr?1YU^oMj-eh^d1x37-GV0`Stw-#Q6_;)k;TMMoy5P^S=3Byq&LL35q+ai+?Vi5Q@ zAN~O+7s+`D{QCocW#=D6_PmK^ApN=WKCY5|n^ZtY_>bo)Nr)Z(^xK zcv>7l^Pe*Cc-zX|rNn_1o1DfVe8r$-$bqe*D;WmqKS9Mk2zBDVWLQ`8Z`9%49sktG z+XrkF&!#}M=@hD-LpY1@6Dn;KRYM^Q&WMwTLO2X-Trd=xlwmd<^n0M- aIKij@)m!S7x@Ec*`bYOm#F4|{%>M$UG;?48 diff --git a/pico-cp/app.py b/pico-cp/app.py index dbc1287..acc5904 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.11" # firmware version (the A/B updater pushes/compares this) +APP_VERSION = "0.0.12" # 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: @@ -89,15 +89,15 @@ BUILTIN_SETLISTS = [ ("Tempo builder 80 up", "t80;woodblock:4;rmp80/4/4"), ("Gap trainer (play 2 / rest 2)", "t100;kick:4;hatClosed:4/2;tr2/2"), ]), - ("Song (continuous)", [ - ("Intro - hats & kick", "t88;b8;kick:4=X.x.;hatClosed:4/2=gggggggg"), - ("Groove in - backbeat", "t88;b16;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2"), - ("Half-time shuffle", "t92;b12;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x"), - ("Build - ramp 92-120", "t92;b16;rmp92/4/2;kick:4;snare:4=.X.X;hatClosed:4/2"), - ("Four-on-the-floor (909)", "t124;b18;kick909:4;clap909:4=.X.X;hat909:4/2=.X.X.X.X"), - ("Samba break (2/4)", "t116;b24;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."), - ("Peak - 16ths", "t132;b16;kick:4=X..x;snare:4=.X.X;hatClosed:4/4"), - ("Outro - ramp down", "t132;b8;rmp132/-7/1;kick:4=X..x;hatClosed:4/2=gggggggg"), + ("Song (continuous)", [ # ~4-bar sections; with Continue on they roll one into the next + ("Intro - hats & kick", "t88;b4;kick:4=X.x.;hatClosed:4/2=gggggggg"), + ("Groove in - backbeat", "t88;b4;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2"), + ("Half-time shuffle", "t92;b4;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x"), + ("Build - ramp 92-120", "t92;b4;rmp92/4/2;kick:4;snare:4=.X.X;hatClosed:4/2"), + ("Four-on-the-floor (909)", "t124;b4;kick909:4;clap909:4=.X.X;hat909:4/2=.X.X.X.X"), + ("Samba break (2/4)", "t116;b4;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."), + ("Peak - 16ths", "t132;b4;kick:4=X..x;snare:4=.X.X;hatClosed:4/4"), + ("Outro - ramp down", "t132;b4;rmp132/-7/1;kick:4=X..x;hatClosed:4/2=gggggggg"), ]), ] @@ -421,6 +421,8 @@ class App: 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._seg_start = 0.0 # timer origin; resets with the bar counter (each segment) + self._refreshNext = 0.0; self._touchNext = 0.0 # cap display refresh + touch polling (tighter MIDI timing) self.ic_midi_pal = None; self.ic_usb_pal = None # practice history - persisted to /history.json (next to programs.json) when we own the filesystem self.can_write = self._probe_write() @@ -628,6 +630,7 @@ class App: for L in self.lanes: L['next'] = now; L['step'] = -1 self._m_steps = 0 # restart the bar count + self._seg_start = time.monotonic() # and the on-screen timer (resets with the bar counter) # ---------- audio + light ---------- def click(self, level): @@ -709,13 +712,14 @@ class App: self._advance = False self.load((self.idx + 1) % len(self.setlists[self.sl]['items'])); self.led_rest() def _on_new_bar(self, bar): - if self.ramp and bar > 0: # tempo ramp: reset each segment, else step every N bars - if self.bars and bar % self.bars == 0: self.set_bpm(self._ramp_base) - elif bar % self.ramp['every'] == 0: self.set_bpm(self.bpm + self.ramp['amt']) - t = self.trainer # gap trainer: silence during the rest bars of each cycle + if self.bars and bar > 0 and bar % self.bars == 0: # segment boundary + self._seg_start = time.monotonic() # timer resets with the bar counter + if self.ramp: self.set_bpm(self._ramp_base) # ramp restarts each segment + if self.continue_on: self._advance = True # Continue: roll to the next item + elif self.ramp and bar > 0 and bar % self.ramp['every'] == 0: + self.set_bpm(self.bpm + self.ramp['amt']) # mid-segment ramp step + t = self.trainer # gap trainer: silence during the rest bars self._muted = bool(t and (t['play'] + t['mute']) and (bar % (t['play'] + t['mute'])) >= t['play']) - if self.continue_on and self.bars and bar >= self.bars: # auto-advance at the end of the segment - self._advance = True # ---------- inputs ---------- def poll(self): @@ -737,14 +741,16 @@ class App: self.goto(self.idx + (1 if x > 0 else -1)); self._joyNext = now + 350_000_000; return else: self._joyNext = now + 20_000_000 - pt = self.touch.read() nowms = time.monotonic() - if pt: - self._touchSeen = nowms - if not self._touchDown: - self._touchDown = True; self._handle_tap(pt[0], pt[1]) - elif self._touchDown and (nowms - self._touchSeen) > 0.14: - self._touchDown = False + if nowms >= self._touchNext: # poll touch ~30x/s (the I2C read adds loop latency -> MIDI jitter) + self._touchNext = nowms + 0.033 + pt = self.touch.read() + if pt: + self._touchSeen = nowms + if not self._touchDown: + self._touchDown = True; self._handle_tap(pt[0], pt[1]) + elif self._touchDown and (nowms - self._touchSeen) > 0.14: + self._touchDown = False # USB-MIDI in: any byte = a host is listening (heartbeat); also assemble SysEx (clock / pushed programs) if self.midi_in is not None: try: n = self.midi_in.readinto(self._mbuf) @@ -801,7 +807,7 @@ class App: 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 + el = (time.monotonic() - self._seg_start) if run else 0 # time within the current segment (resets with the bar) 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" @@ -997,10 +1003,12 @@ class App: try: os.remove("/trial") except Exception: pass committed = True - # push a complete frame only when something changed (no mid-update tearing); - # capped at the display's refresh rate, so dirty regions stay small and quick - if self.dirty and self.display.refresh(): - self.dirty = False + # Refresh at most ~30x/s. display.refresh() BLOCKS while it streams pixels over SPI, which + # would otherwise delay the next beat's MIDI note and make the audio stutter; throttling it + # keeps the click timing tight (the visuals lag a few ms, which is imperceptible). + if self.dirty and tnow >= self._refreshNext: + if self.display.refresh(): self.dirty = False + self._refreshNext = tnow + 0.033 time.sleep(0.0005) App().run()