From 88104e3d5c0071ab6fce123ff4eed64d7fb14fed Mon Sep 17 00:00:00 2001 From: Me Here Date: Fri, 29 May 2026 11:43:54 -0500 Subject: [PATCH] PM_K-1 0.0.7: perform tempo ramps + gap trainer, show their indicators, log bars Device parser now reads the rmp// and tr/ tokens it previously ignored, and the firmware performs them: - Tempo ramp: steps bpm by every bars (resets to the start at each b segment boundary). Shows an amber ramp arrow + "+amt/everyb" (up/down by sign; no starting bpm, per request). - Gap trainer: cycles audible bars then silent bars (no click/MIDI/LED; playheads keep moving). Shows a play|rest symbol + "play/muteb". - Practice log entries now record + show bars played. Verified in the CPython harness: ramp 92->96->100->104->108 (+4 every 2 bars), gap mute cycle play,play,mute,mute, and the on-screen ramp indicator renders. Co-Authored-By: Claude Opus 4.7 (1M context) --- pico-cp/README.md | 16 +++-- pico-cp/__pycache__/app.cpython-312.pyc | Bin 56614 -> 61045 bytes pico-cp/app.py | 75 +++++++++++++++++++----- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/pico-cp/README.md b/pico-cp/README.md index 4ac4cdb..649a8bb 100644 --- a/pico-cp/README.md +++ b/pico-cp/README.md @@ -62,12 +62,16 @@ The editor also syncs the device clock, so the practice log gets real wall‑clo - **Joystick:** up/down = tempo, left/right = previous/next groove. - **Button A (GP15):** play / stop. **Button B (GP14):** tap tempo. -- **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`), 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.** +- **Screen:** VARASYS logo + firmware version + MIDI/USB status icons up top. Running time and bar count + show **of the segment total** when the track has a bar length (`b`), e.g. `1:23 of 2:00` and `bar 3 + of 16`. A track with a tempo ramp (`rmp`) shows a **ramp arrow + amount/every-bars** (e.g. `+4/2b`); a + gap-trainer track (`tr`) shows a **play|rest symbol + bars** (e.g. `2/2b`). Main beats are **squares**, + subdivisions are **circles**, with vertical gridlines lining the beats up across lanes. +- **RGB LED = run state:** dim **green** when stopped ("on"), dim **red** while playing, with the beat + pulsing brighter on top. (The screen background stays black — recoloring it forces a full-screen repaint.) +- The firmware **performs** ramps (tempo steps every N bars) and gap-trainer cycles (silent rest bars). +- **Touchscreen:** the bottom shows the **practice log for the current track** (time · BPM · duration · bars) + — 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. - The log is saved to `/history.json` (next to `programs.json`) in appliance mode and survives power‑cycles. diff --git a/pico-cp/__pycache__/app.cpython-312.pyc b/pico-cp/__pycache__/app.cpython-312.pyc index 47a56bdc98b319858e8441f45fd8a42164cecf53..779707f61f7d1a17c55060517a10dff8f33c8760 100644 GIT binary patch delta 14649 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�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%0A~BN$baIX z!|>E0xqZ=V*a$YUqYujpFrq!OE73`VDbFsZ3D2~^4mj}4%^k>Jk;|yq08cV(IO=bp z8BuO`h-zYKwZqFVW=V%YDkyCxGYAx$DKp4E>{+@vf+s6=79;bE4!FO3_8_vD47~t|uIkRvZ&i2p@Js7s3_ycaNP`RL@i;Cm%J`x^{($U}EuI4#j z=~U2CS2!4Xw9Ol?mP|6b_xVWbdR~PNKOv6J>5m4ZEr3@&rt*3AIC@=(isjY3>;d^2 z>5W1V(9Aw?v)y{#0~gpX&Ex@iN`&|5P%B8PLB-Cp(n&-C7ZZ|HW^53E^@dQwID~f` z(_8~rPGOM|?k$bw>+>Y(1CO=MrWU~p&Lf-#QEjz>9|j&)3DosMP%UFFlln{=F%BO_ zHh19sdLiU<;&x-g7K8_&&s)~}4>$?Fb<)6`VtX%$bs5l8$Xk zw)NSfLem)Tg<$EF6<2L!0#-3a$1 z{D2uQC9!upBpt%2coI|Pb}KwUC#@D;gKG^CUu~WPQ3M#?$vN%&Ny;| zSh@hzID!Erb9>YNP2g(@4CzZcRNqGry!-=r+V@__*${qoS^u(u$ia}YuzCCPM^lDs zM#4W@wC3`% z&Gf4at@DF4y4Y`nWs$LHWB&MEoGf@7=fLaVH^Xn+n5lkqwHnaO)SLV8NI7R>GEODU z6=Nftg6WN>mU#IzG{Ii7iSq7r_{(5teg`bkNp46imc0N! z0Z#f)66}59c3dPoaGnE?) z${4J^L(mfGM_?r%!q5iH{|^9B-dP7yX}U}^=3tqbsn_oDrCv+IP?nt9F3;f_=tGxt z;&7EV96yHNXE}x>#sRQc1vA`feRVl;6Vry3KnqjB(yih%5EbG5fZ5K&gH&wtuLxfw zFl(;^%QKD2blonD?xdyfM!O!yGRPR|O`#2kV+TOJZZox7M3tij-folmlWC}HC+7m5 zyLO2Rb1Q5us1FL&B$#fUi^EW@Cxp$#%z6C+k{y9cnausa-@T$LyMxeffLw|^s>ifZVpq>NKYmx*%5C0 zHks_sQE^PmF{eve@XD zeT$|tK^eyEM`p?qw7?`~{5^(H(K5A6aQ_qBg#@Rc;M@=#^R16n^RNa&87E7VIj+E1 z@0Z<>C#!O9Y~qfRp)y&-p zHmH`|sOC6TEq6nyfEqWnK~Of4CVaBWPp7*5MtCShZ=UULXqqL_bqIymz+XrXaWp$uatePwsSW#_Hu5 zUxokQx5#kY|5fZU*5<)Wer6?#_g0K`}8N!_i%Sk#G z&Olg-u!M^S%!D9juRvHyN-(FGKKE(7Yc~e#5b6;c0YoLVeJ8`^#D=MD2;U*yPh#i{ z0%}!Be=~0b)3kY*#uPO8q}Npx5xfv1|Az3t5CSki5FrSmMh^EVWIG1WWA+{l?M1*_ zZ_ff0SPWt4G{RYglUVLKgi{DtF-?Qj-p5eE zTZl|P{)nMuEVBY(H9{@|TbO@m#op9rwc7di7E3l6b^CvorclLj**ET_tFJ7KuhMXH euge&MJDpSBr}~v{ukHc=zkS;fP4ByM^8WylX|}rn delta 11191 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 J-@Sa`e*iJY46y(J diff --git a/pico-cp/app.py b/pico-cp/app.py index 6edeafe..75b4771 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.6" # firmware version (the A/B updater pushes/compares this) +APP_VERSION = "0.0.7" # 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: @@ -87,7 +87,7 @@ SOUND_GM = {"kick":36,"kick808":36,"kick909":36, "snare":38,"snare808":38,"snare GM_DEFAULT = 37 MIDI_VEL = {2: 120, 1: 90, 3: 45} # accent / normal / ghost MAXLANES = 5 # lanes shown on the pad grid (extras still play) -GRID_TOP = 150 # top of the pad grid (leaves room for stopwatch/bar) +GRID_TOP = 158 # top of the pad grid (leaves room for time/bar/ramp rows) LOG_TOP, LOG_ROWH, LOG_ROWS = 302, 16, 9 # practice-history log area (below the pad grid) MIN_LOG_SEC = 5 # don't log plays shorter than this PAD_DIM = (0x10161E, 0x0A3A52, 0x4A3010, 0x2A1D4A) # idle pad: mute / normal / accent / ghost @@ -196,7 +196,7 @@ PAT = {'X': 2, 'x': 1, 'g': 3, '.': 0, '-': 0, '_': 0} PRIO = {2: 3, 1: 2, 3: 1} def parse_program(s): - bpm = 120; lanes = []; bars = 0 + bpm = 120; lanes = []; bars = 0; ramp = None; trainer = None for tok in s.strip().split(';'): tok = tok.strip() if not tok: continue @@ -204,11 +204,23 @@ def parse_program(s): bpm = int(tok[1:]); continue if tok[0] == 'b' and tok[1:].isdigit(): # b = segment length in bars (totals + Continue) bars = int(tok[1:]); continue # (lane sounds like "beep:4" have a ':' -> not matched here) + if tok.startswith('rmp'): # rmp// tempo ramp (amount may be -) + p = tok[3:].split('/') + if len(p) == 3: + try: ramp = {'amt': int(p[1]), 'every': max(1, int(p[2]))} + except ValueError: pass + continue + if tok.startswith('tr') and '/' in tok and ':' not in tok: # tr/ gap trainer (bars) + p = tok[2:].split('/') + if len(p) == 2: + try: trainer = {'play': max(0, int(p[0])), 'mute': max(0, int(p[1]))} + except ValueError: pass + continue if ':' not in tok: continue lane = _parse_lane(tok) if lane: lanes.append(lane) if not lanes: lanes = [_parse_lane("beep:4")] - return max(30, min(300, bpm)), lanes, bars + return max(30, min(300, bpm)), lanes, bars, ramp, trainer def _parse_lane(tok): poly = '~' in tok; mute = '!' in tok @@ -342,6 +354,7 @@ class App: self._joyNext = 0 self._touchDown = False; self._touchSeen = 0 self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.bars = 0; self.rgb = (0, 0, 0) + self.ramp = None; self.trainer = None; self._lastbar = -1; self._muted = False; self._ramp_base = 120 self.programs = load_programs() self.dirty = True self.pad_pal = displayio.Palette(8) # 0-3 idle levels (mute/normal/accent/ghost), 4-7 the lit playhead @@ -385,6 +398,7 @@ class App: self.g_bpm = displayio.Group(); root.append(self.g_bpm) # big tempo (right) self.g_time = displayio.Group(); root.append(self.g_time) # elapsed [of total] (left) self.g_bar = displayio.Group(); root.append(self.g_bar) # bar [of total] (left) + self.g_train = displayio.Group(); root.append(self.g_train) # ramp / gap-trainer indicators self.g_name = displayio.Group(); root.append(self.g_name) # track title 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 @@ -407,9 +421,10 @@ class App: def load(self, i): n = len(self.programs); self.idx = i % n self.name, prog = self.programs[self.idx] - self.bpm, self.lanes, self.bars = parse_program(prog) - self.master = self.lanes[0] - self._reset_clock(); self.draw_bpm(); self.draw_status(); self.build_grid(); self.draw_log() + self.bpm, self.lanes, self.bars, self.ramp, self.trainer = parse_program(prog) + self.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False + self._reset_clock(); self.draw_bpm(); self.draw_status(); self.draw_train() + self.build_grid(); self.draw_log() def _step_dur(self, L, step): beat = 60_000_000_000 / self.bpm if L['poly']: # ~ polymeter: fit this lane's whole cycle into lane 1's bar @@ -480,14 +495,18 @@ class App: adv = False while now >= L['next']: L['step'] = (L['step'] + 1) % L['steps'] - if li == 0: self._m_steps += 1 # count master-lane steps -> bars + if li == 0: + self._m_steps += 1 # count master-lane steps -> bars + nb = self._m_steps // L['steps'] + if nb != self._lastbar: self._lastbar = nb; self._on_new_bar(nb) # ramp + gap-trainer lvl = 0 if L['mute'] else L['levels'][L['step']] if lvl > 0: fired.append(lvl) - self.midi_send(SOUND_GM.get(L['sound'], GM_DEFAULT), MIDI_VEL.get(lvl, 90)) # one note per lane + if not self._muted: # gap trainer: silent during the rest bars + self.midi_send(SOUND_GM.get(L['sound'], GM_DEFAULT), MIDI_VEL.get(lvl, 90)) L['next'] += self._step_dur(L, L['step']); adv = True if adv and li < len(self.lane_pads): self._move_playhead(li, L['step']) - if fired: + if fired and not self._muted: best = max(fired, key=lambda l: PRIO.get(l, 0)) if not MUTE_BUZZER and not self.midi_host: self.click(best) # computer plays it instead self.flash(best) @@ -498,6 +517,12 @@ class App: b = base[2] + (self.rgb[2]-base[2])*7//10 if abs(r-base[0])+abs(g-base[1])+abs(b-base[2]) < 6: r, g, b = base self.rgb = (r, g, b); self.led.set(r, g, b) + 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 + self._muted = bool(t and (t['play'] + t['mute']) and (bar % (t['play'] + t['mute'])) >= t['play']) # ---------- inputs ---------- def poll(self): @@ -547,9 +572,25 @@ class App: def draw_bpm(self): self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-12) def draw_status(self): # title (bright) + track number set apart (dim, right) - self._place(self.g_name, self.name[:22], 12, 116, C_TXT, C_BG, FONT_M) - self._place(self.g_idx, "%d/%d" % (self.idx + 1, len(self.programs)), 0, 120, + self._place(self.g_name, self.name[:22], 12, 130, C_TXT, C_BG, FONT_M) + self._place(self.g_idx, "%d/%d" % (self.idx + 1, len(self.programs)), 0, 134, C_DIM, C_BG, FONT_S, right_edge=WIDTH - 12) + def draw_train(self): # ramp + gap-trainer indicators (symbol + params), when set + g = self.g_train + while len(g): g.pop() + x = 12; y = 104 + if self.ramp: + up = self.ramp['amt'] >= 0 + pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)] # rising / falling ramp + g.append(vectorio.Polygon(pixel_shader=solid(C_AMBER), points=pts, x=x, y=y)); x += 16 + a = self.ramp['amt']; lbl = ("+%d" % a if a >= 0 else "%d" % a) + "/%db" % self.ramp['every'] + tg, w, h = make_text(lbl, FONT_S, C_AMBER, C_BG); tg.x = x; tg.y = y; g.append(tg); x += w + 14 + if self.trainer: + g.append(rect(x, y, 4, 9, C_CYAN)); g.append(rect(x + 6, y, 4, 9, C_DIM)) # play | rest + x += 14 + tg, w, h = make_text("%d/%db" % (self.trainer['play'], self.trainer['mute']), FONT_S, C_CYAN, C_BG) + tg.x = x; tg.y = y; g.append(tg) + self.dirty = True def draw_icons(self): # recolor the MIDI/USB icons by state (tear-free palette swap) if self.ic_midi_pal is not None: _recolor(self.ic_midi_pal, C_GREEN if self.midi_host else C_DIM, C_BG) @@ -572,9 +613,9 @@ class App: else: ts = self._fmt_t(el); bs = "bar %s" % cur if ts != self._lastTs: - self._place(self.g_time, ts, 12, 52, C_TXT, C_BG, FONT_M); self._lastTs = ts + self._place(self.g_time, ts, 12, 50, C_TXT, C_BG, FONT_M); self._lastTs = ts if bs != self._lastBs: - self._place(self.g_bar, bs, 12, 84, C_MUTE, C_BG, FONT_M); self._lastBs = bs + self._place(self.g_bar, bs, 12, 78, C_MUTE, C_BG, FONT_M); self._lastBs = bs # ---------- pad grid (each lane = a row of step pads; playhead lit as it plays) ---------- def _padbase(self, L, s): @@ -645,9 +686,10 @@ class App: if self.play_start is None: return dur = int(time.monotonic() - self.play_start); self.play_start = None if dur < MIN_LOG_SEC: return # skip plays under 5 seconds + mlen = self.lanes[0]['steps'] if self.lanes else 1 t = time.localtime() self.log.insert(0, {"t": "%02d:%02d" % (t.tm_hour, t.tm_min), "bpm": self.play_bpm, - "dur": dur, "name": self.play_name}) + "dur": dur, "bars": self._m_steps // max(1, mlen), "name": self.play_name}) del self.log[200:]; self._armed = None self._save_log(); self.draw_log() def draw_log(self): @@ -663,7 +705,8 @@ class App: for k in range(min(LOG_ROWS, len(rows))): oi, e = rows[k]; armed = (oi == self._armed) # oi = index into self.log (for delete) dur = "%d:%02d" % (e["dur"] // 60, e["dur"] % 60) - line = "%s%s %3d bpm %s" % ("x " if armed else "", e.get("t", "--:--"), e["bpm"], dur) + bars = e.get("bars", 0); bstr = (" %dbar" % bars) if bars else "" + line = "%s%s %3dbpm %s%s" % ("x " if armed else "", e.get("t", "--:--"), e["bpm"], dur, bstr) 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, oi)) y += LOG_ROWH