diff --git a/common/token.py b/common/token.py index f9bd96e23..1e2b09d06 100755 --- a/common/token.py +++ b/common/token.py @@ -16,6 +16,7 @@ ACTION = 'action' ADC_INPUT = 'adc_input' ANALOG_CONTROLLERS = 'analog_controllers' +AUTOSYNC = 'autosync' BANK = 'bank' BUNDLE = 'bundle' BYPASS = 'bypass' diff --git a/modalapi/modhandler.py b/modalapi/modhandler.py index 2eb0429c6..e677724ab 100755 --- a/modalapi/modhandler.py +++ b/modalapi/modhandler.py @@ -348,6 +348,9 @@ def set_current_pedalboard(self, pedalboard): cfg = yaml.load(ymlfile, Loader=yaml.SafeLoader) self.hardware.reinit(cfg) + # Sync current state of analog controls (expression pedals, etc.) + self.hardware.sync_analog_controls() + # Initialize the data and draw on LCD self.bind_current_pedalboard() self.load_current_presets() diff --git a/pistomp/analogcontrol.py b/pistomp/analogcontrol.py index 19a4b972e..5632a084a 100755 --- a/pistomp/analogcontrol.py +++ b/pistomp/analogcontrol.py @@ -37,4 +37,9 @@ def readChannel(self): return data def refresh(self): + """Read current value from hardware and potentially take action.""" logging.error("AnalogControl subclass hasn't overriden the refresh method") + + def initialize(self): + """Called when the pedalboard has been loaded, e.g. to sync current value.""" + logging.error("AnalogControl subclass hasn't implemented initialize method") \ No newline at end of file diff --git a/pistomp/analogmidicontrol.py b/pistomp/analogmidicontrol.py index 9dab36b19..1fe7c04c9 100755 --- a/pistomp/analogmidicontrol.py +++ b/pistomp/analogmidicontrol.py @@ -12,6 +12,7 @@ # # You should have received a copy of the GNU General Public License # along with pi-stomp. If not, see . +from typing import override import adafruit_mcp3xxx.mcp3008 as MCP from adafruit_mcp3xxx.analog_in import AnalogIn @@ -26,18 +27,23 @@ import logging -class AnalogMidiControl(analogcontrol.AnalogControl): +def as_midi_value(adc_value: int): + """Convert a 10-bit ADC value (0-1023) to a MIDI value (0-127).""" + return util.renormalize(adc_value, 0, 1023, 0, 127) + - def __init__(self, spi, adc_channel, tolerance, midi_CC, midi_channel, midiout, type, id=None, cfg={}): +class AnalogMidiControl(analogcontrol.AnalogControl): + def __init__(self, spi, adc_channel, tolerance, midi_CC, midi_channel, midiout, type, id=None, cfg={}, autosync=False): super(AnalogMidiControl, self).__init__(spi, adc_channel, tolerance) self.midi_CC = midi_CC self.midiout = midiout self.midi_channel = midi_channel + self.autosync = autosync # Parent member overrides self.type = type self.id = id - self.last_read = 0 # this keeps track of the last potentiometer value + self.last_read = 0 # this keeps track of the last potentiometer value self.value = None self.cfg = cfg @@ -47,18 +53,33 @@ def set_midi_channel(self, midi_channel): def set_value(self, value): self.value = value - # Override of base class method + @override + def initialize(self): + if not self.autosync: + return + + # read the analog pin + value = self.readChannel() + set_volume = as_midi_value(value) + + cc = [self.midi_channel | CONTROL_CHANGE, self.midi_CC, set_volume] + logging.debug("AnalogControl force-sending CC event %s" % cc) + self.midiout.send_message(cc) + + # save the reading to prevent duplicate sends on next poll + self.last_read = value + + @override def refresh(self): # read the analog pin value = self.readChannel() # how much has it changed since the last read? pot_adjust = abs(value - self.last_read) - value_changed = (pot_adjust > self.tolerance) + value_changed = pot_adjust > self.tolerance if value_changed: - # convert 16bit adc0 (0-65535) trim pot read into 0-100 volume level - set_volume = util.renormalize(value, 0, 1023, 0, 127) + set_volume = as_midi_value(value) cc = [self.midi_channel | CONTROL_CHANGE, self.midi_CC, set_volume] logging.debug("AnalogControl Sending CC event %s" % cc) diff --git a/pistomp/analogswitch.py b/pistomp/analogswitch.py index 813062ae2..df9c97d95 100755 --- a/pistomp/analogswitch.py +++ b/pistomp/analogswitch.py @@ -12,27 +12,28 @@ # # You should have received a copy of the GNU General Public License # along with pi-stomp. If not, see . +from typing import override import time import pistomp.analogcontrol as analogcontrol import pistomp.switchstate as switchstate -import pistomp.taptempo as taptempo +from pistomp.taptempo import TapTempo LONG_PRESS_TIME = 0.5 # Hold seconds which defines a long press FALLING_THRESHOLD = 800 # ASSUMES 10-bit ADC, can be changed for debounce handling class AnalogSwitch(analogcontrol.AnalogControl): - def __init__(self, spi, adc_channel, tolerance, callback, taptempo=None): + def __init__(self, spi, adc_channel, tolerance, callback, taptempo: TapTempo | None = None): super(AnalogSwitch, self).__init__(spi, adc_channel, tolerance) #self.value = None # this keeps track of the last value, do we still need this? self.callback = callback self.state = switchstate.Value.RELEASED self.start_time = 0 self.duration = 0 - self.taptempo = taptempo + self.taptempo: TapTempo | None = taptempo - # Override of base class method + @override def refresh(self): # read the analog channel new_value = self.readChannel() @@ -57,3 +58,9 @@ def refresh(self): self.callback(switchstate.Value.RELEASED) elif self.state is switchstate.Value.LONGPRESSED: self.state = switchstate.Value.RELEASED + + @override + def initialize(self): + # no-op for stateless switches + pass + \ No newline at end of file diff --git a/pistomp/hardware.py b/pistomp/hardware.py index 27cb73eb5..bcc62a1d9 100755 --- a/pistomp/hardware.py +++ b/pistomp/hardware.py @@ -20,6 +20,7 @@ import common.token as Token import common.util as Util +from pistomp.analogcontrol import AnalogControl import pistomp.analogmidicontrol as AnalogMidiControl import pistomp.footswitch as Footswitch import pistomp.taptempo as taptempo @@ -46,7 +47,7 @@ def __init__(self, default_config, handler, midiout, refresh_callback): # Standard hardware objects (not required to exist) self.relay = None - self.analog_controls = [] + self.analog_controls: list[AnalogControl] = [] self.encoders = [] self.controllers = {} self.footswitches = [] @@ -104,13 +105,15 @@ def reinit(self, cfg): # reinit hardware as specified by the new cfg context (after pedalboard change, etc.) self.cfg = self.default_cfg.copy() - self.__init_midi_default() - # Global footswitch init (callbacks and groups) Footswitch.Footswitch.init(self.handler.callbacks) - # Footswitch configuration - self.__init_footswitches(self.cfg) + # Analog control configuration + for ac in self.analog_controls: + try: + ac.initialize() + except Exception as e: + logging.warning(f"Failed to initialize analog control {ac}: {e}") # Pedalboard specific config if cfg is not None: @@ -239,6 +242,7 @@ def create_analog_controls(self, cfg): midi_cc = Util.DICT_GET(c, Token.MIDI_CC) threshold = Util.DICT_GET(c, Token.THRESHOLD) control_type = Util.DICT_GET(c, Token.TYPE) + autosync = Util.DICT_GET(c, Token.AUTOSYNC) if adc_input is None: logging.error("Config file error. Analog control specified without %s" % Token.ADC_INPUT) @@ -248,9 +252,11 @@ def create_analog_controls(self, cfg): continue if threshold is None: threshold = 16 # Default, 1024 is full scale + if autosync is None: + autosync = False # Default to False control = AnalogMidiControl.AnalogMidiControl(self.spi, adc_input, threshold, midi_cc, midi_channel, - self.midiout, control_type, id, c) + self.midiout, control_type, id, c, autosync) self.analog_controls.append(control) key = format("%d:%d" % (midi_channel, midi_cc)) self.controllers[key] = control @@ -299,9 +305,6 @@ def get_real_midi_channel(self, cfg): pass return chan - def __init_midi_default(self): - self.__init_midi(self.cfg) - def __init_midi(self, cfg): self.midi_channel = self.get_real_midi_channel(cfg) # TODO could iterate thru all objects here instead of handling in __init_footswitches diff --git a/setup/config_templates/default_config.yml b/setup/config_templates/default_config.yml index 6f12f2d86..e0e836ef5 100755 --- a/setup/config_templates/default_config.yml +++ b/setup/config_templates/default_config.yml @@ -54,12 +54,14 @@ hardware: # id: The id and position on the screen (starting with 0 on the left) # type: The control type, used to represent the control on the screen (optional) # midi_CC: The MIDI CC message to be sent when the control is adjusted (optional) + # autosync: Whether to send current value on pedalboard load (optional, default: false) # #analog_controllers: # - adc_input: 5 # id: 0 # type: EXPRESSION # midi_CC: 75 + # autosync: true # encoders: # Each encoder definition is a list which starts with the id diff --git a/setup/config_templates/default_config_3fs_2knob.yml b/setup/config_templates/default_config_3fs_2knob.yml index 1e58c23b7..daf6c7421 100755 --- a/setup/config_templates/default_config_3fs_2knob.yml +++ b/setup/config_templates/default_config_3fs_2knob.yml @@ -48,12 +48,14 @@ hardware: # id: The id and position on the screen (starting with 0 on the left) # type: The control type, used to represent the control on the screen (optional) # midi_CC: The MIDI CC message to be sent when the control is adjusted (optional) + # autosync: Whether to send current value on pedalboard load (optional, default: false) # analog_controllers: #- adc_input: 7 # id: 0 # midi_CC: 77 # type: EXPRESSION + # autosync: true - adc_input: 0 id: 1 midi_CC: 70 diff --git a/setup/config_templates/default_config_3fs_2knob_exp.yml b/setup/config_templates/default_config_3fs_2knob_exp.yml index c7d6ddd2f..e6c95ad8a 100755 --- a/setup/config_templates/default_config_3fs_2knob_exp.yml +++ b/setup/config_templates/default_config_3fs_2knob_exp.yml @@ -48,12 +48,14 @@ hardware: # id: The id and position on the screen (starting with 0 on the left) # type: The control type, used to represent the control on the screen (optional) # midi_CC: The MIDI CC message to be sent when the control is adjusted (optional) + # autosync: Whether to send current value on pedalboard load (optional, default: false) # analog_controllers: - adc_input: 7 id: 0 midi_CC: 77 type: EXPRESSION + autosync: true - adc_input: 0 id: 1 midi_CC: 70 diff --git a/setup/config_templates/default_config_pistompcore.yml b/setup/config_templates/default_config_pistompcore.yml index 16eafcc02..fba746d71 100755 --- a/setup/config_templates/default_config_pistompcore.yml +++ b/setup/config_templates/default_config_pistompcore.yml @@ -48,12 +48,14 @@ hardware: # id: The id and position on the screen (starting with 0 on the left) # type: The control type, used to represent the control on the screen (optional) # midi_CC: The MIDI CC message to be sent when the control is adjusted (optional) + # autosync: Whether to send current value on pedalboard load (optional, default: false) # # analog_controllers: # - adc_input: 7 # id: 0 # midi_CC: 77 # type: EXPRESSION +# autosync: true # - adc_input: 0 # id: 1 # midi_CC: 70 diff --git a/setup/config_templates/default_config_pistomptre.yml b/setup/config_templates/default_config_pistomptre.yml index 6f12f2d86..e0e836ef5 100644 --- a/setup/config_templates/default_config_pistomptre.yml +++ b/setup/config_templates/default_config_pistomptre.yml @@ -54,12 +54,14 @@ hardware: # id: The id and position on the screen (starting with 0 on the left) # type: The control type, used to represent the control on the screen (optional) # midi_CC: The MIDI CC message to be sent when the control is adjusted (optional) + # autosync: Whether to send current value on pedalboard load (optional, default: false) # #analog_controllers: # - adc_input: 5 # id: 0 # type: EXPRESSION # midi_CC: 75 + # autosync: true # encoders: # Each encoder definition is a list which starts with the id