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
5 changes: 4 additions & 1 deletion modalapi/modhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,11 @@ def preset_change(self, index):
logging.error("Bad Rest request: %s" % url)
self.current.preset_index = index

# Update name on lcd
# Update name on lcd, and relight any footswitch mapped to a snapshot.
self.lcd.draw_title()
for fs in self.hardware.footswitches:
if fs.preset_callback_arg is not None:
self.lcd.update_footswitch(fs)
# Bypass/param changes from the snapshot arrive via the WS drain (source of truth).

def preset_incr_and_change(self, *argv):
Expand Down
12 changes: 7 additions & 5 deletions pistomp/footswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def __init__(self, id, led_pin, pixel, midi_CC, midi_channel, midiout, refresh_c
def get_display_label(self):
if self.taptempo and self.taptempo.is_enabled():
return str(round(self.taptempo.get_bpm()))
elif self.midi_CC is None:
elif self.midi_CC is None and self.preset_callback_arg is None:
return ""
else:
return self.display_label
Expand All @@ -149,10 +149,10 @@ def drives_display(self) -> bool:

def set_value(self, bypass_value: float):
self.toggled = (bypass_value < 1)
self._set_led(self.toggled)
self.set_led(self.toggled)
self.refresh_callback(footswitch=self)

def _set_led(self, enabled):
def set_led(self, enabled):
if self.led is not None:
if self.taptempo:
tempo = self.taptempo.get_bpm()
Expand Down Expand Up @@ -228,7 +228,7 @@ def pressed(self, state):
r.enable()
else:
r.disable()
self._set_led(self.toggled)
self.set_led(self.toggled)
self.refresh_callback(True) # True means this is a bypass change only
else:
# TODO consider case where relay and longpress are specified
Expand Down Expand Up @@ -257,7 +257,7 @@ def pressed(self, state):
logging.debug("Sending CC event: %d" % self.midi_CC)
self.midiout.send_message(cc)
if self.drives_display:
self._set_led(self.toggled)
self.set_led(self.toggled)

if self.drives_display:
self.refresh_callback(footswitch=self)
Expand All @@ -282,4 +282,6 @@ def clear_pedalboard_info(self):
self.display_label = None
self.set_category(None)
self.preset_callback = None
self.preset_callback_arg = None
self.parameter = None
self.clear_relays()
14 changes: 9 additions & 5 deletions pistomp/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ def __init_footswitches_default(self):
fs.clear_relays()
self.__init_footswitches(self.cfg)

def __clear_footswitch_midi_cc(self, fs) -> None:
fs.set_midi_CC(None)
for k, v in self.controllers.items():
if v == fs:
self.controllers.pop(k)
break

def __init_footswitches(self, cfg):
if cfg is None or (Token.HARDWARE not in cfg) or (Token.FOOTSWITCHES not in cfg[Token.HARDWARE]):
return
Expand Down Expand Up @@ -380,11 +387,7 @@ def __init_footswitches(self, cfg):
if Token.MIDI_CC in f:
cc = f[Token.MIDI_CC]
if cc == Token.NONE:
fs.set_midi_CC(None)
for k, v in self.controllers.items():
if v == fs:
self.controllers.pop(k)
break
self.__clear_footswitch_midi_cc(fs)
else:
fs.set_midi_channel(self.midi_channel)
fs.set_midi_CC(cc)
Expand All @@ -393,6 +396,7 @@ def __init_footswitches(self, cfg):

# Preset Control
if Token.PRESET in f:
self.__clear_footswitch_midi_cc(fs)
preset_value = f[Token.PRESET]
if preset_value == Token.UP:
fs.add_preset(callback=self.handler.preset_incr_and_change)
Expand Down
33 changes: 28 additions & 5 deletions pistomp/lcd320x240.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,10 @@ def parameter_commit_enum(self, param_value_tuple):
# Footswitches
#
def footswitch_label(self, footswitch):
"""Label for a footswitch bound to a plugin param: the param name, or the plugin instance for a :bypass binding."""
"""Label for a footswitch bound to a snapshot index or a plugin param."""
if footswitch.preset_callback_arg is not None:
name = self.current.presets.get(footswitch.preset_callback_arg) if self.current else None
return self.shorten_name(name, self.footswitch_width) if name else str(footswitch.preset_callback_arg)
param = footswitch.parameter
if param is None:
return None
Expand All @@ -489,6 +492,10 @@ def footswitch_label(self, footswitch):
def draw_footswitch(self, plugin):
for c in plugin.controllers:
if isinstance(c, Footswitch):
if c.preset_callback_arg is not None:
# Preset-bound switches are drawn by draw_unbound_footswitches,
# regardless of any (stale) plugin binding.
continue
fs_id = c.id
#fss[fs_id] = None
label = self.footswitch_label(c)
Expand All @@ -509,20 +516,36 @@ def draw_unbound_footswitches(self):
if fs.id in self.footswitch_slots:
continue
slot = fs.id
dl = fs.get_display_label()
label = "" if dl is None else dl
if fs.preset_callback_arg is not None:
label = self.footswitch_label(fs)
fs.set_display_label(label)
active = self.current is not None and self.current.preset_index == fs.preset_callback_arg
fs.toggled = active
fs.set_led(active) # a press never touches toggled for preset switches
color = self.foreground
is_bypassed = not fs.toggled
else:
dl = fs.get_display_label()
label = "" if dl is None else dl
color = None
is_bypassed = True
y = 0
x = self.get_footswitch_pitch() * slot
p = FootswitchWidget(Box.xywh(x, y, self.plugin_width, self.footswitch_height), self.small_font,
label, None, True, parent=self.footswitch_panel, object=fs)
label, color, is_bypassed, parent=self.footswitch_panel, object=fs)
self.w_footswitches.append(p)
self.footswitch_panel.add_widget(p)
self.footswitch_panel.refresh()

def update_footswitch(self, footswitch):
for wfs in self.w_footswitches:
if wfs.object == footswitch:
if footswitch.parameter is not None:
if footswitch.preset_callback_arg is not None:
footswitch.set_display_label(self.footswitch_label(footswitch))
active = self.current is not None and self.current.preset_index == footswitch.preset_callback_arg
footswitch.toggled = active
footswitch.set_led(active)
elif footswitch.parameter is not None:
# Binding may be new (e.g. MIDI learn) — reflect label + color.
footswitch.set_display_label(self.footswitch_label(footswitch))
wfs.color = self.get_category_color(footswitch.category)
Expand Down
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.
18 changes: 18 additions & 0 deletions tests/test_footswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,21 @@ def test_clears_state(self):
assert fs.toggled is False
assert fs.display_label is None
assert fs.preset_callback is None

def test_clears_preset_callback_arg(self):
with _make_footswitch(midi_CC=None) as fs:
fs.add_preset(callback=MagicMock(), callback_arg=5)
fs.set_display_label("Lead")

fs.clear_pedalboard_info()

assert fs.preset_callback_arg is None
assert fs.get_display_label() == ""

def test_clears_parameter(self):
with _make_footswitch(midi_CC=None) as fs:
fs.parameter = MagicMock() # pyright: ignore[reportAttributeAccessIssue]

fs.clear_pedalboard_info()

assert fs.parameter is None
2 changes: 2 additions & 0 deletions tests/test_lcd320x240.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@


class MockObject:
preset_callback_arg = None # footswitch default; override via kwargs

def __init__(self, **kwargs):
self.__dict__.update(kwargs)

Expand Down
Loading