Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions pistomp/lcd320x240.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
ShroudedPanel,
TextWidget,
)
from uilib import profiling
from uilib.gridpanel import GridPanel, TILE_W, CHANNEL
from uilib.pygame_init import font as _make_font
from uilib.lcd_ili9341 import LcdIli9341
Expand Down Expand Up @@ -303,8 +302,7 @@ def handle(self, event: ControllerEvent) -> bool:
return top.handle(event) if isinstance(top, InputSink) else False

def poll_updates(self):
with profiling.measure("poll_updates"):
self._poll_updates()
self._poll_updates()

def _poll_updates(self):
for d in self.w_parameter_dialogs.values():
Expand Down
58 changes: 26 additions & 32 deletions pistomp/nam/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@
import common.token as Token
import pistomp.switchstate as switchstate

from uilib import profiling

from uilib.panel import Panel
from pistomp.input.event import ControllerEvent, EncoderEvent, SwitchEvent, SwitchEventKind
from pistomp.nam import routing
Expand Down Expand Up @@ -249,32 +247,31 @@ def _color_at(t: float) -> tuple[int, int, int]:
return stops[-1][1]

def _draw(self, ctx: PaintContext) -> None:
with profiling.measure("nam.bar._draw"):
n = self._N_SEGS
filled = int(self._progress * n)
sw = self._seg_w
bx = self._MARGIN
by = self._BAR_Y
ctx.fill((0, 0, 0))

for i in range(n):
t = i / (n - 1) if n > 1 else 0.0
r, g, b = self._color_at(t)
if i < filled:
color: tuple[int, int, int] = (r, g, b)
else:
color = (int(r * _BAR_DIM), int(g * _BAR_DIM), int(b * _BAR_DIM))
ctx.draw_rectangle(Box.xywh(bx + i * (sw + self._SEG_GAP), by, sw, self._BAR_H), fill=color)

label_y = by + self._BAR_H + self._LABEL_GAP
right_x = ctx.width - self._MARGIN

elapsed_str = _fmt_time(self._elapsed)
ctx.draw_text((bx, label_y), elapsed_str, fill=(130, 118, 80), font=self._font)

remaining_str = f"−{_fmt_time(self._remaining)}"
rw, _ = get_text_size(remaining_str, self._font)
ctx.draw_text((right_x - rw, label_y), remaining_str, fill=(205, 180, 110), font=self._font)
n = self._N_SEGS
filled = int(self._progress * n)
sw = self._seg_w
bx = self._MARGIN
by = self._BAR_Y
ctx.fill((0, 0, 0))

for i in range(n):
t = i / (n - 1) if n > 1 else 0.0
r, g, b = self._color_at(t)
if i < filled:
color: tuple[int, int, int] = (r, g, b)
else:
color = (int(r * _BAR_DIM), int(g * _BAR_DIM), int(b * _BAR_DIM))
ctx.draw_rectangle(Box.xywh(bx + i * (sw + self._SEG_GAP), by, sw, self._BAR_H), fill=color)

label_y = by + self._BAR_H + self._LABEL_GAP
right_x = ctx.width - self._MARGIN

elapsed_str = _fmt_time(self._elapsed)
ctx.draw_text((bx, label_y), elapsed_str, fill=(130, 118, 80), font=self._font)

remaining_str = f"−{_fmt_time(self._remaining)}"
rw, _ = get_text_size(remaining_str, self._font)
ctx.draw_text((right_x - rw, label_y), remaining_str, fill=(205, 180, 110), font=self._font)


class LevelMeter(Widget):
Expand Down Expand Up @@ -597,7 +594,6 @@ def __init__(

# Read initial values from hardware to seed the internal trackers
self._init_knob_values()
profiling.maybe_start()

# Connect In2 → Out1 so the user can hear the amp while the panel is open.
routing.connect_monitor()
Expand Down Expand Up @@ -675,10 +671,8 @@ def tick(self) -> None:
self._last_tick = now

state = self._engine.state
profiling.set_context_tag(state.name.lower())

with profiling.measure("nam.tick"):
self._tick_body(now, dt, state)
self._tick_body(now, dt, state)

def _tick_body(self, now: float, dt: float, state: CaptureState) -> None:
if state != self._last_state:
Expand Down
9 changes: 3 additions & 6 deletions pistomp/tuner/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import numpy as np
import numpy.typing as npt

from uilib import profiling
from pistomp.tuner.ringbuffer import RingBuffer
from pistomp.tuner.source import AudioSource
from pistomp.tuner.yin import YinDetector
Expand Down Expand Up @@ -136,8 +135,7 @@ def _process(self) -> None:
if not self._ring.read_latest(self.FRAME_SIZE, self._frame):
return

with profiling.measure("rms", bin_override="dsp"):
rms = float(np.sqrt(np.mean(self._frame**2)))
rms = float(np.sqrt(np.mean(self._frame**2)))

if rms < self.SILENCE_RMS:
self._freq_history.clear()
Expand All @@ -160,9 +158,8 @@ def _process(self) -> None:
self._onset_holdoff -= 1
return

with profiling.measure("detect_pitch(YIN)", bin_override="dsp"):
assert self._detector is not None
estimate = self._detector.detect(self._frame)
assert self._detector is not None
estimate = self._detector.detect(self._frame)
if estimate is None:
return

Expand Down
10 changes: 3 additions & 7 deletions pistomp/tuner/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from collections import deque
from typing import Callable, Literal

from uilib import profiling
from common.fonts import font_path
from uilib.box import Box
from uilib.config import Config
Expand Down Expand Up @@ -409,7 +408,6 @@ def __init__(
self.add_sel_widget(self._btn_input)
self._apply_mute_style(muted)
self._cents_history: deque[float] = deque(maxlen=3)
profiling.maybe_start()

def _create_engine(self, port: int) -> TunerBackend:
return self._backend_factory(port)
Expand Down Expand Up @@ -450,8 +448,6 @@ def tick(self) -> None:
self._cents_history.clear()
cents = None

profiling.set_cents_bin(profiling.bin_for_cents(cents))
with profiling.measure("TunerPanel.tick"):
self._header.tick(reading)
self._bar.tick(cents)
self._strobe.tick(cents)
self._header.tick(reading)
self._bar.tick(cents)
self._strobe.tick(cents)
2 changes: 1 addition & 1 deletion plugins/eq/parametric.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from uilib.box import Box
from uilib.config import Config
from uilib.footswitch import tint_mask
from uilib.glyphs.tint import tint_mask
from uilib.glyphs.circle import CircleGlyph, RingGlyph
from uilib.misc import INACTIVE_SHADE, InputEvent, get_text_size
from uilib.widget import Widget
Expand Down
7 changes: 2 additions & 5 deletions plugins/layouts/reticule_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from uilib.box import Box
from uilib.glyphs.circle import RingGlyph
from uilib.glyphs.tint import tint_mask
from uilib.misc import INACTIVE_SHADE, get_text_size, shade_color
from uilib.widget import Widget

Expand Down Expand Up @@ -188,8 +189,4 @@ def _draw_crosshair(self, ctx, shade: float) -> None:
for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
ctx.draw_line([(x + dx * 3, y + dy * 3), (x + dx * 8, y + dy * 8)], fill=col, width=1)
ring = RingGlyph(6, ring_half=0.9).render()
tinted = ring.copy()
cs = pygame.Surface(ring.get_size(), pygame.SRCALPHA)
cs.fill(col)
tinted.blit(cs, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
ctx.paste(tinted, (x - 7, y - 7))
ctx.paste(tint_mask(ring, col), (x - 7, y - 7))
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion uilib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
from uilib.config import Config
from uilib.container import ContainerWidget
from uilib.dialog import ConfirmDialog, Dialog, DialogDecorator, MessageDialog
from uilib.footswitch import FootswitchWidget, TapTempoProtocol, tint_mask
from uilib.footswitch import FootswitchWidget, TapTempoProtocol
from uilib.glyphs.tint import tint_mask
from uilib.icon import Icon
from uilib.image import ImageWidget, load_surface
from uilib.menu import Menu
Expand Down
2 changes: 1 addition & 1 deletion uilib/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _adjust_box(self):
# Extra margin around text
tmargin = 2

self.tw, self.th = get_text_size(self.title.text, self.title.font, self.title.font_metrics)
self.tw, self.th = get_text_size(self.title.text, self.title.font)
self.box = Box(pb.x0 - m, pb.y0 - self.th - m - tmargin, pb.x1 + m, pb.y1 + m)
trace(self, "new box=", self.box)
tbox = Box(o, o, self.box.width - o, self.th + o)
Expand Down
10 changes: 1 addition & 9 deletions uilib/footswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from uilib.box import Box
from uilib.config import Color, Config
from uilib.glyphs import CircleGlyph, RingGlyph
from uilib.glyphs.tint import tint_mask
from uilib.misc import get_text_size
from uilib.paint import PaintContext
from uilib.widget import Widget
Expand Down Expand Up @@ -57,15 +58,6 @@ def get_bpm(self) -> float: ...
TITLE_WHITE: Color = (255, 255, 255)


def tint_mask(mask: pygame.Surface, color: Color) -> pygame.Surface:
"""Tint a white alpha-mask glyph into `color` (BLEND_RGBA_MULT on a copy)."""
tinted = mask.copy()
color_surf = pygame.Surface(mask.get_size(), pygame.SRCALPHA)
color_surf.fill(color)
tinted.blit(color_surf, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
return tinted


class FootswitchWidget(Widget):
"""Footswitch indicator: a colored dot (the "LED") with a label below,
or a letter badge when unassigned. When the underlying footswitch is the
Expand Down
78 changes: 46 additions & 32 deletions uilib/glyphs/arc_ring.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,25 @@
free for a label.

The glyph renders to an SRCALPHA surface. Blit at (cx - half_size, cy -
half_size) to centre on (cx, cy). The radial/angular grids are built once
in __init__; only the per-t compositing runs in render().
half_size) to centre on (cx, cy). Geometry is precomputed in __init__ over the
ink band only; render() is cached.
"""

from __future__ import annotations

import math
from functools import lru_cache

import numpy as np
import pygame

from common.color import ColorRGB

_START_DEG = 210.0 # arc start — 7-o'clock
_SWEEP_DEG = 300.0 # total arc travel
_AA_DEG = 1.5 # angular AA falloff (≈1 px at r=35)

ColorRGB = tuple[int, int, int]
__all__ = ["ArcRingGlyph", "ColorRGB"]


class ArcRingGlyph:
Expand All @@ -61,20 +64,34 @@ def __init__(
self._size = size

xs = np.arange(size, dtype=float)
ys = np.arange(size, dtype=float)
X, Y = np.meshgrid(xs, ys)
self._X = X
self._Y = Y
X, Y = np.meshgrid(xs, xs)
dx = X - self._half
dy = Y - self._half
d = np.sqrt(dx ** 2 + dy ** 2)

self._ring_cov: np.ndarray = np.clip(
ring_half + 0.5 - np.abs(d - self._r), 0.0, 1.0
)
ring_cov = np.clip(ring_half + 0.5 - np.abs(d - self._r), 0.0, 1.0)
# Clockwise-from-top angle [0, 360), shifted so _START_DEG maps to 0
angle = np.degrees(np.arctan2(dx, -dy)) % 360.0
self._shifted: np.ndarray = (angle - _START_DEG) % 360.0
shifted = (angle - _START_DEG) % 360.0

# Keep only the annulus the ring stroke or tip dot can reach — a third of
# the grid — so render() works on a ~2k vector, not size². Pixels outside
# it are transparent under the full-grid formula anyway.
reach = max(ring_half, tip_radius) + 1.0
rows, cols = np.nonzero(np.abs(d - self._r) <= reach)
self._ring_cov: np.ndarray = ring_cov[rows, cols]
self._shifted: np.ndarray = shifted[rows, cols]
# Tip distance is derived in unflipped space, so keep unflipped coords.
self._px: np.ndarray = cols.astype(float)
self._py: np.ndarray = rows.astype(float)

# Row-major destination index, with flip_v folded in so the reflection is
# free at render time. Packing bytes for image.frombuffer beats scattering
# into a locked surfarray view by ~3x.
iy = (size - 1 - rows) if self._flip_v else rows
self._lin: np.ndarray = iy.astype(np.intp) * size + cols.astype(np.intp)
# Never cleared: the index set is fixed and every render rewrites all of it.
self._rgba: np.ndarray = np.zeros((size * size, 4), dtype=np.uint8)

@property
def half_size(self) -> int:
Expand All @@ -101,14 +118,21 @@ def tip_center(self, t: float) -> tuple[float, float]:
y = self._half + cos if self._flip_v else self._half - cos
return (x, y)

# Keyed on the glyph too, so it pins self — fine, glyphs outlive their panel.
# A dial repaints far more often than its value changes (any sibling cell going
# dirty repaints the whole column), so most calls re-ask for the same t.
@lru_cache(maxsize=64)
def render(
self,
t: float,
filled_color: ColorRGB,
empty_color: ColorRGB,
tip_color: ColorRGB,
) -> pygame.Surface:
"""Return an SRCALPHA surface with the arc drawn in two colours + tip dot."""
"""Arc in two colours + tip dot, on an SRCALPHA surface.

The surface is shared — blit it, never mutate it.
"""
t = max(0.0, min(1.0, t))
sweep = t * _SWEEP_DEG
aa = _AA_DEG
Expand All @@ -128,20 +152,13 @@ def render(
* np.clip((_SWEEP_DEG - shifted) / aa + 0.5, 0.0, 1.0)
)

# Tip dot at the current value position on the ring
tip_deg = _START_DEG + sweep
tip_rad = math.radians(tip_deg)
# Tip dot at the value position; unflipped, per _lin.
tip_rad = math.radians(_START_DEG + sweep)
tip_cx = self._half + self._r * math.sin(tip_rad)
tip_cy = self._half - self._r * math.cos(tip_rad)
tip_d = np.sqrt((self._X - tip_cx) ** 2 + (self._Y - tip_cy) ** 2)
tip_d = np.sqrt((self._px - tip_cx) ** 2 + (self._py - tip_cy) ** 2)
tip_cov = np.clip(self._tip_radius + 0.5 - tip_d, 0.0, 1.0)

if self._flip_v:
# True vertical reflection of the final image: gap moves to the top.
filled_cov = np.flipud(filled_cov)
empty_cov = np.flipud(empty_cov)
tip_cov = np.flipud(tip_cov)

fr, fg_c, fb = filled_color
er, eg, eb = empty_color
tr, tg, tb = tip_color
Expand All @@ -151,13 +168,10 @@ def render(
B = np.clip(filled_cov * fb + empty_cov * eb + tip_cov * tb, 0, 255).astype(np.uint8)
A = np.clip((filled_cov + empty_cov + tip_cov) * 255, 0, 255).astype(np.uint8)

surf = pygame.Surface((self._size, self._size), pygame.SRCALPHA)
pix = pygame.surfarray.pixels3d(surf)
pix[:, :, 0] = R.T
pix[:, :, 1] = G.T
pix[:, :, 2] = B.T
del pix
pa = pygame.surfarray.pixels_alpha(surf)
pa[:] = A.T
del pa
return surf
lin = self._lin
rgba = self._rgba
rgba[lin, 0] = R
rgba[lin, 1] = G
rgba[lin, 2] = B
rgba[lin, 3] = A
return pygame.image.frombuffer(rgba.tobytes(), (self._size, self._size), "RGBA")
Loading
Loading