Skip to content
12 changes: 11 additions & 1 deletion common/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
TTL_SCALEPOINTS = 'scalePoints'
TTL_TAPTEMPO = 'tapTempo'
TTL_TOGGLED = 'toggled'
TTL_TRIGGER = 'trigger'

class Type(Enum):
DEFAULT = 0 # No explicitly defined type (eg. linear float)
Expand All @@ -34,6 +35,7 @@ class Type(Enum):
LOGARITHMIC = 3
TAPTEMPO = 4
TOGGLED = 5
TRIGGER = 6 # pprops:trigger — edge-triggered, self-clearing (momentary)

class Parameter:

Expand All @@ -57,7 +59,9 @@ def __init__(self, plugin_info, value: float, binding, instance_id=None):

properties = util.DICT_GET(plugin_info, TTL_PROPERTIES)
if properties is not None and len(properties) > 0:
if TTL_ENUMERATION in properties:
if TTL_TRIGGER in properties:
self.type = Type.TRIGGER
elif TTL_ENUMERATION in properties:
self.enum_values = util.DICT_GET(plugin_info, TTL_SCALEPOINTS)
self.type = Type.ENUMERATION
elif TTL_INTEGER in properties:
Expand All @@ -69,6 +73,12 @@ def __init__(self, plugin_info, value: float, binding, instance_id=None):
elif TTL_TOGGLED in properties:
self.type = Type.TOGGLED

@property
def is_momentary(self) -> bool:
"""True for edge-triggered, self-clearing ports (pprops:trigger) —
these need a one-shot 127 press rather than an absolute 127/0 toggle."""
return self.type == Type.TRIGGER

def get_enum_value_list(self):
ret = []
for v in self.enum_values:
Expand Down
54 changes: 54 additions & 0 deletions modalapi/led_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# This file is part of pi-stomp.
#
# pi-stomp is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pi-stomp is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pi-stomp. If not, see <https://www.gnu.org/licenses/>.

"""Generic, data-driven footswitch-LED rendering.

Pure function of (LedSpec, plugin.output_values) -> (color, style). No
footswitch, beat, or plugin-instance coupling — the per-tick brightness
envelope (pulse phase, downbeat emphasis) is applied uniformly by the
handler's single LED-writing helper, not here.
"""

from __future__ import annotations

from enum import Enum, auto
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from modalapi.plugin_customization import LedSpec


class LedDisplayStyle(Enum):
SOLID = auto()
METRONOME = auto()


def render_led_spec(
spec: LedSpec, output_values: dict[str, float]
) -> tuple[tuple[int, int, int] | None, LedDisplayStyle]:
state = int(output_values.get(spec.state_symbol, 0))
if state in spec.off_states:
return None, LedDisplayStyle.SOLID
base = spec.colors.get(state)
if base is None:
return None, LedDisplayStyle.SOLID
if spec.downbeat_symbol is not None and int(output_values.get(spec.downbeat_symbol, -1)) == 0:
base = (
min(255, base[0] + spec.downbeat_tint),
min(255, base[1] + spec.downbeat_tint),
min(255, base[2] + spec.downbeat_tint),
)
style = LedDisplayStyle.METRONOME if (spec.pulse and state not in spec.steady_states) else LedDisplayStyle.SOLID
return base, style
45 changes: 31 additions & 14 deletions modalapi/mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
import modalapi.external_midi as ExternalMidi
from pistomp.encoder_controller import EncoderController
from pistomp.input.event import AnalogEvent, ControllerEvent, EncoderEvent, SwitchEvent, SwitchEventKind
from rtmidi.midiconstants import CONTROL_CHANGE
from modalapi.ethernet import EthernetManager
from modalapi.jack_mute import JackMute
from modalapi.led_render import render_led_spec
from pistomp.category import get_category_color
from typing import Optional

from blend.snapshot import SnapshotManager
Expand Down Expand Up @@ -261,19 +262,6 @@ def _handle_switch_v1(self, event: SwitchEvent) -> bool:
return self._handle_footswitch(c, event.kind, event.timestamp)
return False

def _emit_midi(self, controller, midi_value: int) -> None:
if controller.midi_CC is None:
return
cc = [controller.midi_channel | CONTROL_CHANGE, controller.midi_CC, int(midi_value)]
port_name = self.hardware.external_port_name(controller)
if port_name is not None and self.external_midi is not None:
try:
if self.external_midi.send_raw(port_name, cc):
return
except Exception as e:
logging.warning("External CC send failed on %s: %s", port_name, e)
self.hardware.midiout.send_message(cc)

def add_lcd(self, lcd):
self.lcd = lcd

Expand Down Expand Up @@ -533,6 +521,35 @@ def poll_controls(self):
if self.universal_encoder_mode is not UniversalEncoderMode.LOADING:
self.hardware.poll_controls()
self._tick_chords()
self._drive_footswitch_leds()

def _drive_footswitch_leds(self) -> None:
"""v1 has no beat_grid and no pixel (mono LCD) — just render each
footswitch's GPIO on/off state from its bound plugin's LedSpec (if
any) or the default toggle/category behavior. No metronome style."""
if self.hardware is None:
return
for fs in self.hardware.footswitches:
plugin = self._bound_plugin(fs)
if plugin is not None and plugin.customization.led_spec is not None:
color, _style = render_led_spec(plugin.customization.led_spec, plugin.output_values)
elif fs.toggled:
color = get_category_color(fs.category) if fs.category is not None else (255, 255, 255)
else:
color = None
if fs.led is not None:
if color is None:
fs.led.off()
else:
fs.led.on()

def _bound_plugin(self, fs):
if fs.parameter is None or self._current is None:
return None
for plugin in self.current.pedalboard.plugins:
if plugin.instance_id == fs.parameter.instance_id:
return plugin
return None

def poll_wifi(self):
self.wifi_manager.poll()
Expand Down
Loading
Loading