Also i have no any success with making a web ui to work with the controller via midi port. I have very large midi port list on my pc. I have granted port permissions, and the site starts to scan ports and just stucks. So i need to disable the permissions to make the site to start working properly. Sometimes it starts okay with permissions granted and kemper virtual port. So i can open port list on the web site. But i dont see captains port there at all....
Hmm, strange and not really debuggable from here remotely....it seems that the auto discovering stucks. Can you open the Dev COnsole (Chrome: F12, Safari: Alt-Cmd-i) and see if there are errors?
- Sounds strange, i edited display.py lots of times in the emulator… how can i reproduce that?
I am talking about custom made displays the ones like with a class declaration:
DISPLAY_BARS = BarsRuler( ...
First i enter the code to display.py
Next i press apply checkbox.
Then i dont see the display appeared anywhere (except the code).
To make such display appear in web UI there is a need of creation any new display and edit its code. Then it becomes visible on the web UI controller (and it actually works on the controller itself) - but not visible in Display web UI (as it is a custom created class).
Ah, ok. Now i know what you mean....thats not an issue, thats just not supported. The graphical Editor can only know the standard classes. If you write your own classes, its assumed you can also edit the display definition in code
However, the display itself, when it internally just uses the recatanges like PySwitch does, should at least show up in the emulated display correctly. However, the display emaulation itself is not accurate: It does not work on pixel level for perfromance reasons, so the fonst will alsways be different etc....
It is imporsible to add a display code without adding the display in web ui. So to add custom display code there is a need to add dummy display in UI and then edit its code. Otherwise web interface will gine an error.
Sorry again for not being available, but other projects (paid ones) have priority now, but if you still want to talk a bit, we can do next Week if you like, i am available at Mo-Do during the (german) day, but i would like to know in advance a bit....when do you have time? Where are you located?
It looks you got a lot of stuff right anyway, did you implement this without altering the project code (only input/display)? If you like, share your code and i can have a look over it in advance....
I am available any day. any time you want) Just schedule it so i wiill be online. Mo-Do is Monday, right?*
I have already implemented almost all the features i was in need. The hardset ones were at the beginning... Will be happy to discuss))) Yes i was changing only input/display files. Added a few functions.Will share the code.
from pyswitch.clients.kemper.actions.amp import AMP_GAIN from pyswitch.clients.kemper.actions.tempo import SHOW_TEMPO from pyswitch.clients.local.actions.encoder_button import ENCODER_BUTTON from pyswitch.clients.local.actions.custom import CUSTOM_MESSAGE from pyswitch.controller.actions.AnalogAction import AnalogAction from pyswitch.clients.local.actions.pager import PagerAction from pyswitch.controller.callbacks import BinaryParameterCallback from pyswitch.controller.actions import PushButtonAction from display import DISPLAY_PLAYHEAD from display import DISPLAY_BARS from display import DISPLAY_HEADER_1 from display import DISPLAY_HEADER_2 from display import DISPLAY_FOOTER_1 from display import DISPLAY_FOOTER_2 from display import DISPLAY_RIG_NAME from display import DISPLAY_HEADER_3 from display import DISPLAY_HEADER_4 from display import DISPLAY_FOOTER_3 from display import DISPLAY_FOOTER_4 from display import DISPLAY_PAGE from display import DISPLAY_LOOPER_STATE from pyswitch.controller.client import ClientParameterMapping from adafruit_midi.control_change import ControlChange from pyswitch.hardware.devices.pa_midicaptain_10 import *
# Functions def bars_value_callback(action, value): v = 0 if value is None else int(value) if v < 0: v = 0 if v > 127: v = 127 bars = int(round(v * 32.0 / 127.0)) # 0..32
disp = getattr(action, "label", None) # action stores "display" on .label if disp and hasattr(disp, "set_bars"): disp.set_bars(bars)
return (0,0,0) # LEDs unused for this action
def playhead_pos_callback(action, value): v = 0 if value is None else int(value) v = 0 if v < 0 else (127 if v > 127 else v)
# compress by skipping 15,31,47,63,79,95,111,127 skip = (v + 1) // 16 idx = v - skip # 0..119
# resolve display from the action (the "display" config is stored on .label) disp = getattr(action, "label", None) if not disp or not hasattr(disp, "bounds"): return (0,0,0)
from pyswitch.clients.kemper.callbacks.tempo_bpm import KemperTempoDisplayCallback from pyswitch.clients.kemper import TunerDisplayCallback from micropython import const from pyswitch.colors import Colors from pyswitch.colors import DEFAULT_LABEL_COLOR from pyswitch.ui.ui import DisplayElement from pyswitch.ui.ui import DisplayBounds from pyswitch.ui.elements import DisplayLabel from pyswitch.ui.elements import BidirectionalProtocolState from adafruit_display_shapes.rect import Rect
def init(self, ui, appl): super().init(ui, appl) # draw once so you can see it immediately self._bar = Rect( x=self.bounds.x, y=self.bounds.y, width=self._bar_w, height=self.bounds.height, fill=self._color, ) # put it on the screen; order here = z-depth (above backgrounds) ui.splash.append(self._bar)
def set(self, pos_px: int): if not self._bar: return x0 = self.bounds.x x1 = self.bounds.x + self.bounds.width - self._bar_w x = max(x0, min(x1, x0 + int(pos_px))) self._bar.x = x
# export as the name you’ll reference from inputs.py DISPLAY_PLAYHEAD = Playhead( DisplayBounds( x = 0, y = 60, w = 240, h = 90 ), bar_width = 2, color = (255,255,255) )
# Bar Ruler class BarsRuler(DisplayElement): """ Horizontal baseline + up to 31 vertical tick marks (2px wide by default). Call set_bars(n) with n in [0..32]. It will draw (n-1) ticks that divide the baseline into n equal bar slots. Ticks are vertically centered on the baseline. """ def __init__(self, bounds: DisplayBounds, base_h: int = 4, tick_w: int = 4, tick_h: int = 20, color=(0, 0, 0), id: int = 0): super().__init__(bounds=bounds, id=id) self._base_h = int(base_h) self._tick_w = int(tick_w) self._tick_h = int(tick_h) self._color = color
def init(self, ui, appl): """ Create and append the baseline + all (hidden) tick Rects once. """ super().init(ui, appl)
# Baseline: full width along the bottom of bounds bar_y = self.bounds.y + self.bounds.height - self._base_h self._baseline = Rect( x=self.bounds.x, y=bar_y, width=self.bounds.width, height=self._base_h, fill=self._color ) ui.splash.append(self._baseline)
# Pre-create 31 tick rectangles and park them offscreen (x = -1000) # Compute the centered Y for ticks so their midpoint aligns to baseline midpoint. tick_y = bar_y + (self._base_h // 2) - (self._tick_h // 2) for _ in range(31): t = Rect( x=-1000, # hidden until positioned y=tick_y, width=self._tick_w, height=self._tick_h, fill=self._color ) self._ticks.append(t) ui.splash.append(t)
# Apply any previously set value if self._bars: self._apply_layout(self._bars)
# --- public API --------------------------------------------------------
def set_bars(self, n: int): """ Update visual ruler to show n bars (0..32). Draws (n-1) ticks. """ n = max(0, min(32, int(n))) if n == self._bars: return self._bars = n if self._baseline is None or not self._ticks: return # not initialized yet self._apply_layout(n)
def _apply_layout(self, n: int): """ Position ticks evenly across the baseline for n bars. """ # Hide all ticks if n <= 1 k = max(0, n - 1) if k == 0: for t in self._ticks: t.x = -1000 return
# Place ticks at fractions i/n (i = 1..n-1) # round() ensures the last tick sits near the right edge. for i, t in enumerate(self._ticks, start=1): if i <= k: x = x0 + int(round(i * w / float(n))) - half_tick t.x = x else: t.x = -1000 # hide extra ticks # Export an instance for your layout (adjust bounds to your screen) DISPLAY_BARS = BarsRuler( DisplayBounds( x = 0, y = 75, w = 240, h = 30 ), base_h = 4, tick_w = 4, tick_h = 20, color = (0,0,0) )
DISPLAY_HEADER_1 = DisplayLabel( layout = _ACTION_LABEL_LAYOUT, bounds = DisplayBounds( x = 0, y = 160, w = 60, h = 40 ) )
DISPLAY_HEADER_2 = DisplayLabel( layout = _ACTION_LABEL_LAYOUT, bounds = DisplayBounds( x = 60, y = 160, w = 60, h = 40 ) )
DISPLAY_FOOTER_1 = DisplayLabel( layout = _ACTION_LABEL_LAYOUT, bounds = DisplayBounds( x = 0, y = 200, w = 60, h = 40 ) ) DISPLAY_FOOTER_2 = DisplayLabel( layout = _ACTION_LABEL_LAYOUT, bounds = DisplayBounds( x = 60, y = 200, w = 60, h = 40 ) )
DISPLAY_RIG_NAME = DisplayLabel( bounds = DisplayBounds( x = 0, y = 0, w = 90, h = 50 ), layout = { "font": "/fonts/H20.pcf", "lineSpacing": 0.8, "maxTextWidth": 220,
}, callback = KemperTempoDisplayCallback() )
DISPLAY_HEADER_3 = DisplayLabel( bounds = DisplayBounds( x = 120, y = 160, w = 60, h = 40 ), layout = { "font": "/fonts/H20.pcf", "backColor": DEFAULT_LABEL_COLOR, "stroke": 1,
} )
DISPLAY_HEADER_4 = DisplayLabel( bounds = DisplayBounds( x = 180, y = 160, w = 60, h = 40 ), layout = { "font": "/fonts/H20.pcf", "backColor": DEFAULT_LABEL_COLOR, "stroke": 1,
} )
DISPLAY_FOOTER_3 = DisplayLabel( bounds = DisplayBounds( x = 120, y = 200, w = 60, h = 40 ), layout = { "font": "/fonts/H20.pcf", "backColor": DEFAULT_LABEL_COLOR, "stroke": 1,
} )
DISPLAY_FOOTER_4 = DisplayLabel( bounds = DisplayBounds( x = 180, y = 200, w = 60, h = 40 ), layout = { "font": "/fonts/H20.pcf", "backColor": DEFAULT_LABEL_COLOR, "stroke": 1,
} )
DISPLAY_PAGE = DisplayLabel( bounds = DisplayBounds( x = 180, y = 0, w = 60, h = 50 ), layout = { "font": "/fonts/H20.pcf", "backColor": Colors.BLACK, "stroke": 1,
} )
DISPLAY_LABEL_1 = BidirectionalProtocolState( bounds = DisplayBounds( x = 230, y = 0, w = 8, h = 8 ) )
DISPLAY_PLAYHEAD = Playhead( DisplayBounds( x = 0, y = 60, w = 240, h = 90 ), bar_width = 2, color = (255,255,255) )
DISPLAY_LOOPER_STATE = DisplayLabel( bounds = DisplayBounds( x = 0, y = 80, w = 240, h = 50 ), layout = { "font": "/fonts/H20.pcf", "backColor": DEFAULT_LABEL_COLOR, "stroke": 1,
} )
Splashes = TunerDisplayCallback( splash_default = DisplayElement( bounds = DisplayBounds( x = 0, y = 0, w = _DISPLAY_WIDTH, h = _DISPLAY_HEIGHT ), children = [ DISPLAY_HEADER_2, DISPLAY_HEADER_3, DISPLAY_LOOPER_STATE, DISPLAY_HEADER_4, DISPLAY_FOOTER_1, DISPLAY_FOOTER_2, DISPLAY_FOOTER_3, DISPLAY_BARS, DISPLAY_FOOTER_4, DISPLAY_HEADER_1, DISPLAY_PLAYHEAD, DISPLAY_PAGE, DISPLAY_LABEL_1, DISPLAY_RIG_NAME,
Hi! All previous questions were solved. Would like to know how to create an ammount of rectangles that represents BEATS recorded to the looped. Like 1-16. And a moving playhead. Got ideas? Or maybe a clockwise circle witn ableton style.
This way a button would send out custom midi message, no internal callback will be used, callback will be triggered by incoming CC message. So all buttons are mapped and all corresponding displays works fine.
Next is to create a looper emulation on a screen - and i would love to have a conversation with you )))
Also i have developed my own sysex protocol for Ableton and touch osc where i can control lots of parameters by sysex. And as here i am to control many parameters and i am not sticked to Kempers protocol i would like to share the insights of the sysex communication protocol concept. The main idea is next:
It is okay to send long sysex messages on init and it is not comfortable to send long sysex message during the play. With this logic the communication protocol may not contain long sysex at all.
So basic sysex caps are F0 at the beginning and F7 at the end. And anything in between can be from 00 to 7F
So we can define a dict fot 1 or 2 first bytes so both parties will know what they are talking about.
Examples for 1 byte description:
Display ON/OFF value 0 (DO0) - 44 4F 00
Display ON/OFF value 1 (DO1) - 44 4F 01
Display Bounds Full (DBX0Y160W240H10) - 44 42 58 00 00 59 01 20 57 01 70 48 00 0A (bit split for all numbers as we have 240 value range)
Display Bounds short X (DX0) - 44 58 00 00
Display Color RGB (DR256G256B256) - 44 52 02 00 47 02 00 42 02 00 (bit split for all numbers as we have 256 value range)
And so on. As long as we have no repetative values for different parameters in first 1,2 bytes - it would work very nice. Compact and fast. But not really good for scaling as in case with huge parameters ammount we need to rebuild all the protocol and add 2 or more bytes for a parameter description.
I also have question about this protocol and how it is related to tempo requests - would be happy to discuss this topic)))) cuz i am not a kemper user and i dont realy know much about kemper protocols - but for tempo requests and for keep alive requests i have emulated a responces in Bome midi with timers and counters))
It would really be nice to have a call soon cuz it is a 3rd day i am (with chatgpt) fighting with whis task with no result for a display callbacks...
I will try to remove unnecessary pieces from communication .py thank you!
As chatgpt refubrished forum about this topic - it said that BinaryParameterCallback function wotks only wrapped in actions (and actions are always sticked to assignments, and assignments are sticked to buttons entities that's ammounts it really small and not enough to controll big ammount of displays...) - and that might be an issue, i am trying to build a new callback function - but midiCaptain stucks with same longSysex coming out... Cuz i need a callbacks to be sticked only to mapings (So inputs or actions will not affect the display behavior).
I have suceed with ParameterDisplayCallback - mappings ant text swap from a list works really fine with this callback!!
Also your Display realisation is truelly BRILLIANT!!!! It is supercooooooool!!!!))))))))))))
But i need a callback that (is not sticked to inputs and actions) can have a mapping and can control a display parameters (color, on/of with global dimm factors, position, bounds - cencerely all display parameters) cuz i would like to create a screen set that will emitate looper record window with waveform imitation and a playhead moving - and it will take lots of displays to be carefully controlled.
Once again my goal is to:
Create a pySwitch firmware for midiCaptain for Ableton that will work with Bome midi + Remotify.io scripts with a certain Guitar rack in Ableton. The main concept is to have no Kemper entities. Or may be to create Ableton entities. And creating entities for me is a really hard task, i just dont know how to do it (may be you can give me suggestions - i will be happy to acccomplish that task). So the buttons just simply sends out 0-127 cc and all LED's and Displays can be controlled with midiSysex for complex parameters changing (on init mostly) and a simple midi CC to turn on/off (during the play process).