Skip to content
Draft
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
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