PM_K-1 0.0.17: fix gapless-seam off-by-one + GC hygiene around modals / parse
The continuous-playback gap between tracks was a one-master-step early trigger of _on_new_bar(N): nb = _m_steps // steps maps to the LAST step of bar N-1, not the downbeat of bar N. So _seam_t (= lanes[0]['next'] at that moment) was one master step short of the proper boundary -- A lost its last step, and B's "bar 0 step 0" landed at that early slot. With kick:2 (2 steps/bar) that's half a bar each side; with kick:4 a quarter; with kick:1 a whole bar. Fix: nb = (self._m_steps - 1) // L['steps'] -- fires at the downbeat of the new bar. Same adjustment applied to the on-screen "bar X of N" counter (mbars) so it advances on the downbeat, not on the last beat of the previous bar. Memory hygiene: gc.collect() at the entry of every modal (_show_menu / _show_settings / _show_help / _show_about / _show_saverevert / _show_laneedit) so each modal allocates its bitmaps against a defragmented heap. _prepare_next gc.collect()s before parse_program and catches MemoryError -> the segment loops instead of crashing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c9f2288bdd
commit
da71604c0d
1 changed files with 14 additions and 4 deletions
|
|
@ -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.16" # firmware version (the A/B updater pushes/compares this)
|
||||
APP_VERSION = "0.0.17" # 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:
|
||||
|
|
@ -618,6 +618,7 @@ class App:
|
|||
self.load(self.idx) # reload from source -> discard edits
|
||||
# ---------- modal overlay (save / revert / message) ----------
|
||||
def _show_saverevert(self):
|
||||
gc.collect()
|
||||
self._overlay = 'saverevert'; g = self.g_overlay
|
||||
while len(g): g.pop()
|
||||
px, py, pw, ph = 24, 178, WIDTH - 48, 116
|
||||
|
|
@ -665,6 +666,7 @@ class App:
|
|||
self._tap_log(tx, ty) # else the practice log
|
||||
# ---------- lane editor (tap the instrument name): sound / beats / sub / swing / mute + add / remove ----------
|
||||
def _show_laneedit(self, li):
|
||||
gc.collect()
|
||||
self._overlay = 'lane'; self._edit_li = li; self._draw_laneedit()
|
||||
def _draw_laneedit(self):
|
||||
li = self._edit_li; L = self.lanes[li]; g = self.g_overlay
|
||||
|
|
@ -736,6 +738,7 @@ class App:
|
|||
self._close_overlay()
|
||||
# ---------- hamburger menu (main) + sub-modals (Settings / Help / About) ----------
|
||||
def _show_menu(self):
|
||||
gc.collect() # defragment before allocating modal bitmaps
|
||||
self._overlay = 'menu'; self._draw_menu()
|
||||
def _draw_menu(self):
|
||||
g = self.g_overlay
|
||||
|
|
@ -768,6 +771,7 @@ class App:
|
|||
|
||||
# ---------- Settings sub-modal (LED / Speaker / MIDI Out / Channel / Clock Out / Clock In) ----------
|
||||
def _show_settings(self):
|
||||
gc.collect()
|
||||
self._overlay = 'settings'; self._draw_settings()
|
||||
def _draw_settings(self):
|
||||
g = self.g_overlay
|
||||
|
|
@ -836,6 +840,7 @@ class App:
|
|||
|
||||
# ---------- Help sub-modal (paginated) ----------
|
||||
def _show_help(self):
|
||||
gc.collect()
|
||||
self._overlay = 'help'; self._help_page = 0; self._draw_help()
|
||||
def _draw_help(self):
|
||||
g = self.g_overlay
|
||||
|
|
@ -872,6 +877,7 @@ class App:
|
|||
|
||||
# ---------- About sub-modal ----------
|
||||
def _show_about(self):
|
||||
gc.collect()
|
||||
self._overlay = 'about'; self._draw_about()
|
||||
def _draw_about(self):
|
||||
import sys
|
||||
|
|
@ -1017,7 +1023,7 @@ class App:
|
|||
L['step'] = (L['step'] + 1) % L['steps']
|
||||
if li == 0:
|
||||
self._m_steps += 1 # count master-lane steps -> bars
|
||||
nb = self._m_steps // L['steps']
|
||||
nb = (self._m_steps - 1) // L['steps'] # bar of THIS step (off-by-one fix vs 0.0.16)
|
||||
if nb != self._lastbar: self._lastbar = nb; self._on_new_bar(nb)
|
||||
if self._advance: break # seam armed - suppress this step's firing
|
||||
if self.ramp and L['steps'] > 0 and not self._slaved: # CONTINUOUS ramp (off when slaved)
|
||||
|
|
@ -1073,7 +1079,11 @@ class App:
|
|||
nxt = (self.idx + 1) % len(items)
|
||||
if nxt == self.idx: return # 1-item playlist -> just loop, no swap
|
||||
name, prog = items[nxt]
|
||||
gc.collect() # defragment before parse_program allocates new lanes
|
||||
try:
|
||||
bpm, lanes, bars, ramp, trainer = parse_program(prog)
|
||||
except MemoryError:
|
||||
gc.collect(); return # leave _next_pending None -> the segment just loops
|
||||
self._next_pending = {'lanes': lanes, 'bpm': bpm, 'bars': bars, 'ramp': ramp,
|
||||
'trainer': trainer, 'name': name, 'idx': nxt}
|
||||
def _do_advance(self): # gapless seam: swap the prepared track in at seam_t
|
||||
|
|
@ -1180,7 +1190,7 @@ class App:
|
|||
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._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
|
||||
mbars = max(0, self._m_steps - 1) // max(1, mlen) # bar containing THIS step (off-by-one fix vs 0.0.16)
|
||||
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<n>): show "X of TOTAL"
|
||||
ts = "%s of %s" % (self._fmt_t(el), self._fmt_t(self.bars * bpb * 60.0 / self.bpm))
|
||||
|
|
|
|||
Loading…
Reference in a new issue