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<n> 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<n> 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) <noreply@anthropic.com>
This commit is contained in:
parent
dec6c61fce
commit
ca44aa833d
3 changed files with 70 additions and 65 deletions
|
|
@ -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.
|
- **Joystick:** up/down = tempo, left/right = previous/next groove.
|
||||||
- **Button A (GP15):** play / stop. **Button B (GP14):** tap tempo.
|
- **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<n>`), 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.**
|
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.
|
- **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.
|
- The log is saved to `/history.json` (next to `programs.json`) in appliance mode and survives power‑cycles.
|
||||||
|
|
|
||||||
Binary file not shown.
129
pico-cp/app.py
129
pico-cp/app.py
|
|
@ -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.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:
|
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:
|
||||||
|
|
@ -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_DIM = (0x10161E, 0x0A3A52, 0x4A3010, 0x2A1D4A) # idle pad: mute / normal / accent / ghost
|
||||||
PAD_LIT = (0x39414D, 0x0AB3F7, 0xFF9B2E, 0x967BFF) # playhead 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)
|
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)
|
# WS2812 RGB LED - self-contained via the core neopixel_write module (no external library)
|
||||||
class RGB:
|
class RGB:
|
||||||
|
|
@ -195,17 +195,19 @@ PAT = {'X': 2, 'x': 1, 'g': 3, '.': 0, '-': 0, '_': 0}
|
||||||
PRIO = {2: 3, 1: 2, 3: 1}
|
PRIO = {2: 3, 1: 2, 3: 1}
|
||||||
|
|
||||||
def parse_program(s):
|
def parse_program(s):
|
||||||
bpm = 120; lanes = []
|
bpm = 120; lanes = []; bars = 0
|
||||||
for tok in s.strip().split(';'):
|
for tok in s.strip().split(';'):
|
||||||
tok = tok.strip()
|
tok = tok.strip()
|
||||||
if not tok: continue
|
if not tok: continue
|
||||||
if tok[0] == 't' and tok[1:].isdigit():
|
if tok[0] == 't' and tok[1:].isdigit():
|
||||||
bpm = int(tok[1:]); continue
|
bpm = int(tok[1:]); continue
|
||||||
|
if tok[0] == 'b' and tok[1:].isdigit(): # b<n> = 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
|
if ':' not in tok: continue
|
||||||
lane = _parse_lane(tok)
|
lane = _parse_lane(tok)
|
||||||
if lane: lanes.append(lane)
|
if lane: lanes.append(lane)
|
||||||
if not lanes: lanes = [_parse_lane("beep:4")]
|
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):
|
def _parse_lane(tok):
|
||||||
poly = '~' in tok; mute = '!' in 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.jx = analogio.AnalogIn(P_JOYX); self.jy = analogio.AnalogIn(P_JOYY)
|
||||||
self._joyNext = 0
|
self._joyNext = 0
|
||||||
self._touchDown = False; self._touchSeen = 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.programs = load_programs()
|
||||||
self.dirty = True
|
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]
|
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.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.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
|
# 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()
|
||||||
self.log = self._load_log()
|
self.log = self._load_log()
|
||||||
self.play_start = None; self.play_bpm = 0; self.play_name = ""
|
self.play_start = None; self.play_bpm = 0; self.play_name = ""
|
||||||
self._armed = None; self.log_rows = []
|
self._armed = None; self.log_rows = []
|
||||||
self._build_scene()
|
self._build_scene()
|
||||||
self.load(0)
|
self.load(0) # load() also draws the (track-filtered) practice log
|
||||||
self.draw_log(); self.draw_icons(); self.draw_meters()
|
self.draw_icons(); self.draw_meters()
|
||||||
|
|
||||||
def _btn(self, pin):
|
def _btn(self, pin):
|
||||||
d = digitalio.DigitalInOut(pin); d.direction = digitalio.Direction.INPUT; d.pull = digitalio.Pull.UP
|
d = digitalio.DigitalInOut(pin); d.direction = digitalio.Direction.INPUT; d.pull = digitalio.Pull.UP
|
||||||
|
|
@ -363,7 +364,8 @@ class App:
|
||||||
# ---------- scene graph ----------
|
# ---------- scene graph ----------
|
||||||
def _build_scene(self):
|
def _build_scene(self):
|
||||||
root = displayio.Group(); self.display.root_group = root
|
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)
|
# header: VARASYS logo (left, no tagline) + MIDI / USB status icons (right)
|
||||||
if LOGO:
|
if LOGO:
|
||||||
tg, _p, lw, lh = make_glyph(LOGO, C_CYAN, C_BG); tg.x = 10; tg.y = 9; root.append(tg)
|
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))
|
root.append(rect(0, 38, WIDTH, 2, C_PANEL))
|
||||||
# dynamic groups
|
# dynamic groups
|
||||||
self.g_bpm = displayio.Group(); root.append(self.g_bpm) # big tempo (right)
|
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) # elapsed [of total] (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 [of total] (left)
|
||||||
self.g_bar = displayio.Group(); root.append(self.g_bar) # bar counter (right)
|
self.g_name = displayio.Group(); root.append(self.g_name) # track title
|
||||||
self.g_name = displayio.Group(); root.append(self.g_name) # item index + name
|
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
|
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
|
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)
|
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):
|
def _place(self, group, s, x, y, fg, bg, font, right_edge=None):
|
||||||
while len(group): group.pop()
|
while len(group): group.pop()
|
||||||
|
|
@ -401,9 +403,9 @@ class App:
|
||||||
def load(self, i):
|
def load(self, i):
|
||||||
n = len(self.programs); self.idx = i % n
|
n = len(self.programs); self.idx = i % n
|
||||||
self.name, prog = self.programs[self.idx]
|
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.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):
|
def _step_dur(self, L, step):
|
||||||
beat = 60_000_000_000 / self.bpm
|
beat = 60_000_000_000 / self.bpm
|
||||||
if L['poly']: # ~ polymeter: fit this lane's whole cycle into lane 1's bar
|
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
|
self.running = not self.running
|
||||||
if self.running: self._reset_clock(); self._start_play()
|
if self.running: self._reset_clock(); self._start_play()
|
||||||
else: self.buz.duty_cycle = 0; self.led_off(); self.reset_playheads(); self._log_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):
|
def set_bpm(self, v):
|
||||||
v = max(30, min(300, v))
|
v = max(30, min(300, v))
|
||||||
if v != self.bpm:
|
if v != self.bpm:
|
||||||
self.bpm = v
|
self.bpm = v
|
||||||
self.draw_bpm()
|
self.draw_bpm(); self.draw_meters() # total time depends on bpm
|
||||||
def goto(self, i):
|
def goto(self, i):
|
||||||
was = self.running
|
was = self.running
|
||||||
if was: self.running = False; self._log_play() # close out the track that was playing
|
if was: self.running = False; self._log_play() # close out the track that was playing
|
||||||
self.load(i)
|
self.load(i)
|
||||||
if was: self.running = True; self._reset_clock(); self._start_play()
|
if was: self.running = True; self._reset_clock(); self._start_play()
|
||||||
|
self.draw_runbg(); self.draw_meters()
|
||||||
def tap(self):
|
def tap(self):
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if not hasattr(self, '_taps'): self._taps = []
|
if not hasattr(self, '_taps'): self._taps = []
|
||||||
|
|
@ -534,36 +537,42 @@ class App:
|
||||||
# ---------- drawing ----------
|
# ---------- drawing ----------
|
||||||
def draw_bpm(self):
|
def draw_bpm(self):
|
||||||
self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-12)
|
self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-12)
|
||||||
def draw_status(self):
|
def draw_status(self): # title (bright) + track number set apart (dim, right)
|
||||||
self._place(self.g_run, "RUN" if self.running else "STOP", 12, 48,
|
self._place(self.g_name, self.name[:22], 12, 116, C_TXT, C_BG, FONT_M)
|
||||||
C_GREEN if self.running else C_MUTE, C_BG, FONT_M)
|
self._place(self.g_idx, "%d/%d" % (self.idx + 1, len(self.programs)), 0, 120,
|
||||||
self._place(self.g_name, "%d/%d %s" % (self.idx+1, len(self.programs), self.name[:18]),
|
C_DIM, C_BG, FONT_S, right_edge=WIDTH - 12)
|
||||||
12, 120, C_TXT, C_BG, FONT_M)
|
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)
|
def draw_icons(self): # recolor the MIDI/USB icons by state (tear-free palette swap)
|
||||||
if self.ic_midi_pal is not None:
|
if self.ic_midi_pal is not None:
|
||||||
_recolor(self.ic_midi_pal, C_GREEN if self.midi_host else C_DIM, C_BG)
|
_recolor(self.ic_midi_pal, C_GREEN if self.midi_host else C_DIM, C_BG)
|
||||||
if self.ic_usb_pal is not None:
|
if self.ic_usb_pal is not None:
|
||||||
_recolor(self.ic_usb_pal, C_CYAN if self.usb_conn else C_DIM, C_BG)
|
_recolor(self.ic_usb_pal, C_CYAN if self.usb_conn else C_DIM, C_BG)
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
def draw_meters(self): # stopwatch (m:ss) + bar counter; called ~4x/s from run()
|
def _fmt_t(self, s): # m:ss, or h:mm:ss past an hour
|
||||||
if self.running and self.play_start is not None:
|
s = int(s)
|
||||||
el = int(time.monotonic() - self.play_start)
|
return "%d:%02d:%02d" % (s // 3600, (s % 3600) // 60, s % 60) if s >= 3600 else "%d:%02d" % (s // 60, s % 60)
|
||||||
ts = "%d:%02d" % (el // 60, el % 60)
|
def draw_meters(self): # running time [of total] + bar [of total]; ~4x/s from run()
|
||||||
mlen = self.lanes[0]['steps'] if self.lanes else 1
|
run = self.running and self.play_start is not None
|
||||||
bs = "bar %d" % (self._m_steps // max(1, mlen) + 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
|
||||||
|
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<n>): 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:
|
else:
|
||||||
ts = "0:00"; bs = "bar -"
|
ts = self._fmt_t(el); bs = "bar %s" % cur
|
||||||
if ts != self._lastTs:
|
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:
|
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) ----------
|
# ---------- pad grid (each lane = a row of step pads; playhead lit as it plays) ----------
|
||||||
def _padbase(self, L, s):
|
def _padbase(self, L, s):
|
||||||
return 0 if L['mute'] else L['levels'][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):
|
def build_grid(self):
|
||||||
while len(self.g_grid): self.g_grid.pop()
|
while len(self.g_grid): self.g_grid.pop()
|
||||||
self.lane_pads = []; self.lane_lit = []
|
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, 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)
|
tg.x = 8; tg.y = cy - h // 2; self.g_grid.append(tg)
|
||||||
steps = L['steps']; sub = L['sub']; stepw = max(1, usable // steps)
|
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 = []
|
pads = []
|
||||||
for s in range(steps):
|
for s in range(steps):
|
||||||
base = self._padbase(L, s)
|
|
||||||
cxp = px0 + 6 + (s * usable) // steps # proportional -> beats line up across lanes
|
cxp = px0 + 6 + (s * usable) // steps # proportional -> beats line up across lanes
|
||||||
if s % sub == 0: # main beat -> filled square
|
if s % sub == 0: # main beat -> square
|
||||||
sq = self._sq(cxp, cy, side, base); self.g_grid.append(sq)
|
p = vectorio.Rectangle(pixel_shader=self.pad_pal, width=side, height=side,
|
||||||
pads.append(("fill", (sq,), base))
|
x=cxp - side // 2, y=cy - side // 2)
|
||||||
else: # off-beat -> hollow outline square
|
else: # subdivision -> circle
|
||||||
out = self._sq(cxp, cy, side, base if base else 8)
|
p = vectorio.Circle(pixel_shader=self.pad_pal, radius=rad, x=cxp, y=cy)
|
||||||
ins = self._sq(cxp, cy, inner, 9) # bg fill = the hollow centre
|
p.color_index = self._padbase(L, s); self.g_grid.append(p); pads.append(p)
|
||||||
self.g_grid.append(out); self.g_grid.append(ins)
|
|
||||||
pads.append(("out", (out, ins), base))
|
|
||||||
self.lane_pads.append(pads); self.lane_lit.append(-1)
|
self.lane_pads.append(pads); self.lane_lit.append(-1)
|
||||||
self.dirty = True
|
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):
|
def _move_playhead(self, li, step):
|
||||||
pads = self.lane_pads[li]; prev = self.lane_lit[li]
|
pads = self.lane_pads[li]; 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)
|
||||||
if step < len(pads): self._pad_lit(pads[step])
|
if step < len(pads): pads[step].color_index = self._padbase(self.lanes[li], step) + 4
|
||||||
self.lane_lit[li] = step; self.dirty = True
|
self.lane_lit[li] = step; self.dirty = True
|
||||||
def reset_playheads(self):
|
def reset_playheads(self):
|
||||||
for li, pads in enumerate(self.lane_pads):
|
for li, pads in enumerate(self.lane_pads):
|
||||||
prev = self.lane_lit[li]
|
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.lane_lit[li] = -1
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
|
|
||||||
|
|
@ -648,17 +648,18 @@ class App:
|
||||||
g = self.g_log
|
g = self.g_log
|
||||||
while len(g): g.pop()
|
while len(g): g.pop()
|
||||||
self.log_rows = []
|
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)
|
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)
|
||||||
if not self.log:
|
rows = [(i, e) for i, e in enumerate(self.log) if e.get("name") == self.name] # current track only
|
||||||
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)
|
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
|
self.dirty = True; return
|
||||||
y = LOG_TOP + LOG_ROWH + 2
|
y = LOG_TOP + LOG_ROWH + 2
|
||||||
for idx in range(min(LOG_ROWS, len(self.log))):
|
for k in range(min(LOG_ROWS, len(rows))):
|
||||||
e = self.log[idx]; armed = (idx == self._armed)
|
oi, e = rows[k]; armed = (oi == self._armed) # oi = index into self.log (for delete)
|
||||||
dur = "%d:%02d" % (e["dur"] // 60, e["dur"] % 60)
|
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)
|
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
|
y += LOG_ROWH
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
def _tap_log(self, x, ty):
|
def _tap_log(self, x, ty):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue