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
|
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
|
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:
|
try:
|
||||||
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -89,15 +89,15 @@ BUILTIN_SETLISTS = [
|
||||||
("Tempo builder 80 up", "t80;woodblock:4;rmp80/4/4"),
|
("Tempo builder 80 up", "t80;woodblock:4;rmp80/4/4"),
|
||||||
("Gap trainer (play 2 / rest 2)", "t100;kick:4;hatClosed:4/2;tr2/2"),
|
("Gap trainer (play 2 / rest 2)", "t100;kick:4;hatClosed:4/2;tr2/2"),
|
||||||
]),
|
]),
|
||||||
("Song (continuous)", [
|
("Song (continuous)", [ # ~4-bar sections; with Continue on they roll one into the next
|
||||||
("Intro - hats & kick", "t88;b8;kick:4=X.x.;hatClosed:4/2=gggggggg"),
|
("Intro - hats & kick", "t88;b4;kick:4=X.x.;hatClosed:4/2=gggggggg"),
|
||||||
("Groove in - backbeat", "t88;b16;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2"),
|
("Groove in - backbeat", "t88;b4;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"),
|
("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;b16;rmp92/4/2;kick:4;snare:4=.X.X;hatClosed:4/2"),
|
("Build - ramp 92-120", "t92;b4;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"),
|
("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;b24;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."),
|
("Samba break (2/4)", "t116;b4;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"),
|
("Peak - 16ths", "t132;b4;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"),
|
("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.lane_pads = []; self.lane_lit = []
|
||||||
self.usb_conn = False; self._m_steps = 0 # USB-connected state; master-lane steps (for the bar counter)
|
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._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
|
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
|
# practice history - persisted to /history.json (next to programs.json) when we own the filesystem
|
||||||
self.can_write = self._probe_write()
|
self.can_write = self._probe_write()
|
||||||
|
|
@ -628,6 +630,7 @@ class App:
|
||||||
for L in self.lanes:
|
for L in self.lanes:
|
||||||
L['next'] = now; L['step'] = -1
|
L['next'] = now; L['step'] = -1
|
||||||
self._m_steps = 0 # restart the bar count
|
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 ----------
|
# ---------- audio + light ----------
|
||||||
def click(self, level):
|
def click(self, level):
|
||||||
|
|
@ -709,13 +712,14 @@ class App:
|
||||||
self._advance = False
|
self._advance = False
|
||||||
self.load((self.idx + 1) % len(self.setlists[self.sl]['items'])); self.led_rest()
|
self.load((self.idx + 1) % len(self.setlists[self.sl]['items'])); self.led_rest()
|
||||||
def _on_new_bar(self, bar):
|
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 > 0 and bar % self.bars == 0: # segment boundary
|
||||||
if self.bars and bar % self.bars == 0: self.set_bpm(self._ramp_base)
|
self._seg_start = time.monotonic() # timer resets with the bar counter
|
||||||
elif bar % self.ramp['every'] == 0: self.set_bpm(self.bpm + self.ramp['amt'])
|
if self.ramp: self.set_bpm(self._ramp_base) # ramp restarts each segment
|
||||||
t = self.trainer # gap trainer: silence during the rest bars of each cycle
|
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'])
|
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 ----------
|
# ---------- inputs ----------
|
||||||
def poll(self):
|
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
|
self.goto(self.idx + (1 if x > 0 else -1)); self._joyNext = now + 350_000_000; return
|
||||||
else:
|
else:
|
||||||
self._joyNext = now + 20_000_000
|
self._joyNext = now + 20_000_000
|
||||||
pt = self.touch.read()
|
|
||||||
nowms = time.monotonic()
|
nowms = time.monotonic()
|
||||||
if pt:
|
if nowms >= self._touchNext: # poll touch ~30x/s (the I2C read adds loop latency -> MIDI jitter)
|
||||||
self._touchSeen = nowms
|
self._touchNext = nowms + 0.033
|
||||||
if not self._touchDown:
|
pt = self.touch.read()
|
||||||
self._touchDown = True; self._handle_tap(pt[0], pt[1])
|
if pt:
|
||||||
elif self._touchDown and (nowms - self._touchSeen) > 0.14:
|
self._touchSeen = nowms
|
||||||
self._touchDown = False
|
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)
|
# USB-MIDI in: any byte = a host is listening (heartbeat); also assemble SysEx (clock / pushed programs)
|
||||||
if self.midi_in is not None:
|
if self.midi_in is not None:
|
||||||
try: n = self.midi_in.readinto(self._mbuf)
|
try: n = self.midi_in.readinto(self._mbuf)
|
||||||
|
|
@ -801,7 +807,7 @@ class App:
|
||||||
run = self.running and self.play_start is not None
|
run = self.running and self.play_start is not None
|
||||||
mlen = self.lanes[0]['steps'] if self.lanes else 1
|
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
|
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
|
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
|
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"
|
if self.bars: # track has a length (b<n>): show "X of TOTAL"
|
||||||
|
|
@ -997,10 +1003,12 @@ class App:
|
||||||
try: os.remove("/trial")
|
try: os.remove("/trial")
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
committed = True
|
committed = True
|
||||||
# push a complete frame only when something changed (no mid-update tearing);
|
# Refresh at most ~30x/s. display.refresh() BLOCKS while it streams pixels over SPI, which
|
||||||
# capped at the display's refresh rate, so dirty regions stay small and quick
|
# would otherwise delay the next beat's MIDI note and make the audio stutter; throttling it
|
||||||
if self.dirty and self.display.refresh():
|
# keeps the click timing tight (the visuals lag a few ms, which is imperceptible).
|
||||||
self.dirty = False
|
if self.dirty and tnow >= self._refreshNext:
|
||||||
|
if self.display.refresh(): self.dirty = False
|
||||||
|
self._refreshNext = tnow + 0.033
|
||||||
time.sleep(0.0005)
|
time.sleep(0.0005)
|
||||||
|
|
||||||
App().run()
|
App().run()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue