Skip to content
Open
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
1 change: 1 addition & 0 deletions common/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ACTION = 'action'
ADC_INPUT = 'adc_input'
ANALOG_CONTROLLERS = 'analog_controllers'
AUTOSYNC = 'autosync'
BANK = 'bank'
BUNDLE = 'bundle'
BYPASS = 'bypass'
Expand Down
3 changes: 3 additions & 0 deletions modalapi/modhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions pistomp/analogcontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
35 changes: 28 additions & 7 deletions pistomp/analogmidicontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#
# You should have received a copy of the GNU General Public License
# along with pi-stomp. If not, see <https://www.gnu.org/licenses/>.
from typing import override

import adafruit_mcp3xxx.mcp3008 as MCP
from adafruit_mcp3xxx.analog_in import AnalogIn
Expand All @@ -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

Expand All @@ -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)
Expand Down
15 changes: 11 additions & 4 deletions pistomp/analogswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,28 @@
#
# You should have received a copy of the GNU General Public License
# along with pi-stomp. If not, see <https://www.gnu.org/licenses/>.
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()
Expand All @@ -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

21 changes: 12 additions & 9 deletions pistomp/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = []
Expand Down Expand Up @@ -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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just does the same as the "pedalboard-specific config" below.


# Global footswitch init (callbacks and groups)
Footswitch.Footswitch.init(self.handler.callbacks)

# Footswitch configuration
self.__init_footswitches(self.cfg)
Comment on lines -112 to -113
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this; already done below.

# 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:
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions setup/config_templates/default_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ hardware:
# id: <integer> The id and position on the screen (starting with 0 on the left)
# type: <KNOB | EXPRESSION> The control type, used to represent the control on the screen (optional)
# midi_CC: <integer> The MIDI CC message to be sent when the control is adjusted (optional)
# autosync: <true | false> 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
Expand Down
2 changes: 2 additions & 0 deletions setup/config_templates/default_config_3fs_2knob.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ hardware:
# id: <integer> The id and position on the screen (starting with 0 on the left)
# type: <KNOB | EXPRESSION> The control type, used to represent the control on the screen (optional)
# midi_CC: <integer> The MIDI CC message to be sent when the control is adjusted (optional)
# autosync: <true | false> 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
Expand Down
2 changes: 2 additions & 0 deletions setup/config_templates/default_config_3fs_2knob_exp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ hardware:
# id: <integer> The id and position on the screen (starting with 0 on the left)
# type: <KNOB | EXPRESSION> The control type, used to represent the control on the screen (optional)
# midi_CC: <integer> The MIDI CC message to be sent when the control is adjusted (optional)
# autosync: <true | false> 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
Expand Down
2 changes: 2 additions & 0 deletions setup/config_templates/default_config_pistompcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ hardware:
# id: <integer> The id and position on the screen (starting with 0 on the left)
# type: <KNOB | EXPRESSION> The control type, used to represent the control on the screen (optional)
# midi_CC: <integer> The MIDI CC message to be sent when the control is adjusted (optional)
# autosync: <true | false> 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
Expand Down
2 changes: 2 additions & 0 deletions setup/config_templates/default_config_pistomptre.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ hardware:
# id: <integer> The id and position on the screen (starting with 0 on the left)
# type: <KNOB | EXPRESSION> The control type, used to represent the control on the screen (optional)
# midi_CC: <integer> The MIDI CC message to be sent when the control is adjusted (optional)
# autosync: <true | false> 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
Expand Down