PM_X-1 0.0.3: compact 240x320 layout + piezo polarity flag + Pimoroni device filter
Three fixes the user reported on 0.0.2:
1. Layout still overlapped. 0.0.2 ported the Kit's pixel positions verbatim,
but those were designed for 480 px of height; on 320 the same Y values
stack on top of each other. This pass actually scales everything down:
- BPM big number was FONT_L (~30 px tall) -> FONT_M (~16 px tall).
- Time + bar meters were FONT_M -> FONT_S, tightly stacked at y=32/44
instead of y=50/78.
- Setlist tab + CONT y=66 (was 118); track title y=82 (was 134).
- GRID_TOP=104 (was 138). Frees ~34 px more vertical room for the
pad grid.
- Modal panels: PX/PW shrunk to use less of the 240-wide canvas, RH
22 (was 26), inter-line spacing 13-14 (was 14-16). Title strings
trimmed ("Practice log" instead of "Practice log (this track)").
2. No sound from the piezo. Two likely causes:
- SPEAKER_AUTO_MUTE was True. Live sync sends a FULL heartbeat over
USB-MIDI every 5s; the firmware sees those bytes and treats it as
"host listening" -> mutes the piezo. Default now False on Explorer
(toggle to Auto in Settings if you ARE using "Device audio" in the
editor).
- AMP_EN polarity. Added AMP_EN_ACTIVE_HIGH config flag (default True)
and a _amp(on) helper. If your specific board's amp is active-low,
flip to False at the top of CONFIG.
3. Firmware push stalled at chunk 1. Editor's _isDevicePort() only matched
"pico" / "circuitpython" / "usb_midi"; the Pimoroni Explorer reports
under a different name, so the editor broadcast to all MIDI outputs
and the ACK got lost in routing. Filter now also matches "pimoroni",
"explorer", "rp2350", and "varasys" (future-proofing).
The .mpy build dropped from 29.8 KB to 29.3 KB (smaller font footprint plus
fewer modal hint strings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
edb736c1d3
commit
ea7bb9bfee
3 changed files with 51 additions and 36 deletions
|
|
@ -1151,7 +1151,11 @@ async function loadFromDevice() {
|
||||||
let _midiAccess = null, _midiOn = false, _midiFlash = 0, _midiBeat = 0, _saveCb = null, _verCb = null;
|
let _midiAccess = null, _midiOn = false, _midiFlash = 0, _midiBeat = 0, _saveCb = null, _verCb = null;
|
||||||
function _midiInputs() { return _midiAccess ? [..._midiAccess.inputs.values()] : []; }
|
function _midiInputs() { return _midiAccess ? [..._midiAccess.inputs.values()] : []; }
|
||||||
function _midiOutputs() { return _midiAccess ? [..._midiAccess.outputs.values()] : []; }
|
function _midiOutputs() { return _midiAccess ? [..._midiAccess.outputs.values()] : []; }
|
||||||
function _isDevicePort(p) { const n = (p.name || "").toLowerCase(); return n.includes("pico") || n.includes("circuitpython") || n.includes("usb_midi"); }
|
function _isDevicePort(p) { // recognise PM_K-1 (Pico) and PM_X-1 (Pimoroni Explorer RP2350) USB-MIDI ports;
|
||||||
|
const n = (p.name || "").toLowerCase(); // anything unrecognised triggers a broadcast to all outputs - bad for ACK routing.
|
||||||
|
return n.includes("pico") || n.includes("circuitpython") || n.includes("usb_midi") ||
|
||||||
|
n.includes("pimoroni") || n.includes("explorer") || n.includes("rp2350") || n.includes("varasys");
|
||||||
|
}
|
||||||
function _send(bytes) { // send only to the PM_K-1 (not loopback ports like "Midi Through", which just echo)
|
function _send(bytes) { // send only to the PM_K-1 (not loopback ports like "Midi Through", which just echo)
|
||||||
const outs = _midiOutputs(), dev = outs.filter(_isDevicePort);
|
const outs = _midiOutputs(), dev = outs.filter(_isDevicePort);
|
||||||
for (const o of (dev.length ? dev : outs)) { try { o.send(bytes); } catch (_) {} }
|
for (const o of (dev.length ? dev : outs)) { try { o.send(bytes); } catch (_) {} }
|
||||||
|
|
|
||||||
|
|
@ -1147,7 +1147,11 @@ async function loadFromDevice() {
|
||||||
let _midiAccess = null, _midiOn = false, _midiFlash = 0, _midiBeat = 0, _saveCb = null, _verCb = null;
|
let _midiAccess = null, _midiOn = false, _midiFlash = 0, _midiBeat = 0, _saveCb = null, _verCb = null;
|
||||||
function _midiInputs() { return _midiAccess ? [..._midiAccess.inputs.values()] : []; }
|
function _midiInputs() { return _midiAccess ? [..._midiAccess.inputs.values()] : []; }
|
||||||
function _midiOutputs() { return _midiAccess ? [..._midiAccess.outputs.values()] : []; }
|
function _midiOutputs() { return _midiAccess ? [..._midiAccess.outputs.values()] : []; }
|
||||||
function _isDevicePort(p) { const n = (p.name || "").toLowerCase(); return n.includes("pico") || n.includes("circuitpython") || n.includes("usb_midi"); }
|
function _isDevicePort(p) { // recognise PM_K-1 (Pico) and PM_X-1 (Pimoroni Explorer RP2350) USB-MIDI ports;
|
||||||
|
const n = (p.name || "").toLowerCase(); // anything unrecognised triggers a broadcast to all outputs - bad for ACK routing.
|
||||||
|
return n.includes("pico") || n.includes("circuitpython") || n.includes("usb_midi") ||
|
||||||
|
n.includes("pimoroni") || n.includes("explorer") || n.includes("rp2350") || n.includes("varasys");
|
||||||
|
}
|
||||||
function _send(bytes) { // send only to the PM_K-1 (not loopback ports like "Midi Through", which just echo)
|
function _send(bytes) { // send only to the PM_K-1 (not loopback ports like "Midi Through", which just echo)
|
||||||
const outs = _midiOutputs(), dev = outs.filter(_isDevicePort);
|
const outs = _midiOutputs(), dev = outs.filter(_isDevicePort);
|
||||||
for (const o of (dev.length ? dev : outs)) { try { o.send(bytes); } catch (_) {} }
|
for (const o of (dev.length ? dev : outs)) { try { o.send(bytes); } catch (_) {} }
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
import board, busio, digitalio, pwmio, displayio, vectorio, time, json, gc, os, supervisor
|
import board, busio, digitalio, 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)
|
||||||
DEVICE_ID = "X" # 'X' = Explorer, 'K' = 52Pi kit (per docs/livesync-protocol.md and the version reply)
|
DEVICE_ID = "X" # 'X' = Explorer, 'K' = 52Pi kit (per docs/livesync-protocol.md and the version reply)
|
||||||
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
|
||||||
|
|
@ -36,7 +36,11 @@ MIDI_CLOCK_OUT_TRANSPORT = True
|
||||||
MIDI_CLOCK_IN = False # follow an external 24 PPQN clock
|
MIDI_CLOCK_IN = False # follow an external 24 PPQN clock
|
||||||
MIDI_CLOCK_IN_TRANSPORT = True
|
MIDI_CLOCK_IN_TRANSPORT = True
|
||||||
MUTE_SPEAKER = False # always silence the on-board piezo
|
MUTE_SPEAKER = False # always silence the on-board piezo
|
||||||
SPEAKER_AUTO_MUTE = True # auto-mute the piezo when a MIDI host is listening
|
SPEAKER_AUTO_MUTE = False # auto-mute the piezo when a MIDI host is listening. DEFAULT OFF on Explorer:
|
||||||
|
# Live sync sends a FULL heartbeat every 5s which would silence the piezo otherwise.
|
||||||
|
# Toggle to Auto in Settings if you ARE using "Device audio" in the editor.
|
||||||
|
AMP_EN_ACTIVE_HIGH = True # piezo amp enable polarity. If you HEAR sound from the piezo only when click()
|
||||||
|
# has just timed out (~22ms after a beat), flip this to False - your amp is active-low.
|
||||||
DISPLAY_ROTATION = 270 # 0=native landscape; 90/270 = portrait. Hold the device with A/B/C
|
DISPLAY_ROTATION = 270 # 0=native landscape; 90/270 = portrait. Hold the device with A/B/C
|
||||||
# on top: try 270 first; if upside-down try 90; 180 = flipped landscape.
|
# on top: try 270 first; if upside-down try 90; 180 = flipped landscape.
|
||||||
|
|
||||||
|
|
@ -51,10 +55,10 @@ P_SDA, P_SCL = board.GP20, board.GP21 # QwSTEMMA (unuse
|
||||||
# call display.rotation = DISPLAY_ROTATION (above) to turn it portrait so the layout has the same
|
# call display.rotation = DISPLAY_ROTATION (above) to turn it portrait so the layout has the same
|
||||||
# shape as the PM_K-1 Kit's portrait UI but in 240x320 instead of 320x480.
|
# shape as the PM_K-1 Kit's portrait UI but in 240x320 instead of 320x480.
|
||||||
WIDTH, HEIGHT = 240, 320
|
WIDTH, HEIGHT = 240, 320
|
||||||
GRID_TOP = 138 # top of the pad grid (header + meters + title fit above)
|
GRID_TOP = 104 # top of the pad grid (compact header + meters + title fit above)
|
||||||
MAXLANES = 6 # lanes shown on the pad grid (parser still accepts more; they just play silent visually)
|
MAXLANES = 6 # lanes shown on the pad grid (parser still accepts more; they just play silent visually)
|
||||||
MIN_LOG_SEC = 5 # don't log plays shorter than this
|
MIN_LOG_SEC = 5 # don't log plays shorter than this
|
||||||
LOG_MENU_ROWS = 8 # log entries shown in the Practice-log menu screen (more vertical room in portrait)
|
LOG_MENU_ROWS = 8 # log entries shown in the Practice-log menu screen
|
||||||
|
|
||||||
# ----- BUILT-IN playlists: same defaults as the Kit so the two firmwares feel identical -----
|
# ----- BUILT-IN playlists: same defaults as the Kit so the two firmwares feel identical -----
|
||||||
BUILTIN_SETLISTS = [
|
BUILTIN_SETLISTS = [
|
||||||
|
|
@ -337,7 +341,7 @@ class App:
|
||||||
self._fw = None; self._fw_n = 0 # chunked firmware transfer state
|
self._fw = None; self._fw_n = 0 # chunked firmware transfer state
|
||||||
self.spk = pwmio.PWMOut(P_AUDIO, frequency=1600, variable_frequency=True, duty_cycle=0)
|
self.spk = pwmio.PWMOut(P_AUDIO, frequency=1600, variable_frequency=True, duty_cycle=0)
|
||||||
self.amp_en = digitalio.DigitalInOut(P_AMPEN); self.amp_en.direction = digitalio.Direction.OUTPUT
|
self.amp_en = digitalio.DigitalInOut(P_AMPEN); self.amp_en.direction = digitalio.Direction.OUTPUT
|
||||||
self.amp_en.value = False # amp off when no audio playing (saves power, kills hum)
|
self._amp(False) # amp off when no audio playing (saves power, kills hum)
|
||||||
self.spk_off = 0
|
self.spk_off = 0
|
||||||
# buttons - all active-low with internal pull-ups
|
# buttons - all active-low with internal pull-ups
|
||||||
self.btnA = self._btn(P_BTNA); self.btnB = self._btn(P_BTNB); self.btnC = self._btn(P_BTNC)
|
self.btnA = self._btn(P_BTNA); self.btnB = self._btn(P_BTNB); self.btnC = self._btn(P_BTNC)
|
||||||
|
|
@ -495,7 +499,7 @@ class App:
|
||||||
def _draw_menu(self):
|
def _draw_menu(self):
|
||||||
g = self.g_overlay
|
g = self.g_overlay
|
||||||
while len(g): g.pop()
|
while len(g): g.pop()
|
||||||
PX, PY, PW, RH = 24, 36, WIDTH - 48, 26
|
PX, PY, PW, RH = 18, 32, WIDTH - 36, 22
|
||||||
rows = (
|
rows = (
|
||||||
("Continue: " + ("on" if self.continue_on else "off"), None, self._menu_toggle_continue),
|
("Continue: " + ("on" if self.continue_on else "off"), None, self._menu_toggle_continue),
|
||||||
("Settings >", None, self._show_settings),
|
("Settings >", None, self._show_settings),
|
||||||
|
|
@ -527,7 +531,7 @@ class App:
|
||||||
def _draw_settings(self):
|
def _draw_settings(self):
|
||||||
g = self.g_overlay
|
g = self.g_overlay
|
||||||
while len(g): g.pop()
|
while len(g): g.pop()
|
||||||
PX, PY, PW, RH = 14, 30, WIDTH - 28, 26
|
PX, PY, PW, RH = 10, 28, WIDTH - 20, 22
|
||||||
sm = "Off" if MUTE_SPEAKER else ("Auto" if SPEAKER_AUTO_MUTE else "Always")
|
sm = "Off" if MUTE_SPEAKER else ("Auto" if SPEAKER_AUTO_MUTE else "Always")
|
||||||
rows = (
|
rows = (
|
||||||
("Speaker", sm, self._adj_speaker),
|
("Speaker", sm, self._adj_speaker),
|
||||||
|
|
@ -557,7 +561,7 @@ class App:
|
||||||
cur = "off" if MUTE_SPEAKER else ("auto" if SPEAKER_AUTO_MUTE else "always")
|
cur = "off" if MUTE_SPEAKER else ("auto" if SPEAKER_AUTO_MUTE else "always")
|
||||||
i = (modes.index(cur) + d) % 3
|
i = (modes.index(cur) + d) % 3
|
||||||
MUTE_SPEAKER = (modes[i] == "off"); SPEAKER_AUTO_MUTE = (modes[i] == "auto")
|
MUTE_SPEAKER = (modes[i] == "off"); SPEAKER_AUTO_MUTE = (modes[i] == "auto")
|
||||||
if MUTE_SPEAKER: self.spk.duty_cycle = 0; self.amp_en.value = False
|
if MUTE_SPEAKER: self.spk.duty_cycle = 0; self._amp(False)
|
||||||
self._save_settings(); self._draw_settings()
|
self._save_settings(); self._draw_settings()
|
||||||
def _adj_midi_out(self, d):
|
def _adj_midi_out(self, d):
|
||||||
global MIDI_ENABLED
|
global MIDI_ENABLED
|
||||||
|
|
@ -584,9 +588,9 @@ class App:
|
||||||
def _draw_help(self):
|
def _draw_help(self):
|
||||||
g = self.g_overlay
|
g = self.g_overlay
|
||||||
while len(g): g.pop()
|
while len(g): g.pop()
|
||||||
PX, PY, PW = 12, 30, WIDTH - 24
|
PX, PY, PW = 10, 26, WIDTH - 20
|
||||||
title, lines = HELP_PAGES[self._help_page]
|
title, lines = HELP_PAGES[self._help_page]
|
||||||
PH = 26 + 14 * len(lines) + 22
|
PH = 22 + 13 * len(lines) + 18
|
||||||
g.append(rect(PX, PY, PW, PH, C_PANEL)); g.append(rect(PX, PY, PW, 2, C_CYAN))
|
g.append(rect(PX, PY, PW, PH, C_PANEL)); g.append(rect(PX, PY, PW, 2, C_CYAN))
|
||||||
t, w, h = make_text(title, FONT_M, C_TXT, C_PANEL); t.x = PX + 10; t.y = PY + 5; g.append(t)
|
t, w, h = make_text(title, FONT_M, C_TXT, C_PANEL); t.x = PX + 10; t.y = PY + 5; g.append(t)
|
||||||
pi, piw, pih = make_text("%d / %d" % (self._help_page + 1, len(HELP_PAGES)), FONT_S, C_DIM, C_PANEL)
|
pi, piw, pih = make_text("%d / %d" % (self._help_page + 1, len(HELP_PAGES)), FONT_S, C_DIM, C_PANEL)
|
||||||
|
|
@ -624,13 +628,13 @@ class App:
|
||||||
)
|
)
|
||||||
g = self.g_overlay
|
g = self.g_overlay
|
||||||
while len(g): g.pop()
|
while len(g): g.pop()
|
||||||
PX, PY, PW = 24, 28, WIDTH - 48; PH = 12 + 16 * len(lines) + 22
|
PX, PY, PW = 20, 26, WIDTH - 40; PH = 10 + 14 * len(lines) + 20
|
||||||
g.append(rect(PX, PY, PW, PH, C_PANEL)); g.append(rect(PX, PY, PW, 2, C_CYAN))
|
g.append(rect(PX, PY, PW, PH, C_PANEL)); g.append(rect(PX, PY, PW, 2, C_CYAN))
|
||||||
yy = PY + 8
|
yy = PY + 8
|
||||||
for text, col in lines:
|
for text, col in lines:
|
||||||
if col is not None:
|
if col is not None:
|
||||||
lt, lw, lh = make_text(text, FONT_S, col, C_PANEL); lt.x = PX + 14; lt.y = yy; g.append(lt)
|
lt, lw, lh = make_text(text, FONT_S, col, C_PANEL); lt.x = PX + 12; lt.y = yy; g.append(lt)
|
||||||
yy += 16
|
yy += 14
|
||||||
hint, hw, hh = make_text("C = close", FONT_S, C_DIM, C_PANEL)
|
hint, hw, hh = make_text("C = close", FONT_S, C_DIM, C_PANEL)
|
||||||
hint.x = PX + 14; hint.y = PY + PH - 14; g.append(hint)
|
hint.x = PX + 14; hint.y = PY + PH - 14; g.append(hint)
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
|
|
@ -642,21 +646,21 @@ class App:
|
||||||
def _draw_log_modal(self):
|
def _draw_log_modal(self):
|
||||||
g = self.g_overlay
|
g = self.g_overlay
|
||||||
while len(g): g.pop()
|
while len(g): g.pop()
|
||||||
PX, PY, PW = 8, 28, WIDTH - 16; PH = 12 + LOG_MENU_ROWS * 16 + 28
|
PX, PY, PW = 6, 26, WIDTH - 12; PH = 12 + LOG_MENU_ROWS * 14 + 22
|
||||||
g.append(rect(PX, PY, PW, PH, C_PANEL)); g.append(rect(PX, PY, PW, 2, C_CYAN))
|
g.append(rect(PX, PY, PW, PH, C_PANEL)); g.append(rect(PX, PY, PW, 2, C_CYAN))
|
||||||
t, w, h = make_text("Practice log (this track)", FONT_M, C_TXT, C_PANEL); t.x = PX + 10; t.y = PY + 5; g.append(t)
|
t, w, h = make_text("Practice log", FONT_M, C_TXT, C_PANEL); t.x = PX + 10; t.y = PY + 4; g.append(t)
|
||||||
rows = [(i, e) for i, e in enumerate(self.log) if e.get("name") == self.name]
|
rows = [(i, e) for i, e in enumerate(self.log) if e.get("name") == self.name]
|
||||||
if not rows:
|
if not rows:
|
||||||
tg, w, h = make_text("no plays over 5s yet", FONT_S, C_DIM, C_PANEL); tg.x = PX + 14; tg.y = PY + 32; g.append(tg)
|
tg, w, h = make_text("no plays over 5s yet", FONT_S, C_DIM, C_PANEL); tg.x = PX + 12; tg.y = PY + 28; g.append(tg)
|
||||||
else:
|
else:
|
||||||
top = self._log_scroll; yy = PY + 28
|
top = self._log_scroll; yy = PY + 24
|
||||||
for k in range(min(LOG_MENU_ROWS, len(rows) - top)):
|
for k in range(min(LOG_MENU_ROWS, len(rows) - top)):
|
||||||
_oi, e = rows[top + k]
|
_oi, e = rows[top + k]
|
||||||
dur = "%d:%02d" % (e["dur"] // 60, e["dur"] % 60)
|
dur = "%d:%02d" % (e["dur"] // 60, e["dur"] % 60)
|
||||||
bars = e.get("bars", 0); bstr = (" %dbar" % bars) if bars else ""
|
bars = e.get("bars", 0); bstr = (" %dbar" % bars) if bars else ""
|
||||||
line = "%s %3dbpm %s%s" % (e.get("t", "--:--"), e["bpm"], dur, bstr)
|
line = "%s %3dbpm %s%s" % (e.get("t", "--:--"), e["bpm"], dur, bstr)
|
||||||
lt, lw, lh = make_text(line, FONT_S, C_TXT, C_PANEL); lt.x = PX + 14; lt.y = yy; g.append(lt)
|
lt, lw, lh = make_text(line, FONT_S, C_TXT, C_PANEL); lt.x = PX + 12; lt.y = yy; g.append(lt)
|
||||||
yy += 16
|
yy += 14
|
||||||
hint, hw, hh = make_text("X/Z scroll, C close", FONT_S, C_DIM, C_PANEL)
|
hint, hw, hh = make_text("X/Z scroll, C close", FONT_S, C_DIM, C_PANEL)
|
||||||
hint.x = PX + 10; hint.y = PY + PH - 14; g.append(hint)
|
hint.x = PX + 10; hint.y = PY + PH - 14; g.append(hint)
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
|
|
@ -706,11 +710,13 @@ class App:
|
||||||
self._seg_start = time.monotonic()
|
self._seg_start = time.monotonic()
|
||||||
|
|
||||||
# ---------- audio + run-state indicator ----------
|
# ---------- audio + run-state indicator ----------
|
||||||
|
def _amp(self, on): # respect AMP_EN_ACTIVE_HIGH (flip in CONFIG if your amp is active-low)
|
||||||
|
self.amp_en.value = on if AMP_EN_ACTIVE_HIGH else not on
|
||||||
def click(self, level):
|
def click(self, level):
|
||||||
self.amp_en.value = True # enable the amp briefly while we drive the piezo
|
self._amp(True) # enable the amp briefly while we drive the piezo
|
||||||
self.spk.frequency = {2: 2300, 1: 1600, 3: 1050}.get(level, 1600)
|
self.spk.frequency = {2: 2300, 1: 1600, 3: 1050}.get(level, 1600)
|
||||||
self.spk.duty_cycle = {2: 42000, 1: 30000, 3: 14000}.get(level, 30000)
|
self.spk.duty_cycle = {2: 42000, 1: 30000, 3: 14000}.get(level, 30000)
|
||||||
self.spk_off = time.monotonic_ns() + 22_000_000 # silence + amp_en off scheduled in tick()
|
self.spk_off = time.monotonic_ns() + 22_000_000 # silence + amp off scheduled in tick()
|
||||||
def _set_run_dot(self):
|
def _set_run_dot(self):
|
||||||
self.run_dot_pal[0] = C_RUN_GO if self.running else C_RUN_IDLE
|
self.run_dot_pal[0] = C_RUN_GO if self.running else C_RUN_IDLE
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
|
|
@ -853,7 +859,7 @@ class App:
|
||||||
try: self.midi.write(self._start_byte)
|
try: self.midi.write(self._start_byte)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
else:
|
else:
|
||||||
self.spk.duty_cycle = 0; self.amp_en.value = False; self.reset_playheads(); self._log_play()
|
self.spk.duty_cycle = 0; self._amp(False); self.reset_playheads(); self._log_play()
|
||||||
if MIDI_CLOCK_OUT and MIDI_CLOCK_OUT_TRANSPORT and self.midi is not None:
|
if MIDI_CLOCK_OUT and MIDI_CLOCK_OUT_TRANSPORT and self.midi is not None:
|
||||||
try: self.midi.write(self._stop_byte)
|
try: self.midi.write(self._stop_byte)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
@ -885,7 +891,7 @@ class App:
|
||||||
def tick(self):
|
def tick(self):
|
||||||
now = time.monotonic_ns()
|
now = time.monotonic_ns()
|
||||||
if self.spk_off and now >= self.spk_off:
|
if self.spk_off and now >= self.spk_off:
|
||||||
self.spk.duty_cycle = 0; self.spk_off = 0; self.amp_en.value = False
|
self.spk.duty_cycle = 0; self.spk_off = 0; self._amp(False)
|
||||||
if self._slaved and (now - self._clock_in_last_t) > 1_000_000_000: self._slaved = False
|
if self._slaved and (now - self._clock_in_last_t) > 1_000_000_000: self._slaved = False
|
||||||
if self.running:
|
if self.running:
|
||||||
fired_best = 0; fired_prio = -1
|
fired_best = 0; fired_prio = -1
|
||||||
|
|
@ -1118,7 +1124,7 @@ class App:
|
||||||
if host != self.midi_host:
|
if host != self.midi_host:
|
||||||
self.midi_host = host
|
self.midi_host = host
|
||||||
if host and SPEAKER_AUTO_MUTE:
|
if host and SPEAKER_AUTO_MUTE:
|
||||||
self.spk.duty_cycle = 0; self.amp_en.value = False
|
self.spk.duty_cycle = 0; self._amp(False)
|
||||||
self._set_run_dot(); self.draw_icons()
|
self._set_run_dot(); self.draw_icons()
|
||||||
uc = bool(getattr(supervisor.runtime, "usb_connected", True))
|
uc = bool(getattr(supervisor.runtime, "usb_connected", True))
|
||||||
if uc != self.usb_conn:
|
if uc != self.usb_conn:
|
||||||
|
|
@ -1128,19 +1134,20 @@ class App:
|
||||||
def draw_bpm(self):
|
def draw_bpm(self):
|
||||||
if self.bpm == self._displayed_bpm: return
|
if self.bpm == self._displayed_bpm: return
|
||||||
self._displayed_bpm = self.bpm
|
self._displayed_bpm = self.bpm
|
||||||
self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-10)
|
# Smaller than the Kit's BPM: FONT_M instead of FONT_L. Kit's FONT_L was ~30px tall at 480 - too big proportionally at 320.
|
||||||
|
self._place(self.g_bpm, str(self.bpm), 0, 30, C_TXT, C_BG, FONT_M, right_edge=WIDTH-8)
|
||||||
def draw_status(self):
|
def draw_status(self):
|
||||||
sl = self.setlists[self.sl]
|
sl = self.setlists[self.sl]
|
||||||
# setlist tab line at y=118 (matches the Kit's spacing); muted = built-in, cyan = your own
|
# setlist tab line at y=66; muted = built-in, cyan = your own
|
||||||
self._place(self.g_idx, "%s %d/%d" % (sl['title'][:13], self.idx + 1, len(sl['items'])),
|
self._place(self.g_idx, "%s %d/%d" % (sl['title'][:13], self.idx + 1, len(sl['items'])),
|
||||||
10, 118, C_MUTE if sl['builtin'] else C_CYAN, C_BG, FONT_S)
|
6, 66, C_MUTE if sl['builtin'] else C_CYAN, C_BG, FONT_S)
|
||||||
self._place(self.g_cont, "CONT", 0, 118, C_GREEN if self.continue_on else C_DIM, C_BG, FONT_S, right_edge=WIDTH-10)
|
self._place(self.g_cont, "CONT", 0, 66, C_GREEN if self.continue_on else C_DIM, C_BG, FONT_S, right_edge=WIDTH-6)
|
||||||
# track title at y=134 (FONT_M; ~16 px tall, fits above GRID_TOP=138)
|
# track title at y=82 (FONT_M; ~16 px tall, fits above GRID_TOP=104)
|
||||||
self._place(self.g_name, self.name[:20], 10, 134, C_TXT, C_BG, FONT_M)
|
self._place(self.g_name, self.name[:22], 6, 82, C_TXT, C_BG, FONT_M)
|
||||||
def draw_train(self):
|
def draw_train(self):
|
||||||
g = self.g_train
|
g = self.g_train
|
||||||
while len(g): g.pop()
|
while len(g): g.pop()
|
||||||
x = 10; y = 100 # ramp / gap-trainer indicators on a single row above the setlist tab
|
x = 6; y = 52 # ramp / gap-trainer indicators below the meters row, above the setlist tab
|
||||||
if self.ramp:
|
if self.ramp:
|
||||||
up = self.ramp['amt'] >= 0
|
up = self.ramp['amt'] >= 0
|
||||||
pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)]
|
pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)]
|
||||||
|
|
@ -1175,9 +1182,9 @@ class App:
|
||||||
else:
|
else:
|
||||||
ts = self._fmt_t(el); bs = "bar %s" % cur
|
ts = self._fmt_t(el); bs = "bar %s" % cur
|
||||||
if ts != self._lastTs:
|
if ts != self._lastTs:
|
||||||
self._place(self.g_time, ts, 10, 50, C_TXT, C_BG, FONT_M); self._lastTs = ts
|
self._place(self.g_time, ts, 6, 32, C_TXT, C_BG, FONT_S); self._lastTs = ts
|
||||||
if bs != self._lastBs:
|
if bs != self._lastBs:
|
||||||
self._place(self.g_bar, bs, 10, 78, C_MUTE, C_BG, FONT_M); self._lastBs = bs
|
self._place(self.g_bar, bs, 6, 44, C_MUTE, C_BG, FONT_S); self._lastBs = bs
|
||||||
|
|
||||||
# ---------- pad grid (chunked rebuild; per-pad chunks so audio interleaves) ----------
|
# ---------- pad grid (chunked rebuild; per-pad chunks so audio interleaves) ----------
|
||||||
def _padbase(self, L, s):
|
def _padbase(self, L, s):
|
||||||
|
|
@ -1318,7 +1325,7 @@ class App:
|
||||||
def _slave_stop(self):
|
def _slave_stop(self):
|
||||||
if self.running:
|
if self.running:
|
||||||
self.running = False
|
self.running = False
|
||||||
self.spk.duty_cycle = 0; self.amp_en.value = False
|
self.spk.duty_cycle = 0; self._amp(False)
|
||||||
self.reset_playheads(); self._log_play()
|
self.reset_playheads(); self._log_play()
|
||||||
self._set_run_dot(); self.draw_meters()
|
self._set_run_dot(); self.draw_meters()
|
||||||
self._clock_in_last_t = 0; self._clock_in_avg = 0; self._slaved = False
|
self._clock_in_last_t = 0; self._clock_in_avg = 0; self._slaved = False
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue