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<n> 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) <noreply@anthropic.com>
This commit is contained in:
parent
13318daf5b
commit
ecd1d2a189
2 changed files with 36 additions and 28 deletions
Binary file not shown.
|
|
@ -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<n>): 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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue