1. Forum
    1. Unresolved Threads
  • Login
  • Search
  • Kemper Amps Homepage
Everywhere
  • Everywhere
  • Pages
  • Forum
  • More Options
  1. Kemper Profiler Forum

tunetown Intermediate

  • Male
  • from Landshut/DE
  • Member since August 5, 2024
  • Last Activity: December 17, 2025 at 11:19 AM
Posts
419
Reactions Received
164
Points
2,264
Profile Hits
1,104
  • Posts
  • Threads
  • Wall
  • Recent Activity
  • Reactions
  • About Me
Hellem
October 28, 2025 at 12:12 PM

so?

tunetown
October 28, 2025 at 12:36 PM

Fck. Had a call from work. Now?

Hellem
October 28, 2025 at 12:40 PM

yesss!!!) join the meeeting

Hellem
October 28, 2025 at 12:42 PM
Meet
Real-time meetings by Google. Using your browser, share your video, desktop, and presentations with teammates and customers.
meet.google.com
Hellem
October 28, 2025 at 11:58 AM
Meet
Real-time meetings by Google. Using your browser, share your video, desktop, and presentations with teammates and customers.
meet.google.com
Hellem
October 28, 2025 at 11:57 AM

kmon)))

Hellem
October 28, 2025 at 11:29 AM

yo man

Hellem
October 28, 2025 at 10:36 AM

well, zoon is limited to time..

so here is a google meet link:

Meet
Real-time meetings by Google. Using your browser, share your video, desktop, and presentations with teammates and customers.
meet.google.com
Hellem
October 28, 2025 at 11:08 AM

it's time)

tunetown
October 28, 2025 at 10:00 AM

Okay lets propose 11:00, perhaps this time we get together ;)

Hellem
October 28, 2025 at 10:15 AM

Okay, I will at 11 and I am online now. Follow the zoom link below))


https://us05web.zoom.us/j/81523108391?pwd=WN6APFVfBSkig3KF2NSsLBIx3jeavb.1

Hellem
October 28, 2025 at 10:31 AM

speak something when you enter - so i will hear that you joined.

tunetown
October 28, 2025 at 9:58 AM

Im in :)

Hellem
October 28, 2025 at 9:40 AM

Will you join now? Or may be you can schedule another time?

Hellem
October 28, 2025 at 9:03 AM

https://us05web.zoom.us/j/81523108391?pwd=WN6APFVfBSkig3KF2NSsLBIx3jeavb.1


i am here))

Hellem
October 28, 2025 at 8:35 AM

Sure, any time. I am available right now - and all the day.

Lets try at 9am of your time. Like in 30 minutes.

tunetown
October 28, 2025 at 6:29 AM

Sorry had lots of calls, can we do today? Im available in about an hour until evening, please propose a time

Hellem
October 27, 2025 at 4:35 PM

still waiting

Hellem
October 27, 2025 at 3:24 PM

and still waiting))

Hellem
October 27, 2025 at 2:06 PM

join)))


https://us05web.zoom.us/j/85256918316?pwd=G9WfkM2DMfteyPkQaZyMWi5lpFAHl5.1

Hellem
October 27, 2025 at 1:35 PM

Hi, i am online. We can use a zoom/google call.

Hellem
October 26, 2025 at 3:33 PM

okay, i will be online, thank you!)

tunetown
October 26, 2025 at 1:54 PM

Hellem wow that config looks pretty nice! Cannot see anything that would be solved better otherwise.

Tomorrow after around 14:00 german time i have some time if you like to chat a bit

Hellem
October 26, 2025 at 2:06 AM

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....

tunetown
October 26, 2025 at 1:58 PM

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?

Hellem
October 26, 2025 at 2:58 AM

- 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).


- please post a zip

Midi Captain - PySwitch Example.zip

Here it is)))

tunetown
October 26, 2025 at 1:52 PM

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....

tunetown
October 25, 2025 at 6:44 PM

ps Oh shit, how should i read the code that way?? 🤣 please post a zip

Hellem
October 25, 2025 at 1:05 PM

Also found an issue:

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.

tunetown
October 25, 2025 at 6:42 PM

Sounds strange, i edited display.py lots of times in the emulator… how can i reproduce that?

tunetown
October 24, 2025 at 12:01 PM

Hellem wow! That looks awesome!

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....

Have a nice weekend!

Hellem
October 25, 2025 at 1:01 PM

Hey!

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.

Hellem
October 25, 2025 at 1:02 PM

Inputs.py

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)

width = int(disp.bounds.width) if hasattr(disp.bounds, "width") else 240
max_px = max(1, width - 1)
x = int(round(idx * (max_px) / 119.0))

# call the element’s setter
if hasattr(disp, "set"):
disp.set(x)
elif hasattr(disp, "set_position"):
disp.set_position(x)
elif hasattr(disp, "set_offset"):
disp.set_offset(x)

return (255, 255, 255)

def looper_color_callback(action, value):
v = int(value) if value is not None else 0
PALETTE = {
0: (0, 0, 0),
1: (255, 0, 0),
2: (0, 255, 0),
3: (225, 65, 0),
4: (255, 255, 0),
}
return PALETTE.get(v, (0,0,0))



# Mappings

# Playhead
ABLETON_PLAYHEAD = ClientParameterMapping.get(
name = "MAP_CC124_PLAYHEAD",
set = ControlChange(
124,
0,
channel = 6
),
response = ControlChange(
124,
0,
channel = 6
)
)
# BARS Count
BARS_IN = ClientParameterMapping.get(
name = "MAP_CC122_BARS",
set = ControlChange(
122,
0,
channel = 6
),
response = ControlChange(
122,
0,
channel = 6
)
)

# EXP1
ABLETON_EXP1 = ClientParameterMapping.get(
name = "MAP_ABLETON_EXP1",
set = ControlChange(
91,
0,
channel = 6
),
response = ControlChange(
7,
0,
channel = 6
)
)

# EXP2
ABLETON_EXP2 = ClientParameterMapping.get(
name = "MAP_ABLETON_EXP2",
set = ControlChange(
92,
0,
channel = 6
),
response = ControlChange(
1,
0,
channel = 6
)
)

ABLETON_1 = ClientParameterMapping.get(
name = "MAP_ABLETON_1",
set = ControlChange(
1,
0,
channel = 6
),
response = ControlChange(
1,
0,
channel = 6
)
)
ABLETON_2 = ClientParameterMapping.get(
name = "MAP_ABLETON_2",
set = ControlChange(
2,
0,
channel = 6
),
response = ControlChange(
2,
0,
channel = 6
)
)
ABLETON_3 = ClientParameterMapping.get(
name = "MAP_ABLETON_3",
set = ControlChange(
3,
0,
channel = 6
),
response = ControlChange(
3,
0,
channel = 6
)
)
ABLETON_4 = ClientParameterMapping.get(
name = "MAP_ABLETON_4",
set = ControlChange(
4,
0,
channel = 6
),
response = ControlChange(
4,
0,
channel = 6
)
)
ABLETON_A = ClientParameterMapping.get(
name = "MAP_ABLETON_A",
set = ControlChange(
5,
0,
channel = 6
),
response = ControlChange(
5,
0,
channel = 6
)
)
ABLETON_B = ClientParameterMapping.get(
name = "MAP_ABLETON_B",
set = ControlChange(
6,
0,
channel = 6
),
response = ControlChange(
6,
0,
channel = 6
)
)
ABLETON_C = ClientParameterMapping.get(
name = "MAP_ABLETON_C",
set = ControlChange(
7,
0,
channel = 6
),
response = ControlChange(
7,
0,
channel = 6
)
)
ABLETON_D = ClientParameterMapping.get(
name = "MAP_ABLETON_D",
set = ControlChange(
8,
0,
channel = 6
),
response = ControlChange(
8,
0,
channel = 6
)
)
ABLETON_DOWN = ClientParameterMapping.get(
name = "MAP_ABLETON_DOWN",
set = ControlChange(
126,
0,
channel = 6
),
response = ControlChange(
126,
0,
channel = 6
)
)
ABLETON_DOWN_1 = ClientParameterMapping.get(
name = "MAP_ABLETON_DOWN_1",
set = ControlChange(
125,
0,
channel = 6
),
response = ControlChange(
125,
0,
channel = 6
)
)


# Callbacks
looper_state_callback = BinaryParameterCallback(
mapping = ABLETON_DOWN,
color_callback = looper_color_callback,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
reference_value = 0,
use_internal_state = False
)
looper_state_callback_action = BinaryParameterCallback(
mapping = ABLETON_DOWN_1,
color_callback = looper_color_callback,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
reference_value = 0,
use_internal_state = False
)
looper_state_position = BinaryParameterCallback(
mapping = ABLETON_PLAYHEAD,
color_callback = playhead_pos_callback,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
reference_value = 0,
use_internal_state = False
)
bars_callback = BinaryParameterCallback(
mapping = BARS_IN,
color_callback = bars_value_callback,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
reference_value = 0,
use_internal_state = False
)


_accept = ENCODER_BUTTON()

_cancel = ENCODER_BUTTON()

_pager = PagerAction(
pages = [
{
"id": 1,
"color": (0,0,0),
"text": 'FXS',
           
},
{
"id": 2,
"color": (0,0,0),
"text": 'LOO',
           
},
       
],
use_leds = False,
display = DISPLAY_PAGE
)

Inputs = [
{
"assignment": PA_MIDICAPTAIN_10_WHEEL_ENCODER,
"actions": [
AMP_GAIN(
accept_action = _accept,
cancel_action = _cancel,
preview_display = DISPLAY_RIG_NAME,
step_width = 40
),
           
],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_1,
"pixels": (3),
"actions": [
CUSTOM_MESSAGE(
message = [181, 1, 127],
message_release = [181, 1, 0],
use_leds = False,
text = '',
id = 1,
enable_callback = _pager.enable_callback
),
PushButtonAction(
{
"display": DISPLAY_HEADER_1,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_1,
color = (20,0,255),
text = "OCT",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
                   
}
),
           
],
"actionsHold": [],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_2,
"actions": [
CUSTOM_MESSAGE(
message = [181, 2, 127],
message_release = [181, 2, 0],
use_leds = False,
text = '',
id = 1,
enable_callback = _pager.enable_callback
),
PushButtonAction(
{
"display": DISPLAY_HEADER_2,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_2,
color = (255,180,0),
text = "DRT",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
                   
}
),
           
],
"actionsHold": [],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_3,
"actions": [
CUSTOM_MESSAGE(
message = [181, 3, 127],
message_release = [181, 3, 0],
use_leds = False,
text = '',
id = 1,
enable_callback = _pager.enable_callback
),
PushButtonAction(
{
"display": DISPLAY_HEADER_3,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_3,
color = (255,0,180),
text = "PHS",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
                   
}
),
           
],
"actionsHold": [],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_4,
"actions": [
CUSTOM_MESSAGE(
message = [181, 4, 127],
message_release = [181, 4, 0],
use_leds = False,
text = '',
id = 1,
enable_callback = _pager.enable_callback
),
PushButtonAction(
{
"display": DISPLAY_HEADER_4,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_4,
color = (120,255,0),
text = "WAH",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
                   
}
),
           
],
"actionsHold": [],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_UP,
"actions": [
CUSTOM_MESSAGE(
message = [181, 0, 127],
message_release = [181, 0, 0],
color = (0, 0, 0),
text = ''
),
SHOW_TEMPO(
change_display = DISPLAY_RIG_NAME,
text = '{bpm} bpm',
id = None
),
           
],
"actionsHold": [
_pager,
           
],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_A,
"actionsHold": [],
"actions": [
CUSTOM_MESSAGE(
message = [181, 5, 127],
message_release = [181, 5, 0],
use_leds = False,
text = '',
id = 1,
enable_callback = _pager.enable_callback
),
PushButtonAction(
{
"display": DISPLAY_FOOTER_1,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_A,
color = (255,0,0),
text = "AMP",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
                   
}
),
           
],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_B,
"actions": [
CUSTOM_MESSAGE(
message = [181, 6, 127],
message_release = [181, 6, 0],
use_leds = False,
text = '',
id = 1,
enable_callback = _pager.enable_callback
),
PushButtonAction(
{
"display": DISPLAY_FOOTER_2,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_B,
color = (255,120,0),
text = "OVD",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
                   
}
),
           
],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_C,
"actions": [
CUSTOM_MESSAGE(
message = [181, 7, 127],
message_release = [181, 7, 0],
use_leds = False,
text = '',
id = 1,
enable_callback = _pager.enable_callback
),
PushButtonAction(
{
"display": DISPLAY_FOOTER_3,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_C,
color = (0,255,120),
text = "REV",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
                   
}
),
           
],
"actionsHold": [],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_D,
"actions": [
CUSTOM_MESSAGE(
message = [181, 8, 127],
message_release = [181, 8, 0],
use_leds = False,
text = '',
id = 1,
enable_callback = _pager.enable_callback
),
PushButtonAction(
{
"display": DISPLAY_FOOTER_4,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_D,
color = (120,0,255),
text = "DLY",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
                   
}
),
           
],
"actionsHold": [],
       
},
{
"assignment": PA_MIDICAPTAIN_10_SWITCH_DOWN,
"actions": [
CUSTOM_MESSAGE(
message = [181, 126, 127],
message_release = [181, 126, 0],
color = (0, 0, 0),
use_leds = False,
text = ''
),
PushButtonAction(
{
"display": DISPLAY_LOOPER_STATE,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": looper_state_callback,
                   
}
),
PushButtonAction(
{
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": looper_state_callback_action,
                   
}
),
PushButtonAction(
{
"useSwitchLeds": False,
"display": DISPLAY_PLAYHEAD,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": looper_state_position,
                   
}
),
PushButtonAction(
{
"useSwitchLeds": False,
"display": DISPLAY_BARS,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": bars_callback,
                   
}
),
           
],
"actionsHold": [],
       
},
{
"assignment": PA_MIDICAPTAIN_10_WHEEL_BUTTON,
"actions": [
_accept,
           
],
"actionsHold": [
_cancel,
           
],
       
},
{
"assignment": PA_MIDICAPTAIN_10_EXP_PEDAL_1,
"actions": [
AnalogAction(
mapping = ABLETON_EXP1,
auto_calibrate = False,
max_frame_rate = 60,
max_value = 127
),
           
],
       
},
{
"assignment": PA_MIDICAPTAIN_10_EXP_PEDAL_2,
"actions": [
AnalogAction(
mapping = ABLETON_EXP2,
auto_calibrate = False,
max_frame_rate = 60,
max_value = 127
),
           
],
       
},
   
]

Hellem
October 25, 2025 at 1:02 PM

display.py

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


_ACTION_LABEL_LAYOUT = {
"font": "/fonts/H20.pcf",
"backColor": DEFAULT_LABEL_COLOR,
"stroke": 1,
   
}

_DISPLAY_WIDTH = const(
240
)
_DISPLAY_HEIGHT = const(
240
)
_SLOT_WIDTH = const(
120
)
_SLOT_HEIGHT = const(
40
)
_FOOTER_Y = const(
200
)
_RIG_NAME_HEIGHT = const(
160
)

# Playhead
class Playhead(DisplayElement):
def __init__(self, bounds: DisplayBounds, bar_width=2, color=(255,255,255), id=0):
super().__init__(bounds=bounds, id=id)
self._bar = None
self._bar_w = int(bar_width)
self._color = color

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

self._baseline: Rect | None = None
self._ticks: list[Rect] = []
self._bars = 0

# --- lifecycle ---------------------------------------------------------

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)

# --- internal ----------------------------------------------------------

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

# Baseline geometry
x0 = self.bounds.x
w = self.bounds.width
half_tick = self._tick_w // 2

# 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,
           
]
)
)

Hellem
October 22, 2025 at 7:40 AM

[Blocked Image: https://imgur.com/a/mAzAhW9]

Ableton pySwitch built cupturing names, states, playhead position and loopbars count!!

Hellem
October 21, 2025 at 6:23 AM

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.

Hellem
October 18, 2025 at 6:09 AM

Hi! How can different pixels of a button be acessed separately by a callback? IS there a way to change colod of a led with a callback?

Hellem
October 18, 2025 at 4:44 AM

Waiting to hear from you!)

Hellem
October 17, 2025 at 6:42 AM

Have figured out how to set BinaryParameterCallback.

I was wrong at many points)))

In my case i need to use this in inputs.py, not in display.py:

# Mappings

ABLETON_1 = ClientParameterMapping.get(
name = "MAP_ABLETON_1",
set = ControlChange(1, 0),
response = ControlChange(1, 0)
)

Inputs = [

{
"assignment": PA_MIDICAPTAIN_10_SWITCH_1,
"actions": [
CUSTOM_MESSAGE(message = [181, 1, 127], message_release = [181, 1, 0], use_leds = False, text = ''),
PushButtonAction(
{
"display": DISPLAY_HEADER_1,
"useSwitchLeds": True,
"mode": PushButtonAction.NO_STATE_CHANGE,
"callback": BinaryParameterCallback(
mapping = ABLETON_1,
color = (20,0,255),
text = "OCT",
value_enable = 127,
value_disable = 0,
reference_value = 64,
comparison_mode = BinaryParameterCallback.GREATER_EQUAL,
use_internal_state = False
),
}
),
],
"actionsHold": [],
},

]

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 )))

Hellem
October 17, 2025 at 1:04 AM

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.

Examples for 2 byte description:

Display ON/OFF value 0 (DION0)

Display Bounds Full (DIBFX0Y160W240H10)

Display Bounds short X (DIX0)

Display Color RGB (DIR256G256B256)

Hellem
October 17, 2025 at 12:23 AM

i have tried to remove "protocol": KemperBidirectionalProtocol(

from communication.py - and the same issue still exist. Midi captain stucks with a sysex out:

MIDI IN [Midi Captain- MIDI]: F0 00 7C 7D 03 54 7E 00 45 3C 16 50 00 00 00 00 2A 1C 4C 16 1B 15 44 61 31 5A 64 02 43 35 5E 73 3A 08 0E 26 2B 0D 4A 6E 3A 08 0C 36 0B 31 58 20 36 18 2E 37 21 24 74 0D 05 08 04 04 33 25 58 65 10 08 45 76 63 25 44 2F 38 1E 2E 37 3B 25 68 63 34 0B 6E 07 13 3D 46 65 39 5C 65 67 03 64 44 2C 10 1B 0D 16 73 14 40 37 1A 0B 04 06 4B 38 40 3C 36 5B 6C 47 2B 31 4A 3E 06 42 44 02 02 19 52 6C 32 48 00 F7
MIDI IN [Midi Captain- MIDI]:

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))

Hellem
October 16, 2025 at 11:50 PM

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).

tunetown’s Followers 1

Profile Visitors 7

  1. Contact Us
  2. Imprint
  3. Privacy Policy
  4. Cookie Policy

The information provided on this site is subject to change without notice. KEMPER™, PROFILER™, PROFILE™, PROFILING™, PROFILER PowerHead™, PROFILER PowerRack™, PROFILER Stage™, PROFILER Player™, PROFILER Remote™, KEMPER Kabinet™, KEMPER Power Kabinet™, KEMPER Kone™, KEMPER Rig Exchange™, KEMPER Rig Manager™, PURE CABINET™, CabDriver™ and KEMPER Liquid Profile™ are trademarks of Kemper GmbH.
All other product names and company names used on this webpage are (registered) brand names or trademarks of each respective holders, and Kemper GmbH is not necesserily associated or affiliated with them. These product names are used solely for the purpose of identifying the specific products that were used during PROFILING™. All samples and demos may be downloaded but are restricted to personal use and non-profit.

All rights reserved. Copyright ©2025 Kemper GmbH