├── .gitignore ├── LICENSE ├── README.md ├── common ├── token.py └── util.py ├── fonts ├── EtBt6001-JO47.ttf └── EtBt6001_license.txt ├── images ├── .DS_Store ├── eq_blue.png ├── eq_gray.png ├── power_gray.png ├── power_green.png ├── power_silver.png ├── wifi_gray.png ├── wifi_orange.png ├── wifi_silver.png └── wrench_silver.png ├── modalapi ├── mod.py ├── parameter.py ├── pedalboard.py ├── pi-stomp.png ├── plugin.py └── wifi.py ├── modalapistomp.py ├── pistomp ├── analogcontrol.py ├── analogmidicontrol.py ├── analogswitch.py ├── audiocard.py ├── audiocardfactory.py ├── audioinjector.py ├── category.py ├── config.py ├── controller.py ├── encoder.py ├── encoderswitch.py ├── footswitch.py ├── generichost.py ├── gpioswitch.py ├── handler.py ├── hardware.py ├── hardwarefactory.py ├── hifiberry.py ├── iqaudiocodec.py ├── lcd.py ├── lcd128x64.py ├── lcd135x240.py ├── lcdbase.py ├── lcdcolor.py ├── lcdgfx.py ├── lcdili9341.py ├── lcdsy7789.py ├── ledstrip.py ├── pistomp.py ├── pistompcore.py ├── relay.py ├── relaynonlatching.py ├── testhost.py └── tool.py ├── setup.sh ├── setup ├── .install_packages.sh.swp ├── audio │ ├── audiocard-setup.sh │ ├── audioinjector.state │ ├── hifiberry.state │ ├── iqaudiocodec.state │ └── rclocal.diff ├── config_templates │ ├── default_config.yml │ ├── default_config_3fs_2knob.yml │ ├── default_config_3fs_2knob_exp.yml │ ├── default_config_pistomp.yml │ └── default_config_pistompcore.yml ├── mod-tweaks │ ├── advertise.diff │ ├── host.diff │ ├── index.diff │ ├── mod-tweaks.sh │ ├── session.diff │ ├── start_touchosc2midi.sh │ └── webserver.diff ├── mod │ ├── 80 │ ├── browsepy.service │ ├── install.sh │ ├── jack.service │ ├── jackdrc │ ├── mod-amidithru.service │ ├── mod-host.service │ ├── mod-midi-merger-broadcaster.service │ ├── mod-midi-merger.service │ ├── mod-touchosc2midi.service │ └── mod-ui.service ├── pedalboards │ └── get_pedalboards.sh ├── pi-stomp-tweaks │ └── modify_version.sh ├── pkgs │ ├── gfxhat_install.sh │ ├── lilv_install.sh │ ├── mod-ttymidi_install.sh │ └── simple_install.sh ├── plugins │ ├── build_extra_plugins.sh │ └── get_plugins.sh ├── services │ ├── create_services.sh │ ├── hotspot │ │ ├── etc │ │ │ ├── default │ │ │ │ └── hostapd.pistomp │ │ │ ├── dnsmasq.d │ │ │ │ └── wifi-hotspot.conf │ │ │ └── hostapd │ │ │ │ └── hostapd.conf │ │ └── usr │ │ │ └── lib │ │ │ ├── pistomp-wifi │ │ │ ├── disable_wifi_hotspot.sh │ │ │ └── enable_wifi_hotspot.sh │ │ │ └── systemd │ │ │ └── system │ │ │ └── wifi-hotspot.service │ ├── mod-ala-pi-stomp.service │ ├── stop_services.sh │ ├── ttymidi.service │ ├── usbmount.deb │ ├── wifi_check.sh │ └── wlan0.conf └── sys │ ├── bash_aliases │ ├── config_tweaks.sh │ ├── linux-image-5.15.65-rt49-v8+_5.15.65-rt49-v8+-2_arm64.deb │ └── rtkernel.sh └── util ├── change-audio-card.sh ├── monitor_din_midi.py └── relay_toggle.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pi-Stomp! 2 | #### pi-Stomp is a DIY high definition, multi-effects stompbox platform for guitar bass and keyboards 3 | For more info about what it is and what it can do, go to [treefallsound.com](https://treefallsound.com) 4 | 5 | ## pi-Stomp Software and Firmware 6 | We start with a 64-bit Raspberry Pi lite operating system. We then add MOD, which is an open source audio host & UI 7 | created by the awesome folk at moddevices.com 8 | 9 | The pi-Stomp hardware requires drivers to interface with the LCD, potentiometers, encoders, footswitches, MIDI, etc. 10 | 11 | A pi-Stomp software service, mod-ala-pi-stomp, uses the drivers to monitor all input devices, to drive the LCD 12 | and to, among other things, send commands to mod-host for reading/writing pedalboard configuration information. 13 | 14 | This repository includes: 15 | * the pi-Stomp hardware drivers ('pistomp' module) 16 | * the mod-ala-pi-stomp service ('modalapistomp.py' & 'modalapi' module) 17 | * setup scripts for downloading/installing the above along with: 18 | * python dependencies 19 | * MOD software 20 | * sound card drivers 21 | * system tweaks 22 | * hundreds of LV2 plugins 23 | * sample pedalboards 24 | 25 | ## Installing 26 | For full installation instructions including etching the initial operating system, see [this guide](https://www.treefallsound.com/wiki/doku.php?id=software_installation_64-bit) 27 | 28 | After first boot, establish an ssh session to the RPi (the password is the one set during OS install): 29 | 30 | ssh pistomp@pistomp.local 31 | 32 | Once connected, download the pi-Stomp software: 33 | 34 | sudo rpi-update 35 | 36 | sudo apt update --fix-missing && sudo apt install -y git 37 | 38 | git clone https://github.com/TreeFallSound/pi-stomp.git 39 | 40 | cd pi-stomp 41 | 42 | Now run the setup utility to install the software and audio plugins. It could take over a half hour. 43 | There are a few setup options based on your system hardware. 44 | Typical systems should run: 45 | 46 | nohup ./setup.sh > setup.log | tail -f setup.log 47 | 48 | The IQAudio Codec Zero is the default audio card, so the above command is equivalent to adding `-a iqaudio-codec` 49 | (eg: ./setup.sh -a iqaudio-codec). 50 | For an audioInjector card, add: `-a 51 | audioinjector-wm8731-audio` For HiFiBerry add: `-a hifiberry-dacplusadc` 52 | For the original v1.x hardware, add `-v 1.0` 53 | 54 | If all went well, the system will reboot, then finally display the default pedalboard 55 | -------------------------------------------------------------------------------- /common/token.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | ACTION = 'action' 17 | ADC_INPUT = 'adc_input' 18 | ANALOG_CONTROLLERS = 'analog_controllers' 19 | BUNDLE = 'bundle' 20 | BYPASS = 'bypass' 21 | CATEGORY = 'category' 22 | CHANNEL = 'channel' 23 | COLON_BYPASS = ':bypass' 24 | COLOR = 'color' 25 | CONTROL = 'control' 26 | DEBOUNCE_INPUT = 'debounce_input' 27 | DISABLE = 'disable' 28 | DOWN = 'DOWN' 29 | EXPRESSION = 'EXPRESSION' 30 | FOOTSWITCHES = 'footswitches' 31 | GPIO_INPUT = 'gpio_input' 32 | GPIO_OUTPUT = 'gpio_output' 33 | HARDWARE = 'hardware' 34 | ID = 'id' 35 | INPUT = 'input' 36 | KNOB = 'KNOB' 37 | LEDSTRIP_POSITION = 'ledstrip_position' 38 | LEFT = 'LEFT' 39 | LEFT_RIGHT = 'LEFT_RIGHT' 40 | MAXIMUM = 'maximum' 41 | MIDI = 'midi' 42 | MIDI_CC = 'midi_CC' 43 | MINIMUM = 'minimum' 44 | NAME = 'name' 45 | NONE = 'None' 46 | PARAMETER = 'parameter' 47 | PORTS = 'ports' 48 | PRESET = 'preset' 49 | RANGES = 'ranges' 50 | RIGHT = 'RIGHT' 51 | SHORTNAME = 'shortName' 52 | SYMBOL = 'symbol' 53 | THRESHOLD = 'threshold' 54 | TITLE = 'title' 55 | TYPE = 'type' 56 | UP = 'UP' 57 | VERSION = 'version' 58 | -------------------------------------------------------------------------------- /common/util.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | 17 | def LILV_FOREACH(collection, func): 18 | itr = collection.begin() 19 | while itr: 20 | yield func(collection.get(itr)) 21 | itr.next() 22 | if itr.is_end(): 23 | break 24 | 25 | 26 | def DICT_GET(dict, key): 27 | if key in dict: 28 | return dict[key] 29 | else: 30 | return None 31 | 32 | 33 | def renormalize(n, left_min, left_max, right_min, right_max): 34 | # this remaps a value from original (left) range to new (right) range 35 | # Figure out how 'wide' each range is 36 | delta1 = left_max - left_min 37 | delta2 = right_max - right_min 38 | return round((delta2 * (n - left_min) / delta1) + right_min) 39 | 40 | 41 | def renormalize_float(value, left_min, left_max, right_min, right_max): 42 | # this remaps a value from original (left) range to new (right) range 43 | # Figure out how 'wide' each range is 44 | left_span = abs(left_max - left_min) 45 | num_divisions = left_span / value 46 | 47 | right_span = abs(right_max - right_min) 48 | 49 | return round(right_span / num_divisions, 2) 50 | 51 | 52 | def format_float(value): 53 | if value < 10: 54 | if value < 1: 55 | return "%.2f" % value 56 | else: 57 | return "%.1f" % value 58 | else: 59 | return "%d" % value 60 | -------------------------------------------------------------------------------- /fonts/EtBt6001-JO47.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/fonts/EtBt6001-JO47.ttf -------------------------------------------------------------------------------- /fonts/EtBt6001_license.txt: -------------------------------------------------------------------------------- 1 | license: Freeware 2 | link: https://www.fontspace.com/et-bt6001-font-f9581 -------------------------------------------------------------------------------- /images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/.DS_Store -------------------------------------------------------------------------------- /images/eq_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/eq_blue.png -------------------------------------------------------------------------------- /images/eq_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/eq_gray.png -------------------------------------------------------------------------------- /images/power_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/power_gray.png -------------------------------------------------------------------------------- /images/power_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/power_green.png -------------------------------------------------------------------------------- /images/power_silver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/power_silver.png -------------------------------------------------------------------------------- /images/wifi_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/wifi_gray.png -------------------------------------------------------------------------------- /images/wifi_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/wifi_orange.png -------------------------------------------------------------------------------- /images/wifi_silver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/wifi_silver.png -------------------------------------------------------------------------------- /images/wrench_silver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/images/wrench_silver.png -------------------------------------------------------------------------------- /modalapi/parameter.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import json 17 | import common.token as Token 18 | import common.util as util 19 | 20 | 21 | class Parameter: 22 | 23 | def __init__(self, plugin_info, value, binding): 24 | self.name = util.DICT_GET(plugin_info, Token.SHORTNAME) # possibly use name if shortName is None 25 | if self.name is None: 26 | self.name = util.DICT_GET(plugin_info, Token.NAME) 27 | self.symbol = util.DICT_GET(plugin_info, Token.SYMBOL) 28 | self.minimum = util.DICT_GET(util.DICT_GET(plugin_info, Token.RANGES), Token.MINIMUM) 29 | self.maximum = util.DICT_GET(util.DICT_GET(plugin_info, Token.RANGES), Token.MAXIMUM) 30 | self.value = value 31 | self.binding = binding 32 | 33 | def to_json(self): 34 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) 35 | 36 | 37 | -------------------------------------------------------------------------------- /modalapi/pedalboard.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import json 17 | import lilv 18 | import logging 19 | import operator 20 | import os 21 | import requests as req 22 | import sys 23 | import urllib.parse 24 | 25 | import common.token as Token 26 | import common.util as util 27 | import modalapi.parameter as Parameter 28 | import modalapi.plugin as Plugin 29 | 30 | class Pedalboard: 31 | 32 | def __init__(self, title, bundle): 33 | self.root_uri = "http://localhost:80/" 34 | self.title = title 35 | self.bundle = bundle # TODO used? 36 | self.plugins = [] 37 | 38 | self.world = lilv.World() 39 | 40 | # this is needed when loading specific bundles instead of load_all 41 | # (these functions are not exposed via World yet) 42 | self.world.load_specifications() 43 | self.world.load_plugin_classes() 44 | 45 | self.uri_block = self.world.new_uri("http://drobilla.net/ns/ingen#block") 46 | self.uri_head = self.world.new_uri("http://drobilla.net/ns/ingen#head") 47 | self.uri_port = self.world.new_uri("http://lv2plug.in/ns/lv2core#port") 48 | self.uri_tail = self.world.new_uri("http://drobilla.net/ns/ingen#tail") 49 | self.uri_value = self.world.new_uri("http://drobilla.net/ns/ingen#value") 50 | 51 | def get_pedalboard_plugin(self, world, bundlepath): 52 | # lilv wants the last character as the separator 53 | bundle = os.path.abspath(bundlepath) 54 | if not bundle.endswith(os.sep): 55 | bundle += os.sep 56 | # convert bundle string into a lilv node 57 | bundlenode = self.world.new_file_uri(None, bundle) 58 | 59 | # load the bundle 60 | self.world.load_bundle(bundlenode) 61 | 62 | # free bundlenode, no longer needed 63 | #self.world.node_free(bundlenode) # TODO find out why this is no longer necessary (why did API method go away) 64 | 65 | # get all plugins in the bundle 66 | ps = self.world.get_all_plugins() 67 | 68 | # make sure the bundle includes 1 and only 1 plugin (the pedalboard) 69 | if len(ps) != 1: 70 | raise Exception('get_pedalboard_info(%s) - bundle has 0 or > 1 plugin'.format(bundle)) 71 | 72 | # no indexing in python-lilv yet, just get the first item 73 | plugin = None 74 | for p in ps: 75 | plugin = p 76 | break 77 | 78 | if plugin is None: 79 | raise Exception('get_pedalboard_plugin(%s)'.format(bundle)) 80 | 81 | return plugin 82 | 83 | def get_plugin_data(self, uri): 84 | url = self.root_uri + "effect/get?uri=" + urllib.parse.quote(uri) 85 | try: 86 | resp = req.get(url, headers={'Cache-Control': 'no-cache', 'Pragma': 'no-cache'}) 87 | except: # TODO 88 | logging.error("Cannot connect to mod-host.") 89 | sys.exit() 90 | 91 | if resp.status_code != 200: 92 | logging.error("mod-host not able to get plugin data: %s\nStatus: %s" % (url, resp.status_code)) 93 | return {} 94 | #sys.exit() 95 | 96 | return json.loads(resp.text) 97 | 98 | def chase_tail(self, block, conn): 99 | if block is None: 100 | return 101 | conn.append(block) 102 | 103 | ports = self.world.find_nodes(block, self.uri_port, None) 104 | for port in ports: 105 | tail = self.world.get(None, self.uri_tail, port) 106 | if tail is None: 107 | continue 108 | head = self.world.get(tail, self.uri_head, None) 109 | if head is not None: 110 | block = self.world.get(None, self.uri_port, head) 111 | if block is not None and block not in conn: 112 | self.chase_tail(block, conn) 113 | break 114 | return conn 115 | 116 | # Get info from an lv2 bundle 117 | # @a bundle is a string, consisting of a directory in the filesystem (absolute pathname). 118 | def load_bundle(self, bundlepath, plugin_dict): 119 | # Load the bundle, return the single plugin for the pedalboard 120 | plugin = self.get_pedalboard_plugin(self.world, bundlepath) 121 | 122 | # check if the plugin is a pedalboard 123 | def fill_in_type(node): 124 | if node is not None and node.is_uri(): 125 | return node 126 | return None 127 | 128 | u = self.world.new_uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type") 129 | plugin_types = [i for i in util.LILV_FOREACH(plugin.get_value(u), fill_in_type)] 130 | if "http://moddevices.com/ns/modpedal#Pedalboard" not in plugin_types: 131 | raise Exception('get_pedalboard_info(%s) - plugin has no mod:Pedalboard type'.format(bundlepath)) 132 | 133 | # Walk ports starting from capture1 to determine general plugin order 134 | # TODO can this be generalized to use the chase_tail function? 135 | plugin_order = [] 136 | ports = plugin.get_value(self.uri_port) 137 | for port in ports: 138 | if port is None: 139 | continue 140 | tail = self.world.get(None, self.uri_tail, port) # TODO could end up being capture2 141 | if tail is None: 142 | continue 143 | head = self.world.get(tail, self.uri_head, None) 144 | if head is not None: 145 | block = self.world.get(None, self.uri_port, head) 146 | if block is not None: 147 | self.chase_tail(block, plugin_order) 148 | break 149 | 150 | # Iterate blocks (plugins) 151 | plugins_unordered = {} 152 | plugins_extra = [] 153 | blocks = plugin.get_value(self.uri_block) 154 | for block in blocks: 155 | if block is None or block.is_blank(): 156 | continue 157 | 158 | # Add plugin data (from plugin registry) to global plugin dictionary 159 | plugin_info = {} 160 | category = None 161 | prototype = self.world.find_nodes(block, self.world.ns.lv2.prototype, None) 162 | if len(prototype) > 0: 163 | #logging.debug("prototype %s" % prototype[0]) 164 | plugin_uri = str(prototype[0]) # plugin.get_uri() 165 | if plugin_uri not in plugin_dict: 166 | plugin_info = self.get_plugin_data(plugin_uri) 167 | if plugin_info: 168 | logging.debug("added %s" % plugin_uri) 169 | plugin_dict[plugin_uri] = plugin_info 170 | else: 171 | plugin_info = plugin_dict[plugin_uri] 172 | if plugin_info is not None: 173 | cat = util.DICT_GET(plugin_info, Token.CATEGORY) 174 | if cat is not None and len(cat) > 0: 175 | category = cat[0] 176 | 177 | # Extract Parameter data 178 | instance_id = str(block.get_path()).replace(bundlepath, "", 1) 179 | nodes = self.world.find_nodes(block, self.world.ns.lv2.port, None) 180 | parameters = {} 181 | if len(nodes) > 0: 182 | # These are the port nodes used to define parameter controls 183 | for port in nodes: 184 | param_value = self.world.get(port, self.uri_value, None) 185 | #logging.debug("port: %s value: %s" % (port, param_value)) 186 | binding = self.world.get(port, self.world.ns.midi.binding, None) 187 | if binding is not None: 188 | controller_num = self.world.get(binding, self.world.ns.midi.controllerNumber, None) 189 | channel = self.world.get(binding, self.world.ns.midi.channel, None) 190 | if (controller_num is not None) and (channel is not None): 191 | binding = "%d:%d" % (self.world.new_int(channel), self.world.new_int(controller_num)) 192 | logging.debug(" MIDI CC binding %s" % binding) 193 | path = str(port) 194 | symbol = os.path.basename(path) 195 | value = None 196 | if param_value is not None: 197 | if param_value.is_float(): 198 | value = float(self.world.new_float(param_value)) 199 | elif param_value.is_int(): 200 | value = int(self.world.new_int(param_value)) 201 | else: 202 | value = str(value) 203 | # Bypass "parameter" is a special case without an entry in the plugin definition 204 | if symbol == Token.COLON_BYPASS: 205 | info = {"shortName": "bypass", "symbol": symbol, "ranges": {"minimum": 0, "maximum": 1}} # TODO tokenize 206 | v = False if value is 0 else True 207 | param = Parameter.Parameter(info, v, binding) 208 | parameters[symbol] = param 209 | continue # don't try to find matching symbol in plugin_dict 210 | # Try to find a matching symbol in plugin_dict to obtain the remaining param details 211 | try: 212 | plugin_params = plugin_info[Token.PORTS][Token.CONTROL][Token.INPUT] 213 | except KeyError: 214 | logging.warning("plugin port info not found, could be missing LV2 for: %s", instance_id) 215 | continue 216 | for pp in plugin_params: 217 | sym = util.DICT_GET(pp, Token.SYMBOL) 218 | if sym == symbol: 219 | #logging.debug("PARAM: %s %s %s" % (util.DICT_GET(pp, 'name'), info[uri], category)) 220 | param = Parameter.Parameter(pp, value, binding) 221 | #logging.debug("Param: %s %s %4.2f %4.2f %s" % (param.name, param.symbol, param.minimum, value, binding)) 222 | parameters[symbol] = param 223 | 224 | #logging.debug(" Label: %s" % label) 225 | inst = Plugin.Plugin(instance_id, parameters, plugin_info, category) 226 | 227 | try: 228 | index = plugin_order.index(block) 229 | plugins_unordered[index] = inst 230 | except: 231 | plugins_extra.append(inst) 232 | #logging.debug("dump: %s" % inst.to_json()) 233 | 234 | # Add "extra" plugins (those not part of the tail_chase order) to the plugins_unordered dict 235 | max_index = len(plugins_unordered) 236 | for e in plugins_extra: 237 | plugins_unordered[max_index] = e 238 | max_index = max_index + 1 239 | 240 | # Sort the dictionary based on their order index and add to the pedalboard.plugin list 241 | # TODO improve the creation (tail chasing, sorting, dict>list conversion) 242 | if max_index > 0: 243 | sorted_dict = dict(sorted(plugins_unordered.items(), key=operator.itemgetter(0))) 244 | for i in range(0, len(sorted_dict)): 245 | val = sorted_dict.get(i) 246 | if val is not None: 247 | self.plugins.append(val) 248 | 249 | # Done obtaining relevant lilv for the pedalboard 250 | return 251 | 252 | def to_json(self): 253 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) 254 | -------------------------------------------------------------------------------- /modalapi/pi-stomp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/modalapi/pi-stomp.png -------------------------------------------------------------------------------- /modalapi/plugin.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import json 17 | from pistomp.footswitch import Footswitch 18 | 19 | 20 | class Plugin: 21 | 22 | def __init__(self, instance_id, parameters, info, category=None): 23 | 24 | self.instance_id = instance_id 25 | self.parameters = parameters 26 | self.bypass_indicator_xy = ((0,0), (0,0)) 27 | self.lcd_xyz = None 28 | self.controllers = [] 29 | self.has_footswitch = False 30 | self.category = category 31 | #self.info_dict = info # TODO could store this but not sure we need to 32 | 33 | def is_bypassed(self): 34 | param = self.parameters.get(":bypass") # TODO tokenize 35 | if param is not None: 36 | return param.value 37 | return True 38 | 39 | def toggle_bypass(self): 40 | param = self.parameters.get(":bypass") 41 | if param is None: 42 | return 0 43 | if param is not None: 44 | param.value = not param.value 45 | return param.value # return the new value 46 | 47 | def set_bypass(self, bypass): 48 | param = self.parameters.get(":bypass") 49 | param.value = 1.0 if bypass else 0.0 50 | if self.has_footswitch: 51 | for c in self.controllers: 52 | if isinstance(c, Footswitch): 53 | c.set_value(param.value) 54 | 55 | def to_json(self): 56 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) 57 | 58 | 59 | -------------------------------------------------------------------------------- /modalapi/wifi.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | # 16 | # Parts of this file borrowed from patchbox-cli 17 | # 18 | # Copyright (C) 2017 Vilniaus Blokas UAB, https://blokas.io/pisound 19 | 20 | import os 21 | import threading 22 | import subprocess 23 | import logging 24 | 25 | class WifiManager(): 26 | 27 | # For now hard wire wifi interface to avoid spending time scrubbing sysfs 28 | # 29 | # our hotspot scripts are also hard wired to this name. Long run we could make 30 | # it a config option or similar... or better plumb the whole thing with a 31 | # proper network management, but we aren't there. Alternatively, we could 32 | # monitor for hotplug events via dbus... 33 | # 34 | def __init__(self, ifname = 'wlan0'): 35 | # Grab default wifi interface 36 | self.iface_name = 'wlan0' 37 | self.lock = threading.Lock() 38 | self.last_status = {} 39 | self.changed = False 40 | self.stop = threading.Event() 41 | self.wireless_supported = False 42 | self.wireless_file = os.path.join(os.sep, 'sys', 'class', 'net', self.iface_name, 'wireless') 43 | self.operstate_file = os.path.join(os.sep, 'sys', 'class', 'net', self.iface_name, 'operstate') 44 | self.thread = threading.Thread(target=self._polling_thread, daemon=True).start() 45 | 46 | def __del__(self): 47 | logging.info("Wifi monitor cleanup") 48 | self.stop.set() 49 | self.thread.join() 50 | 51 | def _is_wifi_supported(self): 52 | # Once we know it's supported, no need to check the file again 53 | if self.wireless_supported: 54 | return True 55 | self.wireless_supported = os.path.exists(self.wireless_file) 56 | return self.wireless_supported 57 | 58 | def _is_wifi_connected(self): 59 | try: 60 | with open(self.operstate_file) as f: 61 | line = f.readline() 62 | f.close() 63 | return line.startswith('up') 64 | except Exception as e: 65 | return False 66 | 67 | def _is_hotspot_active(self): 68 | try: 69 | subprocess.check_output(['systemctl', 'is-active', 'wifi-hotspot', '--quiet']).strip().decode('utf-8') 70 | except: 71 | return False 72 | return True 73 | 74 | def _get_wpa_status(self, status): 75 | try: 76 | text_out = subprocess.check_output(['wpa_cli', '-i', self.iface_name, 'status']).strip().decode('utf-8') 77 | for i in text_out.split('\n'): 78 | if len(i) is 0: 79 | continue 80 | (key, value) = i.split('=') 81 | if key and value: 82 | status[key] = value 83 | except Exception as e: 84 | logging.error("WPA CLI fail:" + str(e)) 85 | 86 | def _polling_thread(self): 87 | while not self.stop.wait(5.0): 88 | new_status = {} 89 | new_status['wifi_supported'] = supported = self._is_wifi_supported() 90 | new_status['wifi_connected'] = connected = self._is_wifi_connected() 91 | new_status['hotspot_active'] = hp_active = self._is_hotspot_active() 92 | if supported and (connected or hp_active): 93 | self._get_wpa_status(new_status) 94 | if new_status != self.last_status: 95 | logging.debug("Wifi status changed:" + str(new_status)) 96 | self.lock.acquire() 97 | self.last_status = new_status 98 | self.changed = True 99 | self.lock.release() 100 | 101 | # External API 102 | def poll(self): 103 | if self.changed: 104 | logging.debug("wifi poll changed detect !") 105 | # We don't need to do a deep copy because that dictionnary content 106 | # is never modified by the Timer thread (the whole dictionnary is 107 | # replaced) 108 | # 109 | # Note: Use context manager to use a non-blocking lock safely vs. ctrl-C 110 | with self.lock: 111 | update = self.last_status 112 | self.changed = False 113 | return update 114 | return None 115 | 116 | def enable_hotspot(self): 117 | try: 118 | subprocess.check_output(['sudo', 'systemctl', 'enable', 'wifi-hotspot']).strip().decode('utf-8') 119 | subprocess.check_output(['sudo', 'systemctl', 'start', 'wifi-hotspot']).strip().decode('utf-8') 120 | except: 121 | logging.debug('Wifi hotspot enabling failed') 122 | 123 | def disable_hotspot(self): 124 | try: 125 | subprocess.check_output(['sudo', 'systemctl', 'stop', 'wifi-hotspot']).strip().decode('utf-8') 126 | subprocess.check_output(['sudo', 'systemctl', 'disable', 'wifi-hotspot']).strip().decode('utf-8') 127 | except: 128 | logging.debug('Wifi hotspot disabling failed') 129 | -------------------------------------------------------------------------------- /modalapistomp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | import argparse 18 | import logging 19 | import os 20 | import RPi.GPIO as GPIO 21 | import sys 22 | import time 23 | 24 | from rtmidi.midiutil import open_midioutput 25 | 26 | import modalapi.mod as Mod 27 | import pistomp.audiocardfactory as Audiocardfactory 28 | import pistomp.generichost as Generichost 29 | import pistomp.testhost as Testhost 30 | import pistomp.hardwarefactory as Hardwarefactory 31 | import pistomp.handler as Handler 32 | 33 | 34 | def main(): 35 | sys.settrace 36 | 37 | # Command line parsing 38 | parser = argparse.ArgumentParser() 39 | parser.add_argument("--log", "-l", nargs='+', help="Provide logging level. Example --log debug'", default="info", 40 | choices=['debug', 'info', 'warning', 'error', 'critical']) 41 | parser.add_argument("--host", nargs='+', help="Plugin host to use. Example --host mod'", default=['mod'], 42 | choices=['mod', 'generic', 'test']) 43 | 44 | args = parser.parse_args() 45 | 46 | # Handle Log Level 47 | level_config = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, 48 | 'critical': logging.CRITICAL} 49 | log = args.log[0] 50 | log_level = level_config[log] if log in level_config else None 51 | if log_level: 52 | print("Log level now set to: %s" % logging.getLevelName(log_level)) 53 | logging.basicConfig(level=log_level) 54 | 55 | # Current Working Dir 56 | cwd = os.path.dirname(os.path.realpath(__file__)) 57 | 58 | # Audio Card Config - doing this early so audio passes ASAP 59 | factory = Audiocardfactory.Audiocardfactory(cwd) 60 | audiocard = factory.create() 61 | audiocard.restore() 62 | 63 | # MIDI initialization 64 | # Prompts user for MIDI input port, unless a valid port number or name 65 | # is given as the first argument on the command line. 66 | # API backend defaults to ALSA on Linux. 67 | # TODO discover and use the thru port (seems to be 14:0 on my system) 68 | # shouldn't need to aconnect, just send msgs directly to the thru port 69 | port = 0 # TODO get this (the Midi Through port) programmatically 70 | #port = sys.argv[1] if len(sys.argv) > 1 else None 71 | try: 72 | midiout, port_name = open_midioutput(port) 73 | except (EOFError, KeyboardInterrupt): 74 | sys.exit() 75 | 76 | # Hardware and handler objects 77 | hw = None 78 | handler = None 79 | 80 | if args.host[0] == 'mod': 81 | 82 | # Create singleton Mod handler 83 | handler = Mod.Mod(audiocard, cwd) 84 | 85 | # Initialize hardware (Footswitches, Encoders, Analog inputs, etc.) 86 | factory = Hardwarefactory.Hardwarefactory() 87 | hw = factory.create(handler, midiout) 88 | handler.add_hardware(hw) 89 | 90 | # Load all pedalboard info from the lilv ttl file 91 | handler.load_pedalboards() 92 | 93 | # Load the current pedalboard as "current" 94 | current_pedal_board_bundle = handler.get_current_pedalboard_bundle_path() 95 | if not current_pedal_board_bundle: 96 | # Apparently, no pedalboard is currently loaded so just change to the default 97 | handler.pedalboard_change() 98 | else: 99 | handler.set_current_pedalboard(handler.pedalboards[current_pedal_board_bundle]) 100 | 101 | # Load system info. This can take a few seconds 102 | handler.system_info_load() 103 | 104 | elif args.host[0] == 'generic': 105 | # No specific plugin host specified, so use a generic handler 106 | # Encoders and LCD not mapped without specific purpose 107 | # Just initialize the control hardware (footswitches, analog controls, etc.) for use as MIDI controls 108 | handler = Generichost.Generichost(homedir=cwd) 109 | factory = Hardwarefactory.Hardwarefactory() 110 | hw = factory.create(handler, midiout) 111 | handler.add_hardware(hw) 112 | 113 | elif args.host[0] == 'test': 114 | handler = Testhost.Testhost(audiocard, homedir=cwd) 115 | try: 116 | factory = Hardwarefactory.Hardwarefactory() 117 | hw = factory.create(handler, midiout) 118 | handler.add_hardware(hw) 119 | except: 120 | handler.cleanup() 121 | raise 122 | 123 | logging.info("Entering main loop. Press Control-C to exit.") 124 | period = 0 125 | try: 126 | while True: 127 | handler.poll_controls() 128 | time.sleep(0.01) # lower to increase responsiveness, but can cause conflict with LCD if too low 129 | 130 | # For less frequent events 131 | period += 1 132 | if period > 100: 133 | handler.poll_modui_changes() 134 | period = 0 135 | 136 | except KeyboardInterrupt: 137 | logging.info('keyboard interrupt') 138 | finally: 139 | handler.cleanup() 140 | logging.info("Exit.") 141 | midiout.close_port() 142 | if handler.lcd is not None: 143 | handler.lcd.cleanup() 144 | GPIO.cleanup() 145 | del handler 146 | logging.info("Completed cleanup") 147 | 148 | 149 | if __name__ == '__main__': 150 | main() 151 | -------------------------------------------------------------------------------- /pistomp/analogcontrol.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import busio 17 | import digitalio 18 | import board 19 | import adafruit_mcp3xxx.mcp3008 as MCP 20 | import logging 21 | from adafruit_mcp3xxx.analog_in import AnalogIn 22 | 23 | 24 | class AnalogControl: 25 | 26 | def __init__(self, spi, adc_channel, tolerance): 27 | 28 | self.spi = spi 29 | self.adc_channel = adc_channel 30 | self.last_read = 0 # this keeps track of the last potentiometer value 31 | self.tolerance = tolerance # to keep from being jittery we'll only change the 32 | # value when the control has moved a significant amount 33 | 34 | def readChannel(self): 35 | adc = self.spi.xfer2([1, (8 + self.adc_channel) << 4, 0]) 36 | data = ((adc[1] & 3) << 8) + adc[2] 37 | return data 38 | 39 | def refresh(self): 40 | logging.error("AnalogControl subclass hasn't overriden the refresh method") 41 | -------------------------------------------------------------------------------- /pistomp/analogmidicontrol.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import adafruit_mcp3xxx.mcp3008 as MCP 17 | from adafruit_mcp3xxx.analog_in import AnalogIn 18 | 19 | from rtmidi.midiutil import open_midioutput 20 | from rtmidi.midiconstants import CONTROL_CHANGE 21 | 22 | import common.util as util 23 | import json 24 | import pistomp.analogcontrol as analogcontrol 25 | 26 | import logging 27 | 28 | 29 | class AnalogMidiControl(analogcontrol.AnalogControl): 30 | 31 | def __init__(self, spi, adc_channel, tolerance, midi_CC, midi_channel, midiout, type, cfg={}): 32 | super(AnalogMidiControl, self).__init__(spi, adc_channel, tolerance) 33 | self.midi_CC = midi_CC 34 | self.midiout = midiout 35 | self.midi_channel = midi_channel 36 | 37 | # Parent member overrides 38 | self.type = type 39 | self.last_read = 0 # this keeps track of the last potentiometer value 40 | self.value = None 41 | self.cfg = cfg 42 | 43 | def set_midi_channel(self, midi_channel): 44 | self.midi_channel = midi_channel 45 | 46 | def set_value(self, value): 47 | self.value = value 48 | 49 | # Override of base class method 50 | def refresh(self): 51 | # read the analog pin 52 | value = self.readChannel() 53 | 54 | # how much has it changed since the last read? 55 | pot_adjust = abs(value - self.last_read) 56 | value_changed = (pot_adjust > self.tolerance) 57 | 58 | if value_changed: 59 | # convert 16bit adc0 (0-65535) trim pot read into 0-100 volume level 60 | set_volume = util.renormalize(value, 0, 1023, 0, 127) 61 | 62 | cc = [self.midi_channel | CONTROL_CHANGE, self.midi_CC, set_volume] 63 | logging.debug("AnalogControl Sending CC event %s" % cc) 64 | self.midiout.send_message(cc) 65 | 66 | # save the potentiometer reading for the next loop 67 | self.last_read = value 68 | -------------------------------------------------------------------------------- /pistomp/analogswitch.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import busio 17 | import digitalio 18 | import board 19 | import adafruit_mcp3xxx.mcp3008 as MCP 20 | from adafruit_mcp3xxx.analog_in import AnalogIn 21 | from enum import Enum 22 | 23 | 24 | import pistomp.analogcontrol as analogcontrol 25 | 26 | class Value(Enum): 27 | DEFAULT = 0 28 | PRESSED = 1 29 | RELEASED = 2 30 | LONGPRESSED = 3 31 | CLICKED = 4 32 | DOUBLECLICKED = 5 33 | 34 | LONGPRESS_THRESHOLD = 60 # TODO somewhat LAME. It's dependent on the refresh frequency of the main loop 35 | 36 | class AnalogSwitch(analogcontrol.AnalogControl): 37 | 38 | def __init__(self, spi, adc_channel, tolerance, callback): 39 | super(AnalogSwitch, self).__init__(spi, adc_channel, tolerance) 40 | self.value = None # this keeps track of the last value 41 | self.trigger_count = 0 42 | self.callback = callback 43 | self.longpress_state = False 44 | 45 | # Override of base class method 46 | def refresh(self): 47 | # read the analog pin 48 | new_value = self.readChannel() 49 | 50 | # if last read is None, this is the first refresh so don't do anything yet 51 | if self.value is None: 52 | self.value = new_value 53 | return 54 | 55 | # how much has it changed since the last read? 56 | pot_adjust = abs(new_value - self.value) 57 | value_changed = (pot_adjust > self.tolerance) 58 | 59 | # Count the number of simultaneous refresh cycles had the switch Low (triggered) 60 | if not self.longpress_state and new_value < self.tolerance and self.value < self.tolerance: 61 | self.trigger_count += 1 62 | if self.trigger_count > LONGPRESS_THRESHOLD: 63 | value_changed = True 64 | self.longpress_state = True 65 | 66 | if value_changed: 67 | 68 | # save the potentiometer reading for the next loop 69 | self.value = new_value 70 | 71 | if self.trigger_count > LONGPRESS_THRESHOLD: 72 | new_value = Value.LONGPRESSED 73 | elif new_value < self.tolerance: 74 | new_value = Value.PRESSED 75 | elif new_value >= self.tolerance: 76 | if self.longpress_state: 77 | self.longpress_state = False 78 | self.trigger_count = 0 79 | return 80 | else: 81 | new_value = Value.RELEASED 82 | self.trigger_count = 0 83 | 84 | self.callback(new_value) 85 | -------------------------------------------------------------------------------- /pistomp/audiocard.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import logging 17 | import mmap 18 | import os 19 | import re 20 | import subprocess 21 | from enum import Enum 22 | 23 | 24 | class Audiocard: 25 | 26 | def __init__(self, cwd): 27 | self.cwd = cwd 28 | self.card_index = 0 29 | self.config_file = '/var/lib/alsa/asound.state' # global config used by alsamixer, etc. 30 | self.initial_config_file = None # use this if common config_file loading fails 31 | self.initial_config_name = None 32 | self.card_index = 0 33 | 34 | # Superset of Alsa parameters for all cards (None == not supported) 35 | # Override in subclass with actual name 36 | self.CAPTURE_VOLUME = None 37 | self.DAC_EQ = None 38 | self.EQ_1 = None 39 | self.EQ_2 = None 40 | self.EQ_3 = None 41 | self.EQ_4 = None 42 | self.EQ_5 = None 43 | self.MASTER = None 44 | 45 | def restore(self): 46 | # If the global config_file either doesn't exist, doesn't contain the name of our audiocard, or fails restore, 47 | # read initial_config_file (our backup). This will be the case on first boot after install. 48 | # Subsequent boots will likely use the global config_file since initial_config_file settings will get 49 | # appended if a 'alsactl store' operation occurs or the system has a clean shutdown 50 | conf_files = [self.config_file, self.initial_config_file] 51 | for fname in conf_files: 52 | if os.access(fname, os.R_OK) is True: 53 | try: 54 | looking_for = bytes(("state.%s" % self.initial_config_name), 'utf-8') 55 | f = open(fname) 56 | with f as text: 57 | s = mmap.mmap(text.fileno(), 0, access=mmap.ACCESS_READ) 58 | if s.find(looking_for) != -1: 59 | logging.info("restoring audio card settings from: %s" % fname) 60 | subprocess.run(['/usr/sbin/alsactl', '-f', fname, '--no-lock', 'restore']) 61 | f.close() 62 | # If the file loaded was not the global, then save it so it will be next time 63 | if fname is not self.config_file: 64 | self.store() 65 | break 66 | f.close() 67 | except: 68 | logging.error("Failed trying to restore audio card settings from: %s" % fname) 69 | 70 | def store(self): 71 | # This will fail when the top level program is not run as root 72 | # Unfortunate that setting changes will not be persisted between boots, but not worth getting the mess of 73 | # dealing with file permissions or sync issues when settings are changed via another program (eg. aslamixer) 74 | try: 75 | subprocess.run(['/usr/sbin/alsactl', '-f', self.config_file, 'store'], stderr=subprocess.DEVNULL) 76 | logging.info("audio card settings saved to: %s" % self.config_file) 77 | except: 78 | logging.error("Failed trying to store audio card settings to: %s" % self.config_file) 79 | 80 | def _amixer_sget(self, param_name): 81 | cmd = "amixer -c %d -- sget '%s'" % (self.card_index, param_name) 82 | try: 83 | output = subprocess.check_output(cmd, shell=True) 84 | except subprocess.CalledProcessError: 85 | logging.error("Failed trying to get audio card parameter") 86 | return None 87 | return output.decode() 88 | 89 | def _amixer_sset(self, param_name, value, store): 90 | # when store is False settings will not be persisted between sessions unless an explicit call 91 | # to store() is made 92 | # setting to False is good when you want to set a bunch of things, then store 93 | cmd = "amixer -c %d -q -- sset '%s' '%s'" % (self.card_index, param_name, value) 94 | try: 95 | subprocess.check_output(cmd, shell=True) 96 | except subprocess.CalledProcessError: 97 | logging.error("Failed trying to set audio card parameter") 98 | return False 99 | if store: 100 | self.store() 101 | return True 102 | 103 | # 104 | # Use the following get and set methods depending on the value type 105 | # 106 | def get_volume_parameter(self, param_name): 107 | # for fader controls with values in dB, returns a float 108 | if param_name is None: 109 | return float(0) 110 | s = self._amixer_sget(param_name) 111 | pattern = r': (\d+) \[(\d+%)\] \[(-?\d+\.\d+)dB\]' 112 | matches = re.search(pattern, s) 113 | if matches: 114 | return round(float(matches.group(3)), 1) 115 | return float(0) 116 | 117 | def get_switch_parameter(self, param_name): 118 | # for switch/mute type controls, returns a boolean 119 | if param_name is None: 120 | return False 121 | s = self._amixer_sget(param_name) 122 | pattern = r': (.*) \[(on|off)\]' 123 | matches = re.search(pattern, s) 124 | if matches: 125 | return bool("on" == matches.group(2)) 126 | return False 127 | 128 | def get_enum_parameter(self, param_name): 129 | # for enum/selection type controls, returns a string 130 | if param_name is None: 131 | return None 132 | s = self._amixer_sget(param_name) 133 | pattern = r"Item0: '(.+)'" 134 | matches = re.search(pattern, s) 135 | if matches: 136 | return matches.group(1) 137 | return None 138 | 139 | def set_volume_parameter(self, param_name, value, store=True): 140 | # value expected to be a number (int or float) 141 | return self._amixer_sset(param_name, str(value) + "db", store) 142 | 143 | def set_switch_parameter(self, param_name, value, store=True): 144 | # value expected to be a boolean 145 | return self._amixer_sset(param_name, "on" if value else "off", store) 146 | 147 | def set_enum_parameter(self, param_name, value, store=True): 148 | # value expected to be a string (specifically one of the enum choices for the parameter) 149 | return self._amixer_sset(param_name, str(value), store) 150 | 151 | -------------------------------------------------------------------------------- /pistomp/audiocardfactory.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import pistomp.audioinjector 17 | import pistomp.hifiberry 18 | import pistomp.iqaudiocodec 19 | from pathlib import Path 20 | 21 | 22 | class Audiocardfactory: 23 | __single = None 24 | 25 | def __init__(self, cwd): 26 | if Audiocardfactory.__single: 27 | raise Audiocardfactory.__single 28 | Audiocardfactory.__single = self 29 | self.cwd = cwd 30 | self.system_card_file="/proc/asound/cards" 31 | 32 | def get_current_card(self): 33 | result = None 34 | if Path(self.system_card_file).exists() is False: 35 | return result 36 | 37 | with open(self.system_card_file) as f: 38 | line = f.readline() 39 | while line: 40 | strs = line.split() 41 | if len(strs) > 2 and strs[0] == '0': 42 | result = strs[1].lstrip('[').rstrip(']:') 43 | break 44 | line = f.readline() 45 | f.close() 46 | return result 47 | 48 | def create(self): 49 | # get the current card 50 | card_name = self.get_current_card() 51 | if card_name == "IQaudIOCODEC": 52 | card = pistomp.iqaudiocodec.IQaudioCodec(self.cwd) 53 | elif card_name == "sndrpihifiberry": 54 | card = pistomp.hifiberry.Hifiberry(self.cwd) 55 | elif card_name == "audioinjectorpi": 56 | card = pistomp.audioinjector.Audioinjector(self.cwd) 57 | else: # Could be explicit here but we need to return some card, so make it the most common option 58 | card = pistomp.iqaudiocodec.IQaudioCodec(self.cwd) 59 | 60 | return card 61 | -------------------------------------------------------------------------------- /pistomp/audioinjector.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import os 17 | import pistomp.audiocard as audiocard 18 | 19 | 20 | class Audioinjector(audiocard.Audiocard): 21 | 22 | def __init__(self, cwd): 23 | super(Audioinjector, self).__init__(cwd) 24 | self.initial_config_file = os.path.join(cwd, 'setup', 'audio', 'audioinjector.state') 25 | self.initial_config_name = 'audioinjectorpi' 26 | self.CAPTURE_VOLUME = 'Capture' 27 | self.MASTER = 'Master' -------------------------------------------------------------------------------- /pistomp/category.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import logging 17 | from PIL import ImageColor 18 | import common.util as util 19 | 20 | 21 | category_color_map = { 22 | 'Delay': "MediumVioletRed", 23 | 'Distortion': "Lime", 24 | 'Dynamics': "OrangeRed", 25 | 'Filter': (205, 133, 40), 26 | 'Generator': "Indigo", 27 | 'Midiutility': "Gray", 28 | 'Modulator': (50, 50, 255), 29 | 'Reverb': (20, 160, 255), 30 | 'Simulator': "SaddleBrown", 31 | 'Spacial': "Gray", 32 | 'Spectral': "Red", 33 | 'Utility': "Gray" 34 | } 35 | 36 | 37 | # Try to map color to a valid displayable color, otherwise return None 38 | def valid_color(color): 39 | if color is None: 40 | return None 41 | try: 42 | return ImageColor.getrgb(color) 43 | except ValueError: 44 | logging.error("Cannot convert color name: %s" % color) 45 | return None 46 | 47 | 48 | # Get the color assigned to the plugin category 49 | def get_category_color(category, default_color=(80, 80, 80)): 50 | if category: 51 | c = util.DICT_GET(category_color_map, category) 52 | if c and isinstance(c, tuple): 53 | return c 54 | converted = valid_color(c) 55 | if converted: 56 | return converted 57 | return default_color 58 | -------------------------------------------------------------------------------- /pistomp/config.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import os 17 | import yaml 18 | 19 | data_dir = '/home/pistomp/data/config' 20 | 21 | DEFAULT_CONFIG_FILE = "default_config.yml" 22 | 23 | def load_default_cfg(): 24 | # Read the default config file - should only need to read once per session 25 | default_config_file = os.path.join(data_dir, DEFAULT_CONFIG_FILE) 26 | with open(default_config_file, 'r') as ymlfile: 27 | cfg = yaml.load(ymlfile, Loader=yaml.SafeLoader) 28 | return cfg 29 | -------------------------------------------------------------------------------- /pistomp/controller.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | from enum import Enum 17 | import json 18 | import logging 19 | 20 | 21 | class Controller: 22 | 23 | def __init__(self, midi_channel, midi_CC): 24 | self.midi_channel = midi_channel 25 | self.midi_CC = midi_CC 26 | self.minimum = None 27 | self.maximum = None 28 | self.parameter = None 29 | self.hardware_name = None 30 | self.type = None 31 | 32 | def to_json(self): 33 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) 34 | 35 | def set_value(self, value): 36 | logging.error("Controller subclass hasn't overriden the set_value method") 37 | 38 | 39 | -------------------------------------------------------------------------------- /pistomp/encoder.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import RPi.GPIO as GPIO 17 | import threading 18 | 19 | from functools import partial 20 | 21 | 22 | class Encoder: 23 | 24 | def _process_gpios(self): 25 | # This decode/debouce algorithm adapted from 26 | # https://www.best-microcontroller-projects.com/rotary-encoder.html 27 | 28 | self.prevNextCode <<= 2 29 | if GPIO.input(self.clk_pin): 30 | self.prevNextCode |= 0x02 31 | if GPIO.input(self.d_pin): 32 | self.prevNextCode |= 0x01 33 | self.prevNextCode &= 0x0f 34 | 35 | direction = 0 36 | # Check for valid code 37 | if self.rot_enc_table[self.prevNextCode]: 38 | self.store <<= 4 39 | self.store |= self.prevNextCode 40 | # Check last two codes (end of detent transition) 41 | if (self.store & 0xff) == 0x2b: # code 2 followed by code 11 (full sequence is 13,4,2,11) 42 | direction = -1 # Counter Clockwise 43 | if (self.store & 0xff) == 0x17: # code 1 followed by code 7 (full sequence is 14,8,1,7) 44 | direction = 1 # Clockwise 45 | if direction != 0: 46 | self.store = self.prevNextCode 47 | return direction 48 | 49 | def _gpio_callback(self, channel): 50 | d = self._process_gpios() 51 | if d != 0: 52 | with self._lock: 53 | self.direction += d 54 | 55 | def __init__(self, d_pin, clk_pin, callback, use_interrupt = True): 56 | 57 | self.d_pin = d_pin 58 | self.clk_pin = clk_pin 59 | self.callback = callback 60 | self.use_interrupt = use_interrupt 61 | 62 | GPIO.setup(self.d_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 63 | GPIO.setup(self.clk_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 64 | 65 | if self.use_interrupt: 66 | GPIO.add_event_detect(self.d_pin, GPIO.BOTH, callback=self._gpio_callback) 67 | GPIO.add_event_detect(self.clk_pin, GPIO.BOTH, callback=self._gpio_callback) 68 | # It works fine without a lock since this is just dumb UI, but let's be correct.. 69 | self._lock = threading.Lock() 70 | 71 | self.prevNextCode = 0 72 | self.store = 0 73 | self.direction = 0 74 | 75 | # 16 possible grey codes. 1=Valid, 0=Invalid (bounce) 76 | self.rot_enc_table = [0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0] 77 | 78 | def __del__(self): 79 | GPIO.remove_event_detect(self._gpio_callback) 80 | 81 | def get_data(self): 82 | return GPIO.input(self.d_pin) 83 | 84 | def get_clk(self): 85 | return GPIO.input(self.clk_pin) 86 | 87 | def read_rotary(self): 88 | d = 0 89 | if self.use_interrupt: 90 | if self.direction != 0: 91 | with self._lock: 92 | if self.direction > 0: 93 | d = 1 94 | elif self.direction < 0: 95 | d = -1 96 | self.direction -= d 97 | else: 98 | d = self._process_gpios() 99 | if d != 0: 100 | self.callback(d) 101 | -------------------------------------------------------------------------------- /pistomp/encoderswitch.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | from enum import Enum 17 | 18 | import pistomp.gpioswitch as gpioswitch 19 | import time 20 | 21 | class Value(Enum): 22 | DEFAULT = 0 23 | PRESSED = 1 24 | RELEASED = 2 25 | LONGPRESSED = 3 26 | CLICKED = 4 27 | DOUBLECLICKED = 5 28 | 29 | 30 | class EncoderSwitch(gpioswitch.GpioSwitch): 31 | 32 | def __init__(self, gpio, callback): 33 | super(EncoderSwitch, self).__init__(gpio, None, None) 34 | self.last_read = None # this keeps track of the last value 35 | self.trigger_count = 0 36 | self.callback = callback 37 | self.longpress_state = False 38 | self.gpio = gpio 39 | 40 | # Override of base class method 41 | def pressed(self, short): 42 | self.callback(Value.RELEASED if short else Value.LONGPRESSED) 43 | -------------------------------------------------------------------------------- /pistomp/footswitch.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import logging 17 | import RPi.GPIO as GPIO 18 | from rtmidi.midiconstants import CONTROL_CHANGE 19 | 20 | import pistomp.gpioswitch as gpioswitch 21 | 22 | class Footswitch(gpioswitch.GpioSwitch): 23 | 24 | def __init__(self, id, fs_pin, led_pin, pixel, midi_CC, midi_channel, midiout, refresh_callback): 25 | super(Footswitch, self).__init__(fs_pin, midi_channel, midi_CC) 26 | self.id = id 27 | self.display_label = None 28 | self.enabled = False 29 | self.fs_pin = fs_pin 30 | self.led_pin = led_pin 31 | self.midiout = midiout 32 | self.refresh_callback = refresh_callback 33 | self.relay_list = [] 34 | self.preset_callback = None 35 | self.preset_callback_arg = None 36 | self.lcd_color = None 37 | self.category = None 38 | self.pixel = pixel 39 | 40 | if led_pin is not None: 41 | GPIO.setup(led_pin, GPIO.OUT) 42 | self._set_led(GPIO.LOW) 43 | 44 | # Should this be in Controller ? 45 | def set_midi_CC(self, midi_CC): 46 | self.midi_CC = midi_CC 47 | 48 | # Should this be in Controller ? 49 | def set_midi_channel(self, midi_channel): 50 | self.midi_channel = midi_channel 51 | 52 | def set_value(self, value): 53 | self.enabled = (value < 1) 54 | self._set_led(self.enabled) 55 | 56 | def _set_led(self, enabled): 57 | if self.led_pin is not None: 58 | GPIO.output(self.led_pin, enabled) 59 | if self.pixel: 60 | self.pixel.set_enable(enabled) 61 | 62 | def set_category(self, category): 63 | self.category = category 64 | if self.pixel: 65 | self.pixel.set_color_by_category(category, self.enabled) 66 | 67 | def set_lcd_color(self, color): 68 | self.lcd_color = color 69 | 70 | def pressed(self, short): 71 | # If a footswitch can be mapped to control a relay, preset, MIDI or all 3 72 | # 73 | # The footswitch will only "toggle" if it's associated with a relay 74 | # (in which case it will toggle with the relay) or with a Midi message 75 | # 76 | new_enabled = not self.enabled 77 | 78 | # Update Relay (if relay is associated with this footswitch) 79 | if len(self.relay_list) > 0: 80 | if short is False: 81 | # Pin kept low (long press) 82 | # toggle the relay and LED, exit this method 83 | self.enabled = new_enabled 84 | for r in self.relay_list: 85 | if self.enabled: 86 | r.enable() 87 | else: 88 | r.disable() 89 | self._set_led(self.enabled) 90 | self.refresh_callback(True) # True means this is a bypass change only 91 | return 92 | 93 | # If mapped to preset change 94 | if self.preset_callback is not None: 95 | # Change the preset and exit this method. Don't flip "enabled" since 96 | # there is no "toggle" action associated with a preset 97 | if self.preset_callback_arg is None: 98 | self.preset_callback() 99 | else: 100 | self.preset_callback(self.preset_callback_arg) 101 | return 102 | 103 | # Send midi 104 | if self.midi_CC is not None: 105 | self.enabled = new_enabled 106 | # Update LED 107 | self._set_led(self.enabled) 108 | cc = [self.midi_channel | CONTROL_CHANGE, self.midi_CC, 127 if self.enabled else 0] 109 | logging.debug("Sending CC event: %d %s" % (self.midi_CC, self.fs_pin)) 110 | self.midiout.send_message(cc) 111 | 112 | # Update plugin parameter if any 113 | if self.parameter is not None: 114 | self.parameter.value = not self.enabled # TODO assumes mapped parameter is :bypass 115 | 116 | # Update LCD 117 | self.refresh_callback() 118 | 119 | def set_display_label(self, label): 120 | self.display_label = label 121 | 122 | def add_relay(self, relay): 123 | self.relay_list.append(relay) 124 | self.set_value(not relay.init_state()) 125 | 126 | def clear_relays(self): 127 | self.relay_list.clear() 128 | 129 | def add_preset(self, callback, callback_arg=None): 130 | self.preset_callback = callback 131 | self.preset_callback_arg = callback_arg 132 | 133 | def clear_pedalboard_info(self): 134 | self.enabled = False 135 | self.display_label = None 136 | self.set_category(None) 137 | self.preset_callback = None 138 | self.clear_relays() 139 | -------------------------------------------------------------------------------- /pistomp/generichost.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | from pistomp.handler import Handler 17 | 18 | class Generichost(Handler): 19 | 20 | def __init__(self, homedir=None): 21 | self.homedir = homedir 22 | self.hardware = None 23 | 24 | def add_hardware(self, hardware): 25 | self.hardware = hardware 26 | 27 | def poll_controls(self): 28 | if self.hardware: 29 | self.hardware.poll_controls() 30 | -------------------------------------------------------------------------------- /pistomp/gpioswitch.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import logging 17 | import RPi.GPIO as GPIO 18 | from rtmidi.midiconstants import CONTROL_CHANGE 19 | 20 | import pistomp.controller as controller 21 | import time 22 | import queue 23 | 24 | class GpioSwitch(controller.Controller): 25 | 26 | def __init__(self, fs_pin, midi_channel, midi_CC): 27 | super(GpioSwitch, self).__init__(midi_channel, midi_CC) 28 | self.fs_pin = fs_pin 29 | self.cur_tstamp = None 30 | self.events = queue.Queue() 31 | 32 | # Long press threshold in seconds 33 | self.long_press_threshold = 0.5 34 | 35 | GPIO.setup(fs_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 36 | GPIO.add_event_detect(fs_pin, GPIO.FALLING, callback=self._gpio_down, bouncetime=250) 37 | 38 | def __del__(self): 39 | GPIO.remove_event_detect(self.fs_pin) 40 | 41 | def _gpio_down(self, gpio): 42 | # This is run from a separate thread, timestamp pressed and queue an event 43 | # 44 | # I considered using a dual edge callback and handle the timestamp here 45 | # to queue long/short press events, but in practice, I noticed dual edge 46 | # is rather unreliable with such a long debounce, we often don't get the 47 | # rising edge callback at all. So let's just timestamp and we'll handle 48 | # everything from the poller thread 49 | # 50 | self.events.put(time.monotonic()) 51 | 52 | def poll(self): 53 | # Grab press event if any 54 | if not self.events.empty(): 55 | new_tstamp = self.events.get_nowait() 56 | else: 57 | new_tstamp = None 58 | 59 | # If we were a already pressed and waiting for a release, drop it, it's easier 60 | # that way and we should be polling fast enough for this not to matter. 61 | # Otherwise record it 62 | if self.cur_tstamp is None: 63 | self.cur_tstamp = new_tstamp 64 | 65 | # Are we waiting for release ? 66 | if self.cur_tstamp is None: 67 | return 68 | 69 | time_pressed = time.monotonic() - self.cur_tstamp 70 | 71 | # If it's a long press, process as soon as we reach the threshold, otherwise 72 | # check the GPIO input 73 | if time_pressed > self.long_press_threshold: 74 | short = False 75 | elif GPIO.input(self.fs_pin): 76 | short = True 77 | else: 78 | return 79 | self.cur_tstamp = None 80 | 81 | logging.debug("Switch %d %s press" % (self.fs_pin, "short" if short else "long")) 82 | self.pressed(short) 83 | -------------------------------------------------------------------------------- /pistomp/handler.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | 17 | class Handler: 18 | 19 | def __init__(self): 20 | self.homedir = None 21 | self.lcd = None 22 | pass 23 | 24 | def noop(self): 25 | pass 26 | 27 | def update_lcd_fs(self, bypass_change=False): 28 | pass 29 | 30 | def add_lcd(self, lcd): 31 | pass 32 | 33 | def add_hardware(self, hardware): 34 | pass 35 | 36 | def poll_controls(self): 37 | pass 38 | 39 | def poll_modui_changes(self): 40 | pass 41 | 42 | def preset_incr_and_change(self): 43 | pass 44 | 45 | def preset_decr_and_change(self): 46 | pass 47 | 48 | def top_encoder_select(self, direction): 49 | pass 50 | 51 | def top_encoder_sw(self, value): 52 | pass 53 | 54 | def bot_encoder_select(self, direction): 55 | pass 56 | 57 | def bottom_encoder_sw(self, value): 58 | pass 59 | 60 | def universal_encoder_select(self, direction): 61 | pass 62 | 63 | def universal_encoder_sw(self, value): 64 | pass 65 | 66 | def cleanup(self): 67 | pass 68 | -------------------------------------------------------------------------------- /pistomp/hardware.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import logging 17 | import os 18 | import spidev 19 | import sys 20 | 21 | import common.token as Token 22 | import common.util as Util 23 | import pistomp.analogmidicontrol as AnalogMidiControl 24 | import pistomp.footswitch as Footswitch 25 | import pistomp.ledstrip as Ledstrip 26 | 27 | from abc import abstractmethod 28 | 29 | 30 | class Hardware: 31 | 32 | def __init__(self, default_config, mod, midiout, refresh_callback): 33 | logging.info("Init hardware: " + type(self).__name__) 34 | self.mod = mod 35 | self.midiout = midiout 36 | self.refresh_callback = refresh_callback 37 | self.spi = None 38 | self.test_pass = False 39 | self.test_sentinel = None 40 | 41 | # From config file(s) 42 | self.default_cfg = default_config 43 | self.version = self.default_cfg[Token.HARDWARE][Token.VERSION] 44 | self.cfg = None # compound cfg (default with user/pedalboard specific cfg overlaid) 45 | self.midi_channel = 0 46 | 47 | # Standard hardware objects (not required to exist) 48 | self.relay = None 49 | self.analog_controls = [] 50 | self.encoders = [] 51 | self.controllers = {} 52 | self.footswitches = [] 53 | self.encoder_switches = [] 54 | self.debounce_map = None 55 | self.ledstrip = None 56 | 57 | def init_spi(self): 58 | self.spi = spidev.SpiDev() 59 | self.spi.open(0, 1) # Bus 0, CE1 60 | # TODO SPI bus is shared by ADC and LCD. Ideally, they would use the same frequency. 61 | # MCP3008 ADC has a max of 1MHz (higher makes it loose resolution) 62 | # Color LCD needs to run at 24Mhz 63 | # until we can get them on the same, we'll set ADC (the one set here) to be a slower multiple of the LCD 64 | #self.spi.max_speed_hz = 24000000 65 | #self.spi.max_speed_hz = 1000000 66 | self.spi.max_speed_hz = 240000 67 | 68 | def poll_controls(self): 69 | # This is intended to be called periodically from main working loop to poll the instantiated controls 70 | for c in self.analog_controls: 71 | c.refresh() 72 | for e in self.encoders: 73 | e.read_rotary() 74 | for s in self.encoder_switches: 75 | s.poll() 76 | for s in self.footswitches: 77 | s.poll() 78 | 79 | def reinit(self, cfg): 80 | # reinit hardware as specified by the new cfg context (after pedalboard change, etc.) 81 | self.cfg = self.default_cfg.copy() 82 | 83 | self.__init_midi_default() 84 | self.__init_footswitches(self.cfg) 85 | 86 | if cfg is not None: 87 | self.__init_midi(cfg) 88 | self.__init_footswitches(cfg) 89 | 90 | @abstractmethod 91 | def init_analog_controls(self): 92 | pass 93 | 94 | @abstractmethod 95 | def init_encoders(self): 96 | pass 97 | 98 | @abstractmethod 99 | def init_footswitches(self): 100 | pass 101 | 102 | @abstractmethod 103 | def init_relays(self): 104 | pass 105 | 106 | @abstractmethod 107 | def test(self): 108 | pass 109 | 110 | def run_test(self): 111 | # if test sentinel file exists execute hardware test 112 | script_dir = os.path.dirname(os.path.realpath(__file__)) 113 | self.test_sentinel = os.path.join(script_dir, ".hardware_tests_passed") 114 | if not os.path.isfile(self.test_sentinel): 115 | self.test_pass = False 116 | self.test() 117 | 118 | def create_footswitches(self, cfg): 119 | if cfg is None or (Token.HARDWARE not in cfg) or (Token.FOOTSWITCHES not in cfg[Token.HARDWARE]): 120 | return 121 | 122 | cfg_fs = cfg[Token.HARDWARE][Token.FOOTSWITCHES] 123 | if cfg_fs is None: 124 | return 125 | 126 | # determine if an ledstrip is referenced, if so create an object 127 | ledstrip_gpio = None 128 | gpio_output_list = [] 129 | for f in cfg_fs: 130 | if self.ledstrip is None and Util.DICT_GET(f, Token.LEDSTRIP_POSITION) is not None: 131 | self.ledstrip = Ledstrip.Ledstrip() 132 | ledstrip_gpio = self.ledstrip.get_gpio() 133 | gpio_output_list.append(Util.DICT_GET(f, Token.GPIO_OUTPUT)) 134 | 135 | # Must make sure a gpio_output is not specified on the PWM pin used for an ledstring 136 | if ledstrip_gpio is not None and ledstrip_gpio in gpio_output_list: 137 | logging.error("Config file error. Cannot have %s on the same GPIO as used for an ledstring referenced by %s" 138 | % (Token.GPIO_OUTPUT, Token.LEDSTRIP_POSITION)) 139 | sys.exit() 140 | 141 | midi_channel = self.__get_real_midi_channel(cfg) 142 | idx = 0 143 | for f in cfg_fs: 144 | if Util.DICT_GET(f, Token.DISABLE) is True: 145 | continue 146 | 147 | di = Util.DICT_GET(f, Token.DEBOUNCE_INPUT) 148 | if self.debounce_map and di in self.debounce_map: 149 | gpio_input = self.debounce_map[di] 150 | else: 151 | gpio_input = Util.DICT_GET(f, Token.GPIO_INPUT) 152 | 153 | gpio_output = Util.DICT_GET(f, Token.GPIO_OUTPUT) 154 | midi_cc = Util.DICT_GET(f, Token.MIDI_CC) 155 | id = Util.DICT_GET(f, Token.ID) 156 | led_position = Util.DICT_GET(f, Token.LEDSTRIP_POSITION) 157 | 158 | if gpio_input is None: 159 | logging.error("Switch specified without %s or %s" % (Token.DEBOUNCE_INPUT, Token.GPIO_INPUT)) 160 | continue 161 | 162 | pixel = None 163 | if self.ledstrip and led_position is not None: 164 | pixel = self.ledstrip.add_pixel(id if id else idx, led_position) 165 | fs = Footswitch.Footswitch(id if id else idx, gpio_input, gpio_output, pixel, midi_cc, midi_channel, 166 | self.midiout, refresh_callback=self.refresh_callback) 167 | self.footswitches.append(fs) 168 | idx += 1 169 | 170 | def create_analog_controls(self, cfg): 171 | if cfg is None or (Token.HARDWARE not in cfg) or (Token.ANALOG_CONTROLLERS not in cfg[Token.HARDWARE]): 172 | return 173 | 174 | midi_channel = self.__get_real_midi_channel(cfg) 175 | cfg_c = cfg[Token.HARDWARE][Token.ANALOG_CONTROLLERS] 176 | if cfg_c is None: 177 | return 178 | for c in cfg_c: 179 | if Util.DICT_GET(c, Token.DISABLE) is True: 180 | continue 181 | 182 | adc_input = Util.DICT_GET(c, Token.ADC_INPUT) 183 | midi_cc = Util.DICT_GET(c, Token.MIDI_CC) 184 | threshold = Util.DICT_GET(c, Token.THRESHOLD) 185 | control_type = Util.DICT_GET(c, Token.TYPE) 186 | 187 | if adc_input is None: 188 | logging.error("Analog control specified without %s" % Token.ADC_INPUT) 189 | continue 190 | if midi_cc is None: 191 | logging.error("Analog control specified without %s" % Token.MIDI_CC) 192 | continue 193 | if threshold is None: 194 | threshold = 16 # Default, 1024 is full scale 195 | 196 | control = AnalogMidiControl.AnalogMidiControl(self.spi, adc_input, threshold, midi_cc, midi_channel, 197 | self.midiout, control_type, c) 198 | self.analog_controls.append(control) 199 | key = format("%d:%d" % (midi_channel, midi_cc)) 200 | self.controllers[key] = control 201 | 202 | def __get_real_midi_channel(self, cfg): 203 | chan = 0 204 | try: 205 | val = cfg[Token.HARDWARE][Token.MIDI][Token.CHANNEL] 206 | # LAME bug in Mod detects MIDI channel as one higher than sent (7 sent, seen by mod as 8) so compensate here 207 | chan = val - 1 if val > 0 else 0 208 | except KeyError: 209 | pass 210 | return chan 211 | 212 | def __init_midi_default(self): 213 | self.__init_midi(self.cfg) 214 | 215 | def __init_midi(self, cfg): 216 | self.midi_channel = self.__get_real_midi_channel(cfg) 217 | # TODO could iterate thru all objects here instead of handling in __init_footswitches 218 | for ac in self.analog_controls: 219 | if isinstance(ac, AnalogMidiControl.AnalogMidiControl): 220 | ac.set_midi_channel(self.midi_channel) 221 | 222 | def __init_footswitches_default(self): 223 | for fs in self.footswitches: 224 | fs.clear_relays() 225 | self.__init_footswitches(self.cfg) 226 | 227 | def __init_footswitches(self, cfg): 228 | if cfg is None or (Token.HARDWARE not in cfg) or (Token.FOOTSWITCHES not in cfg[Token.HARDWARE]): 229 | return 230 | cfg_fs = cfg[Token.HARDWARE][Token.FOOTSWITCHES] 231 | idx = 0 232 | for fs in self.footswitches: 233 | # See if a corresponding cfg entry exists. if so, override 234 | f = None 235 | for f in cfg_fs: 236 | if f[Token.ID] == idx: 237 | break 238 | else: 239 | f = None 240 | 241 | if f is not None: 242 | # TODO reusing the footswitch object for multiple pedalboards is not ideal 243 | # could easily have spillover from a previous pedalboard 244 | # The mutable data should probably be stored in a separate object and destructed/constructed upon 245 | # each pedalboard load 246 | fs.clear_pedalboard_info() 247 | 248 | # Bypass 249 | if Token.BYPASS in f: 250 | # TODO no more right or left 251 | if f[Token.BYPASS] == Token.LEFT_RIGHT or f[Token.BYPASS] == Token.LEFT: 252 | fs.add_relay(self.relay) 253 | fs.set_display_label("byps") 254 | 255 | # Midi 256 | if Token.MIDI_CC in f: 257 | cc = f[Token.MIDI_CC] 258 | if cc == Token.NONE: 259 | fs.set_midi_CC(None) 260 | for k, v in self.controllers.items(): 261 | if v == fs: 262 | self.controllers.pop(k) 263 | break 264 | else: 265 | fs.set_midi_channel(self.midi_channel) 266 | fs.set_midi_CC(cc) 267 | key = format("%d:%d" % (self.midi_channel, fs.midi_CC)) 268 | self.controllers[key] = fs # TODO problem if this creates a new element? 269 | 270 | # Preset Control 271 | if Token.PRESET in f: 272 | preset_value = f[Token.PRESET] 273 | if preset_value == Token.UP: 274 | fs.add_preset(callback=self.mod.preset_incr_and_change) 275 | fs.set_display_label("Pre+") 276 | elif preset_value == Token.DOWN: 277 | fs.add_preset(callback=self.mod.preset_decr_and_change) 278 | fs.set_display_label("Pre-") 279 | elif isinstance(preset_value, int): 280 | fs.add_preset(callback=self.mod.preset_set_and_change, callback_arg=preset_value) 281 | fs.set_display_label(str(preset_value)) 282 | 283 | # LCD/LED attributes 284 | if Token.COLOR in f: 285 | fs.set_lcd_color(f[Token.COLOR]) 286 | 287 | idx += 1 288 | -------------------------------------------------------------------------------- /pistomp/hardwarefactory.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import common.token as Token 17 | import pistomp.config as config 18 | 19 | import pistomp.pistomp as Pistomp 20 | import pistomp.pistompcore as Pistompcore 21 | 22 | class Hardwarefactory: 23 | __single = None 24 | 25 | def __init__(self): 26 | if Hardwarefactory.__single: 27 | raise Hardwarefactory.__single 28 | Hardwarefactory.__single = self 29 | 30 | self.cfg = config.load_default_cfg() 31 | 32 | def create(self, handler, midiout): 33 | version = self.cfg[Token.HARDWARE][Token.VERSION] 34 | if version is None or (version < 2.0): 35 | return Pistomp.Pistomp(self.cfg, handler, midiout, refresh_callback=handler.update_lcd_fs) 36 | elif (version >= 2.0) and (version < 3.0): 37 | return Pistompcore.Pistompcore(self.cfg, handler, midiout, refresh_callback=handler.update_lcd_fs) 38 | -------------------------------------------------------------------------------- /pistomp/hifiberry.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import os 17 | import pistomp.audiocard as audiocard 18 | 19 | 20 | class Hifiberry(audiocard.Audiocard): 21 | 22 | def __init__(self, cwd): 23 | super(Hifiberry, self).__init__(cwd) 24 | self.initial_config_file = os.path.join(cwd, 'setup', 'audio', 'hifiberry.state') 25 | self.initial_config_name = 'sndrpihifiberry' 26 | self.CAPTURE_VOLUME = 'Digital' 27 | self.MASTER = None 28 | -------------------------------------------------------------------------------- /pistomp/iqaudiocodec.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import os 17 | import pistomp.audiocard as audiocard 18 | 19 | 20 | class IQaudioCodec(audiocard.Audiocard): 21 | 22 | def __init__(self, cwd): 23 | super(IQaudioCodec, self).__init__(cwd) 24 | self.initial_config_file = os.path.join(cwd, 'setup', 'audio', 'iqaudiocodec.state') 25 | self.initial_config_name = 'IQaudIOCODEC' 26 | self.CAPTURE_VOLUME = 'Aux' 27 | self.MASTER = 'Headphone' # Changed to headphone to allow digital control of output. 28 | self.DAC_EQ = "DAC EQ" 29 | self.EQ_1 = 'DAC EQ1' 30 | self.EQ_2 = 'DAC EQ2' 31 | self.EQ_3 = 'DAC EQ3' 32 | self.EQ_4 = 'DAC EQ4' 33 | self.EQ_5 = 'DAC EQ5' -------------------------------------------------------------------------------- /pistomp/lcd.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | from abc import ABC, abstractmethod 17 | 18 | 19 | class Lcd(ABC): 20 | 21 | def __init__(self, cwd): 22 | # expects cwd (current working directory) 23 | pass 24 | 25 | @abstractmethod 26 | def splash_show(self, boot=True): 27 | pass 28 | 29 | @abstractmethod 30 | def clear(self): 31 | pass 32 | 33 | @abstractmethod 34 | def erase_all(self): 35 | pass 36 | 37 | @abstractmethod 38 | def clear_select(self): 39 | pass 40 | 41 | # Toolbar 42 | @abstractmethod 43 | def draw_tools(self, wifi_type, eq_type, bypass_type, system_type): 44 | pass 45 | 46 | @abstractmethod 47 | def update_wifi(self, wifi_status): 48 | pass 49 | 50 | @abstractmethod 51 | def update_eq(self, eq_status): 52 | pass 53 | 54 | @abstractmethod 55 | def update_bypass(self, bypass): 56 | pass 57 | 58 | @abstractmethod 59 | def draw_tool_select(self, tool_type): 60 | pass 61 | 62 | # Menu Screens (uses deep_edit image and draw objects) 63 | @abstractmethod 64 | def menu_show(self, page_title, menu_items): 65 | pass 66 | 67 | @abstractmethod 68 | def menu_highlight(self, index): 69 | pass 70 | 71 | # Parameter Value Edit 72 | @abstractmethod 73 | def draw_value_edit(self, plugin_name, parameter, value): 74 | pass 75 | 76 | @abstractmethod 77 | def draw_value_edit_graph(self, parameter, value): 78 | pass 79 | 80 | @abstractmethod 81 | def draw_title(self, pedalboard, preset, invert_pb, invert_pre, highlight_only): 82 | pass 83 | 84 | # Analog Assignments (Tweak, Expression Pedal, etc.) 85 | @abstractmethod 86 | def draw_analog_assignments(self, controllers): 87 | pass 88 | 89 | @abstractmethod 90 | def draw_info_message(self, text): 91 | pass 92 | 93 | # Plugins 94 | @abstractmethod 95 | def draw_plugin_select(self, plugin=None): 96 | pass 97 | 98 | @abstractmethod 99 | def draw_bound_plugins(self, plugins, footswitches): 100 | pass 101 | 102 | @abstractmethod 103 | def draw_plugins(self, plugins): 104 | pass 105 | 106 | @abstractmethod 107 | def refresh_plugins(self): 108 | pass 109 | 110 | @abstractmethod 111 | def refresh_zone(self, zone_idx): 112 | pass 113 | 114 | @abstractmethod 115 | def shorten_name(self): 116 | pass 117 | -------------------------------------------------------------------------------- /pistomp/lcd135x240.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | from abc import ABC, abstractmethod 17 | 18 | import board 19 | import busio 20 | import digitalio 21 | from PIL import Image, ImageDraw, ImageFont 22 | import adafruit_rgb_display.st7789 as st7789 23 | 24 | 25 | class Lcd(ABC): 26 | 27 | def __init__(self, cwd): 28 | # Configuration for CS and DC pins (these are FeatherWing defaults on M0/M4): 29 | cs_pin = digitalio.DigitalInOut(board.CE0) 30 | dc_pin = digitalio.DigitalInOut(board.D1) 31 | reset_pin = None 32 | 33 | # Config for display baudrate (default max is 24mhz): 34 | BAUDRATE = 64000000 35 | 36 | # Setup SPI bus using hardware SPI: 37 | spi = board.SPI() 38 | 39 | # Create the ST7789 display: 40 | self.disp = st7789.ST7789( 41 | spi, 42 | cs=cs_pin, 43 | dc=dc_pin, 44 | rst=reset_pin, 45 | baudrate=BAUDRATE, 46 | width=135, 47 | height=240, 48 | x_offset=53, 49 | y_offset=40, 50 | ) 51 | 52 | # Create blank image for drawing. 53 | # Make sure to create image with mode '1' for 1-bit color. 54 | self.width = self.disp.width - 1 55 | self.height = self.disp.height - 1 56 | 57 | padding = 0 58 | self.top = padding 59 | self.bottom = self.height - padding 60 | self.image = Image.new("RGB", (self.height, self.width)) 61 | 62 | # Get drawing object to draw on image. 63 | self.draw = ImageDraw.Draw(self.image) 64 | 65 | # Draw a black filled box to clear the image. 66 | #self.draw.rectangle((0, 0, self.height, self.width), outline=0, fill=0) 67 | 68 | # Font 69 | self.font_size = 30 70 | self.font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', self.font_size) 71 | self.splash_font_size = 40 72 | self.splash_font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', self.splash_font_size) 73 | 74 | # Splash 75 | self.splash_show() 76 | 77 | def refresh(self): 78 | self.disp.image(self.image, 90) 79 | 80 | def splash_show(self, boot=True): 81 | self.clear() 82 | self.draw.text((0, self.top + 30), "pi Stomp!", font=self.splash_font, fill=(255, 255, 255)) 83 | self.refresh() 84 | 85 | def cleanup(self): 86 | self.clear() 87 | 88 | def clear(self): 89 | self.draw.rectangle((0, 0, self.height, self.width), outline=0, fill=(255, 255, 255)) 90 | self.disp.image(self.image, 90) 91 | 92 | # Menu Screens (uses deep_edit image and draw objects) 93 | def menu_show(self, page_title, menu_items): 94 | pass 95 | 96 | def menu_highlight(self, index): 97 | pass 98 | 99 | # Parameter Value Edit 100 | def draw_value_edit(self, plugin_name, parameter, value): 101 | pass 102 | 103 | def draw_value_edit_graph(self, parameter, value): 104 | pass 105 | 106 | def draw_title(self, pedalboard, preset, invert_pb, invert_pre): 107 | x = 0 108 | self.clear() 109 | self.draw.text((x, self.top), pedalboard, font=self.font, fill=255) 110 | self.draw.text((x, self.top + self.font_size), preset, font=self.font, fill=255) 111 | 112 | x = 5 113 | y = 70 114 | square = 30 115 | pitch = 10 116 | self.draw.rectangle((x, y, x + square, y + square), outline=0, fill=(200, 0, 0)) 117 | x = x + square + pitch 118 | self.draw.rectangle((x, y, x + square, y + square), outline=(0, 200, 0), fill=(255, 255, 255)) 119 | x = x + square + pitch 120 | self.draw.rectangle((x, y, x + square, y + square), outline=1, fill=(0, 0, 200)) 121 | 122 | 123 | self.refresh() 124 | 125 | # Analog Assignments (Tweak, Expression Pedal, etc.) 126 | def draw_analog_assignments(self, controllers): 127 | pass 128 | 129 | def draw_info_message(self, text): 130 | pass 131 | 132 | # Plugins 133 | def draw_plugin_select(self, plugin=None): 134 | pass 135 | 136 | def draw_bound_plugins(self, plugins, footswitches): 137 | pass 138 | 139 | def draw_plugins(self, plugins): 140 | pass 141 | -------------------------------------------------------------------------------- /pistomp/lcdbase.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # This file is part of pi-stomp. 3 | # 4 | # pi-stomp is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # pi-stomp is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with pi-stomp. If not, see . 16 | 17 | import logging 18 | import os 19 | import pistomp.category as Category 20 | import pistomp.lcd as abstract_lcd 21 | 22 | from pistomp.footswitch import Footswitch # TODO would like to avoid this module knowing such details 23 | 24 | 25 | class Lcdbase(abstract_lcd.Lcd): 26 | 27 | def __init__(self, cwd): 28 | 29 | # The following parameters need to be specified by the concrete subclass 30 | 31 | # Fonts 32 | self.title_font = None 33 | self.splash_font = None 34 | self.small_font = None 35 | 36 | # Colors 37 | self.background = None 38 | self.foreground = None 39 | self.highlight = None 40 | self.color_plugin = None 41 | self.color_plugin_bypassed = None 42 | 43 | # Dimensions 44 | self.width = None 45 | self.height = None 46 | self.top = None 47 | self.left = None 48 | self.zone_height = None 49 | self.zone_y = None 50 | self.flip = False 51 | self.footswitch_width = None 52 | self.footswitch_height = None 53 | self.plugin_height = None 54 | self.plugin_width = None 55 | self.plugin_width_medium = None 56 | self.plugin_rect_x_pad = None 57 | self.plugin_bypass_thickness = None 58 | self.plugin_label_length = None 59 | self.footswitch_width = None 60 | self.footswitch_ring_width = None 61 | self.graph_width = None 62 | self.menu_y0 = None 63 | 64 | # Toolbar 65 | self.supports_toolbar = None 66 | self.tools = [] 67 | self.imagedir = os.path.join(cwd, "images") 68 | self.tool_wifi = None 69 | self.tool_eq = None 70 | self.tool_bypass = None 71 | self.tool_system = None 72 | 73 | # Content 74 | self.zones = None 75 | self.zone_height = None 76 | self.images = None 77 | self.draw = None 78 | self.selected_plugin = None 79 | self.selected_box = None # ((x0, y0), (x1, y1), width) 80 | 81 | 82 | # This method verifies that each variable declared above in __init__ gets assigned a value by the object class 83 | # It might flag vars which get assigned a value of None intentionally by the object class 84 | # A better solution might be to create these as abstract properties, but then they are accessed as strings 85 | # which is likely worse 86 | def check_vars_set(self): 87 | known_exceptions = ["selected_plugin", "selected_box", "tool_wifi", "tool_bypass", "tool_system", "tool_eq"] 88 | for v in self.__dict__: 89 | if getattr(self, v) is None: 90 | if v not in known_exceptions: 91 | logging.error("%s class doesn't set variable: %s" % (self, v)) 92 | 93 | def get_plugin_color(self, plugin): 94 | if plugin.category: 95 | return Category.get_category_color(plugin.category) 96 | return "Silver" 97 | 98 | # Convert zone height values to absolute y values considering the flip setting 99 | def calc_zone_y(self): 100 | y_offset = 0 if not self.flip else self.height 101 | for i in range(self.zones): 102 | if self.flip: 103 | y_offset -= (self.zone_height[i]) 104 | if y_offset < 0: 105 | break 106 | else: 107 | if i != 0: 108 | y_offset += (self.zone_height[i-1]) 109 | if y_offset > self.height: 110 | break 111 | self.zone_y[i] = y_offset 112 | 113 | def base_draw_title(self, draw, font, pedalboard, preset, invert_pb, invert_pre, highlight_only=False): 114 | pb_size = font.getsize(pedalboard)[0] 115 | font_height = font.getsize(pedalboard)[1] 116 | x0 = self.left 117 | y = self.top # negative pushes text to top of LCD 118 | highlight_color = self.highlight 119 | fill = highlight_color if highlight_only else self.background 120 | text_color = self.foreground 121 | 122 | # Pedalboard Name 123 | if invert_pb: 124 | draw.rectangle(((x0, y), (pb_size, font_height - 2)), fill, highlight_color) 125 | if highlight_only: 126 | text_color = self.background 127 | draw.text((x0, y), pedalboard, text_color, font) 128 | 129 | if preset != None: 130 | 131 | # delimiter 132 | delimiter = "/" 133 | x = x0 + pb_size + 1 134 | draw.text((x, y), delimiter, self.foreground, font) 135 | 136 | # Preset Name 137 | pre_size = font.getsize(preset)[0] 138 | x = x + font.getsize(delimiter)[0] 139 | x2 = x + pre_size 140 | y2 = font_height 141 | if invert_pre: 142 | draw.rectangle(((x, y), (x2, y2 - 2)), fill, highlight_color) 143 | if highlight_only: 144 | text_color = self.background 145 | draw.text((x, y), preset, text_color, font) 146 | 147 | def base_draw_bound_plugins(self, zone, plugins, footswitches): 148 | bypass_label = "byps" 149 | fss = footswitches.copy() 150 | for p in plugins: 151 | if p.has_footswitch is False: 152 | continue 153 | for c in p.controllers: 154 | if isinstance(c, Footswitch): 155 | fs_id = c.id 156 | fss[fs_id] = None 157 | if c.parameter.symbol != ":bypass": # TODO token 158 | label = c.parameter.name 159 | else: 160 | label = self.shorten_name(p.instance_id, self.footswitch_width) 161 | color = Category.valid_color(c.lcd_color) if c.lcd_color else self.get_plugin_color(p) 162 | x = self.footswitch_pitch[len(fss)] * fs_id 163 | self.draw_plugin(zone, x, 0, label, self.footswitch_width, False, p, True, color) 164 | 165 | # Draw any footswitches which weren't found to be bound to a plugin 166 | for fs_id in range(len(fss)): 167 | if fss[fs_id] is None: 168 | continue 169 | f = fss[fs_id] 170 | color = Category.valid_color(f.lcd_color) 171 | if self.color_plugin_bypassed is not None and not f.enabled: 172 | color = self.color_plugin_bypassed 173 | label = "" if f.display_label is None else f.display_label 174 | x = self.footswitch_pitch[len(fss)] * fs_id 175 | self.draw_plugin(zone, x, 0, label, self.footswitch_width, False, None, True, color) 176 | 177 | def draw_just_a_box(self, draw, xy, xy2, fill=False, color=None, width=1): 178 | if color is None: 179 | color = self.foreground 180 | f = color if fill else None 181 | draw.rectangle((xy, xy2), f, outline=color, width=width) 182 | 183 | def draw_box(self, xy, xy2, zone, text=None, round_bottom_corners=False, fill=False, color=None, width=2): 184 | self.draw_just_a_box(self.draw[zone], xy, xy2, fill, color, width) 185 | #self.draw[zone].point(xy, self.background) # Round the top corners 186 | #self.draw[zone].point((xy2[0],xy[1]), self.background) 187 | #if round_bottom_corners: 188 | # self.draw[zone].point((xy[0],xy2[1])) 189 | # self.draw[zone].point((xy2[0],xy2[1])) 190 | if text: 191 | f = self.background if fill else self.foreground 192 | self.draw[zone].text((xy[0] + 2, xy[1] + 2), text, f, self.small_font) 193 | 194 | def draw_box_outline(self, xy, xy2, zone, color, width=2): 195 | self.draw[zone].line((xy, (xy[0], xy2[1])), color, width) 196 | self.draw[zone].line((xy, (xy2[0], xy[1])), color, width) 197 | self.draw[zone].line((xy2, (xy[0], xy2[1])), color, width) 198 | self.draw[zone].line((xy2, (xy2[0], xy[1])), color, width) 199 | 200 | def erase_all(self): 201 | for z in range(self.zones): 202 | self.erase_zone(z) 203 | for z in range(self.zones): 204 | self.refresh_zone(z) 205 | 206 | def erase_zone(self, zone_idx): 207 | self.images[zone_idx].paste(self.background, (0, 0, self.width, self.zone_height[zone_idx])) 208 | 209 | def shorten_name(self, name, width): 210 | text = "" 211 | for x in name.lower().replace('_', '').replace('/', '').replace(' ', ''): 212 | test = text + x 213 | test_size = self.small_font.getsize(test)[0] 214 | if test_size >= width: 215 | break 216 | text = test 217 | return text 218 | -------------------------------------------------------------------------------- /pistomp/lcdcolor.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import os 17 | import pistomp.category as Category 18 | import pistomp.lcdbase as lcdbase 19 | import common.token as Token 20 | import common.util as util 21 | 22 | class Lcdcolor(lcdbase.Lcdbase): 23 | 24 | def __init__(self, cwd): 25 | super(Lcdcolor, self).__init__(cwd) 26 | 27 | # Menu Screens (uses deep_edit image and draw objects) 28 | def menu_show(self, page_title, menu_items): 29 | pass 30 | 31 | def menu_highlight(self, index): 32 | pass 33 | 34 | # Parameter Value Edit 35 | def draw_value_edit(self, plugin_name, parameter, value): 36 | self.draw_title(plugin_name, None, False, False, False) 37 | self.draw_value_edit_graph(parameter, value) 38 | 39 | def draw_value_edit_graph(self, parameter, value): 40 | # TODO super inefficient here redrawing the whole image every time the value changes 41 | self.draw_title(parameter.name, None, False, False, False) 42 | self.menu_image.paste(0, (0, 0, self.width, self.menu_image_height)) 43 | 44 | y0 = self.menu_y0 45 | y1 = y0 - 2 46 | ytext = y0 // 2 47 | x = 0 48 | xpitch = 4 49 | 50 | # The current value text 51 | self.menu_draw.text((0, ytext), "%s" % util.format_float(value), self.foreground, self.title_font) 52 | 53 | val = util.renormalize(value, parameter.minimum, parameter.maximum, 0, self.graph_width) 54 | yref = y1 55 | while x < self.graph_width: 56 | self.menu_draw.line(((x + 2, y0), (x + 2, yref)), self.color_plugin, 1) 57 | if (x < val) and (x % xpitch) == 0: 58 | self.menu_draw.rectangle(((x, y0), (x + 2, y1)), self.highlight, 2) 59 | y1 = y1 - 1 60 | x = x + xpitch 61 | yref = yref - 1 62 | 63 | self.menu_draw.text((0, self.menu_y0 + 4), "%d" % parameter.minimum, self.foreground, self.small_font) 64 | self.menu_draw.text((self.graph_width - (len(str(parameter.maximum)) * 4), self.menu_y0 + 4), 65 | "%d" % parameter.maximum, self.foreground, self.small_font) 66 | self.refresh_menu() 67 | self.draw_info_message("Click to exit") 68 | 69 | def update_wifi(self, wifi_status): 70 | if not self.supports_toolbar: 71 | return 72 | if util.DICT_GET(wifi_status, 'hotspot_active'): 73 | img = "wifi_orange.png" 74 | elif util.DICT_GET(wifi_status, 'wifi_connected'): 75 | img = "wifi_silver.png" 76 | else: 77 | img = "wifi_gray.png" 78 | path = os.path.join(self.imagedir, img) 79 | self.change_tool_img(self.tool_wifi, path) 80 | 81 | def update_eq(self, eq_status): 82 | if not self.supports_toolbar: 83 | return 84 | img = "eq_blue.png" if eq_status else "eq_gray.png" 85 | path = os.path.join(self.imagedir, img) 86 | self.change_tool_img(self.tool_eq, path) 87 | 88 | def update_bypass(self, bypass): 89 | if not self.supports_toolbar: 90 | return 91 | img = "power_green.png" if bypass else "power_gray.png" 92 | path = os.path.join(self.imagedir, img) 93 | self.change_tool_img(self.tool_bypass, path) 94 | 95 | def change_tool_img(self, tool, img_path): 96 | if not self.supports_toolbar: 97 | return 98 | tool.update_img(img_path) 99 | self.images[self.ZONE_TOOLS].paste(tool.image, (tool.x, tool.y)) 100 | self.refresh_zone(self.ZONE_TOOLS) 101 | 102 | def clear_select(self): 103 | if self.selected_box: 104 | self.draw_box_outline(self.selected_box[0], self.selected_box[1], self.ZONE_TOOLS, 105 | color=self.background, width=self.selected_box[2]) 106 | self.refresh_zone(self.ZONE_TOOLS) 107 | self.selected_box = None 108 | 109 | def draw_title(self, pedalboard, preset, invert_pb, invert_pre, highlight_only=False): 110 | zone = self.ZONE_TITLE 111 | self.erase_zone(zone) # TODO to avoid redraw of entire zone, could we just redraw what changed? 112 | self.base_draw_title(self.draw[zone], self.title_font, pedalboard, preset, invert_pb, invert_pre, 113 | highlight_only) 114 | self.refresh_zone(zone) 115 | 116 | # Zone 1 - Analog Assignments (Tweak, Expression Pedal, etc.) 117 | def draw_knob(self, text, x, color="gray"): 118 | zone = self.ZONE_ASSIGNMENTS 119 | self.draw[zone].ellipse(((x, 3), (x + 14, 17)), self.background, color, 2) 120 | self.draw[zone].line(((x + 12, 5), (x + 7, 10)), color, 2) 121 | self.draw[zone].text((x + 19, 1), text, self.foreground, self.tiny_font) 122 | 123 | def draw_pedal(self, text, x, color="gray"): 124 | zone = self.ZONE_ASSIGNMENTS 125 | self.draw[zone].line(((x, 14), (x + 13, 4)), color, 2) 126 | self.draw[zone].line(((x, 14), (x + 14, 14)), color, 4) 127 | self.draw[zone].text((x + 19, 1), text, self.foreground, self.tiny_font) 128 | 129 | def draw_analog_assignments(self, controllers): 130 | zone = self.ZONE_ASSIGNMENTS 131 | self.erase_zone(zone) 132 | 133 | # spacing and scaling of text 134 | width_per_control = self.width 135 | text_per_control = self.width 136 | num = len(controllers) 137 | if num > 0: 138 | width_per_control = int(round(self.width / num)) 139 | text_per_control = width_per_control - 16 # minus width of control icon 140 | 141 | x = 0 142 | for k, v in controllers.items(): 143 | control_type = util.DICT_GET(v, Token.TYPE) 144 | color = util.DICT_GET(v, Token.COLOR) 145 | if color is None: 146 | # color not specified for control in config file 147 | category = util.DICT_GET(v, Token.CATEGORY) 148 | color = Category.get_category_color(category) 149 | name = k.split(":")[1] 150 | n = self.shorten_name(name, text_per_control) 151 | if control_type == Token.KNOB: 152 | self.draw_knob(n, x, color) 153 | if control_type == Token.EXPRESSION: 154 | self.draw_pedal(n, x, color) 155 | x += width_per_control 156 | 157 | self.refresh_zone(zone) 158 | 159 | def draw_info_message(self, text): 160 | zone = self.ZONE_TOOLS 161 | self.erase_zone(zone) 162 | self.draw[zone].text((0, 0), text, self.foreground, self.tiny_font) 163 | self.refresh_zone(zone) 164 | 165 | # Plugins 166 | def draw_plugin_select(self, plugin=None): 167 | width = 2 168 | # First unselect currently selected 169 | if self.selected_plugin: 170 | x0 = self.selected_plugin.lcd_xyz[0][0] - 3 171 | y0 = self.selected_plugin.lcd_xyz[0][1] - 3 172 | x1 = self.selected_plugin.lcd_xyz[1][0] + 3 173 | y1 = self.selected_plugin.lcd_xyz[1][1] + 3 174 | c = self.background # if self.selected_plugin.has_footswitch else self.get_plugin_color(self.selected_plugin) 175 | self.draw_box_outline((x0, y0), (x1, y1), self.selected_plugin.lcd_xyz[2], color=c, width=width) 176 | self.refresh_zone(self.selected_plugin.lcd_xyz[2]) 177 | 178 | if plugin is not None: 179 | # Highlight new selection 180 | x0 = plugin.lcd_xyz[0][0] - 3 181 | y0 = plugin.lcd_xyz[0][1] - 3 182 | x1 = plugin.lcd_xyz[1][0] + 3 183 | y1 = plugin.lcd_xyz[1][1] + 3 184 | self.draw_box_outline((x0, y0), (x1, y1), plugin.lcd_xyz[2], color=self.highlight, width=width) 185 | self.refresh_zone(plugin.lcd_xyz[2]) 186 | self.selected_plugin = plugin 187 | 188 | def draw_bound_plugins(self, plugins, footswitches): 189 | zone = self.ZONE_FOOTSWITCHES 190 | self.erase_zone(zone) # necessary when changing pedalboards with different switch assignments 191 | self.base_draw_bound_plugins(zone, plugins, footswitches) 192 | self.refresh_zone(zone) 193 | 194 | def draw_footswitch(self, xy1, xy2, zone, text, color): 195 | # implement in display class 196 | pass 197 | 198 | def draw_plugins(self, plugins): 199 | y = self.top + 3 200 | x = self.left 201 | xwrap = self.width - self.plugin_width # scroll if exceeds this width 202 | ymax = 64 # Maximum y for plugin LCD zone 203 | zone = self.ZONE_PLUGINS1 204 | self.erase_zone(self.ZONE_PLUGINS1) 205 | self.erase_zone(self.ZONE_PLUGINS2) 206 | self.erase_zone(self.ZONE_PLUGINS3) 207 | 208 | count = 0 209 | for p in plugins: 210 | if not p.has_footswitch: 211 | count = count + 1 212 | width = self.plugin_width_medium if count <= 8 else self.plugin_width 213 | 214 | count = 0 215 | eol = False 216 | for p in plugins: 217 | if p.has_footswitch: 218 | continue 219 | label = p.instance_id.replace('/', "")[:self.plugin_label_length] 220 | label = label.replace("_", "") 221 | count += 1 222 | if count > 4: 223 | eol = True 224 | count = 0 225 | x = self.draw_plugin(zone, x, y, label, width, eol, p) 226 | eol = False 227 | x = x + self.plugin_rect_x_pad 228 | if x > xwrap: 229 | zone += 1 230 | x = self.left 231 | if y >= ymax: 232 | break # Only display 2 rows, huge pedalboards won't fully render # TODO make sure this works 233 | self.refresh_plugins() 234 | 235 | def draw_plugin(self, zone, x, y, text, width, eol, plugin, is_footswitch=False, color=0): 236 | text = self.shorten_name(text, width) 237 | 238 | y2 = y + (self.footswitch_height if is_footswitch else self.plugin_height) 239 | x2 = x + width 240 | if eol: 241 | x2 = x2 - 1 242 | xy1 = (x, y) 243 | xy2 = (x2, y2) 244 | 245 | if is_footswitch: 246 | if plugin: 247 | plugin.lcd_xyz = (xy1, xy2, zone) 248 | c = self.color_plugin_bypassed if plugin is not None and plugin.is_bypassed() else color 249 | self.draw_footswitch(xy1, xy2, zone, text, c) 250 | elif plugin: 251 | plugin.lcd_xyz = (xy1, xy2, zone) 252 | self.draw_box(xy1, xy2, zone, text, is_footswitch, not plugin.is_bypassed(), self.get_plugin_color(plugin)) 253 | 254 | return x2 255 | -------------------------------------------------------------------------------- /pistomp/lcdsy7789.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | from abc import ABC, abstractmethod 17 | 18 | import board 19 | #from board import SCL, SDA 20 | import busio 21 | import digitalio 22 | from PIL import Image, ImageDraw, ImageFont 23 | import adafruit_rgb_display.st7789 as st7789 24 | 25 | import ST7789 26 | 27 | class Lcd(ABC): 28 | 29 | def __init__(self, cwd): 30 | 31 | # Create ST7789 LCD display class. 32 | self.disp = ST7789.ST7789( 33 | port=0, 34 | cs=ST7789.BG_SPI_CS_BACK, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT 35 | dc=1, 36 | backlight=18, # 18 for back BG slot, 19 for front BG slot. 37 | width=240, 38 | height=135, 39 | rotation=0, 40 | spi_speed_hz=80 * 1000 * 1000 41 | ) 42 | 43 | # Create blank image for drawing. 44 | # Make sure to create image with mode '1' for 1-bit color. 45 | self.width = self.disp.width 46 | self.height = self.disp.height 47 | 48 | padding = 50 49 | self.top = padding 50 | self.bottom = self.height - padding 51 | self.left = padding 52 | self.image = Image.new("RGB", (self.height, self.width)) 53 | 54 | # Get drawing object to draw on image. 55 | self.draw = ImageDraw.Draw(self.image) 56 | 57 | # Draw a black filled box to clear the image. 58 | self.draw.rectangle((0, 0, self.height, self.width), outline=0, fill=0) 59 | 60 | # Font 61 | self.font_size = 26 62 | self.font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', self.font_size) 63 | self.splash_font_size = 40 64 | self.splash_font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', self.splash_font_size) 65 | 66 | # Turn on the backlight 67 | backlight = digitalio.DigitalInOut(board.D22) 68 | backlight.switch_to_output() 69 | backlight.value = True 70 | self.splash_show() 71 | 72 | def refresh(self): 73 | self.disp.display(self.image) 74 | 75 | def splash_show(self, boot=True): 76 | self.clear() 77 | self.draw.text((self.left + 10, self.top + 70), "pi Stomp!", font=self.splash_font, fill=255) 78 | self.refresh() 79 | 80 | def cleanup(self): 81 | self.clear() 82 | 83 | def clear(self): 84 | self.draw.rectangle((0, 0, self.height, self.width), outline=0, fill=(0, 0, 0)) 85 | self.disp.display(self.image) 86 | 87 | # Menu Screens (uses deep_edit image and draw objects) 88 | def menu_show(self, page_title, menu_items): 89 | pass 90 | 91 | def menu_highlight(self, index): 92 | pass 93 | 94 | # Parameter Value Edit 95 | def draw_value_edit(self, plugin_name, parameter, value): 96 | pass 97 | 98 | def draw_value_edit_graph(self, parameter, value): 99 | pass 100 | 101 | def draw_title(self, pedalboard, preset, invert_pb, invert_pre): 102 | x = 0 103 | self.clear() 104 | self.draw.text((x, self.top), pedalboard, font=self.font, fill=255) 105 | self.draw.text((x, self.top + self.font_size), preset, font=self.font, fill=255) 106 | self.refresh() 107 | 108 | # Analog Assignments (Tweak, Expression Pedal, etc.) 109 | def draw_analog_assignments(self, controllers): 110 | pass 111 | 112 | def draw_info_message(self, text): 113 | pass 114 | 115 | # Plugins 116 | def draw_plugin_select(self, plugin=None): 117 | pass 118 | 119 | def draw_bound_plugins(self, plugins, footswitches): 120 | pass 121 | 122 | def draw_plugins(self, plugins): 123 | pass 124 | -------------------------------------------------------------------------------- /pistomp/ledstrip.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import _rpi_ws281x as ws 17 | from rpi_ws281x import PixelStrip 18 | import matplotlib 19 | from PIL import ImageColor 20 | 21 | import pistomp.category as Category 22 | 23 | # LED strip configuration: # TODO get these from hardware impl (pisompcore.py) 24 | LED_COUNT = 4 # Number of LED pixels. 25 | LED_PIN = 13 # GPIO pin connected to the pixels (must have PWM). 26 | LED_FREQ_HZ = 800000 # LED signal frequency in hertz (usually 800khz) 27 | LED_DMA = 12 # DMA channel to use for generating signal (try 10) # TODO XXX need to figure this out 28 | LED_BRIGHTNESS = 30 # Set to 0 for darkest and 255 for brightest 29 | LED_INVERT = False # True to invert the signal (when using NPN transistor level shift) 30 | LED_CHANNEL = 1 # set to '1' for GPIOs 13, 19, 41, 45 or 53 31 | 32 | class Ledstrip: 33 | 34 | def __init__(self): 35 | # Create NeoPixel object with appropriate configuration. 36 | self.strip = PixelStrip(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL, 37 | strip_type=ws.WS2811_STRIP_RGB) 38 | # Intialize the library (must be called once before other functions). 39 | self.strip.begin() 40 | 41 | self.pixels = [] 42 | 43 | def add_pixel(self, id, position): 44 | p = Pixel(self.strip, id, position) 45 | self.pixels.append(p) 46 | return p 47 | 48 | def get_gpio(self): 49 | return LED_PIN 50 | 51 | 52 | class Pixel: 53 | def __init__(self, strip, id, position): 54 | self.strip = strip 55 | self.id = id 56 | self.position = position 57 | self.color = (0, 0, 0) 58 | 59 | # set the color for the pixel based on category, then render based on enabled status 60 | def set_color_by_category(self, category, enabled): 61 | print(category, enabled) 62 | self._set_color(Category.get_category_color(category)) 63 | self.set_enable(enabled) 64 | 65 | # render based on enable 66 | def set_enable(self, enable): 67 | if enable and self.color: 68 | self._render_color_rgb(self.color[0], self.color[1], self.color[2]) 69 | else: 70 | self._render_color_rgb(0, 0, 0) 71 | 72 | # set the color for the pixel based on the name or rgb 73 | def _set_color(self, color): 74 | try: 75 | c = matplotlib.colors.cnames[color] 76 | c = ImageColor.getcolor(c, "RGB") 77 | except: 78 | c = color 79 | if c is None: 80 | c = (0, 0, 0) 81 | self.color = c 82 | 83 | def _render_color_rgb(self, r, g, b): 84 | self.strip.setPixelColorRGB(self.position, r, g, b) 85 | # TODO would be nice to do this once for multiple pixel changes 86 | self.strip.show() 87 | -------------------------------------------------------------------------------- /pistomp/pistomp.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | # This subclass defines hardware specific to pi-stomp! v1 17 | # 3 Footswitches 18 | # 1 Analog Pot 19 | # 1 Expression Pedal 20 | # 2 Encoders with switches 21 | # 22 | # A new version with different controls should have a new separate subclass 23 | 24 | import RPi.GPIO as GPIO 25 | 26 | from pathlib import Path 27 | import pistomp.analogmidicontrol as AnalogMidiControl 28 | import pistomp.analogswitch as AnalogSwitch 29 | import pistomp.encoder as Encoder 30 | import pistomp.footswitch as Footswitch 31 | import pistomp.hardware as hardware 32 | import pistomp.relay as Relay 33 | 34 | import pistomp.lcdgfx as Lcd 35 | 36 | import sys 37 | import time 38 | 39 | # Pins (Unless the hardware has been changed, these should not be altered) 40 | TOP_ENC_PIN_D = 17 41 | TOP_ENC_PIN_CLK = 4 42 | TOP_ENC_SWITCH_CHANNEL = 7 43 | BOT_ENC_PIN_D = 22 44 | BOT_ENC_PIN_CLK = 27 45 | BOT_ENC_SWITCH_CHANNEL = 6 46 | ENC_SW_THRESHOLD = 512 47 | 48 | RELAY_RESET_PIN = 16 49 | RELAY_SET_PIN = 12 50 | 51 | # Each footswitch defined by a quad touple: 52 | # 1: id (left = 0, mid = 1, right = 2) 53 | # 2: the GPIO pin it's attached to 54 | # 3: the associated LED output pin and 55 | # 4: the MIDI Control (CC) message that will be sent when the switch is toggled 56 | # Pin modifications should only be made if the hardware is changed accordingly 57 | FOOTSW = [(0, 23, 24, 61), (1, 25, 0, 62), (2, 13, 26, 63)] 58 | 59 | # TODO replace in default_config.yml 60 | # Analog Controls defined by a triple touple: 61 | # 1: the ADC channel 62 | # 2: the minimum threshold for considering the value to be changed 63 | # 3: the MIDI Control (CC) message that will be sent 64 | # 4: control type (KNOB, EXPRESSION, etc.) 65 | # Tweak, Expression Pedal 66 | ANALOG_CONTROL = [(0, 16, 64, 'KNOB'), (1, 16, 65, 'EXPRESSION')] 67 | 68 | class Pistomp(hardware.Hardware): 69 | __single = None 70 | 71 | def __init__(self, cfg, mod, midiout, refresh_callback): 72 | super(Pistomp, self).__init__(cfg, mod, midiout, refresh_callback) 73 | if Pistomp.__single: 74 | raise Pistomp.__single 75 | Pistomp.__single = self 76 | 77 | self.mod = mod 78 | self.midiout = midiout 79 | 80 | GPIO.setmode(GPIO.BCM) 81 | 82 | self.init_spi() 83 | 84 | self.init_lcd() 85 | 86 | self.run_test() 87 | 88 | self.init_relays() 89 | 90 | self.init_footswitches() 91 | 92 | self.init_analog_controls() 93 | 94 | self.init_encoders() 95 | 96 | def init_lcd(self): 97 | self.mod.add_lcd(Lcd.Lcd(self.mod.homedir)) 98 | 99 | def init_analog_controls(self): 100 | for c in ANALOG_CONTROL: 101 | control = AnalogMidiControl.AnalogMidiControl(self.spi, c[0], c[1], c[2], self.midi_channel, 102 | self.midiout, c[3]) 103 | self.analog_controls.append(control) 104 | key = format("%d:%d" % (self.midi_channel, c[2])) 105 | self.controllers[key] = control # Controller.Controller(self.midi_channel, c[1], Controller.Type.ANALOG) 106 | 107 | def init_encoders(self): 108 | top_enc = Encoder.Encoder(TOP_ENC_PIN_D, TOP_ENC_PIN_CLK, callback=self.mod.top_encoder_select) 109 | self.encoders.append(top_enc) 110 | bot_enc = Encoder.Encoder(BOT_ENC_PIN_D, BOT_ENC_PIN_CLK, callback=self.mod.bot_encoder_select) 111 | self.encoders.append(bot_enc) 112 | control = AnalogSwitch.AnalogSwitch(self.spi, TOP_ENC_SWITCH_CHANNEL, ENC_SW_THRESHOLD, 113 | callback=self.mod.top_encoder_sw) 114 | self.analog_controls.append(control) 115 | control = AnalogSwitch.AnalogSwitch(self.spi, BOT_ENC_SWITCH_CHANNEL, ENC_SW_THRESHOLD, 116 | callback=self.mod.bottom_encoder_sw) 117 | self.analog_controls.append(control) 118 | 119 | def init_footswitches(self): 120 | for f in FOOTSW: 121 | fs = Footswitch.Footswitch(f[0], f[1], f[2], None, f[3], self.midi_channel, self.midiout, 122 | refresh_callback=self.refresh_callback) 123 | self.footswitches.append(fs) 124 | self.reinit(None) 125 | 126 | def init_relays(self): 127 | self.relay = Relay.Relay(RELAY_SET_PIN, RELAY_RESET_PIN) 128 | 129 | # Test procedure for verifying hardware controls 130 | def test(self): 131 | self.mod.lcd.erase_all() 132 | self.mod.lcd.draw_title("Hardware test...", None, False, False) 133 | failed = 0 134 | 135 | try: 136 | GPIO.setmode(GPIO.BCM) 137 | 138 | # TODO kinda lame that the instantiations of hardware objects here must match those in __init__ 139 | # except with different callbacks 140 | 141 | # Footswitches 142 | for f in FOOTSW: 143 | self.mod.lcd.draw_info_message("Press Footswitch %d" % int(f[0] + 1)) 144 | fs = Footswitch.Footswitch(f[0], f[1], f[2], None, f[3], self.midi_channel, self.midiout, 145 | refresh_callback=self.test_passed) 146 | self.test_pass = False 147 | timeout = 1000 # 10 seconds 148 | initial_value = GPIO.input(f[2]) 149 | while self.test_pass is False and timeout > 0: 150 | fs.poll() 151 | new_value = GPIO.input(f[2]) # Verify that LED pin toggles 152 | if new_value is not initial_value: 153 | break 154 | time.sleep(0.01) 155 | timeout = timeout - 1 156 | del fs 157 | if timeout > 0: 158 | self.mod.lcd.draw_info_message("Passed") 159 | else: 160 | self.mod.lcd.draw_info_message("Failed") 161 | failed = failed + 1 162 | time.sleep(1.2) 163 | 164 | # Encoder rotary 165 | encoders = [["Turn the PBoard Knob", TOP_ENC_PIN_D, TOP_ENC_PIN_CLK], 166 | ["Turn the Effect Knob", BOT_ENC_PIN_D, BOT_ENC_PIN_CLK]] 167 | for e in encoders: 168 | enc = Encoder.Encoder(e[1], e[2], callback=self.test_passed) 169 | self.mod.lcd.draw_info_message(e[0]) 170 | self.test_pass = False 171 | timeout = 1000 172 | while self.test_pass is False and timeout > 0: 173 | enc.read_rotary() 174 | time.sleep(0.01) 175 | timeout = timeout - 1 176 | del enc 177 | if timeout > 0: 178 | self.mod.lcd.draw_info_message("Passed") 179 | else: 180 | self.mod.lcd.draw_info_message("Failed") 181 | failed = failed + 1 182 | time.sleep(1.2) 183 | 184 | # Encoder switches 185 | encoders = [["Press the PBoard Knob", TOP_ENC_SWITCH_CHANNEL], 186 | ["Press the Effect Knob", BOT_ENC_SWITCH_CHANNEL]] 187 | for e in encoders: 188 | enc = AnalogSwitch.AnalogSwitch(self.spi, e[1], ENC_SW_THRESHOLD, callback=self.test_passed) 189 | self.mod.lcd.draw_info_message(e[0]) 190 | self.test_pass = False 191 | timeout = 1000 192 | while self.test_pass is False and timeout > 0: 193 | enc.refresh() 194 | time.sleep(0.01) 195 | timeout = timeout - 1 196 | del enc 197 | if timeout > 0: 198 | self.mod.lcd.draw_info_message("Passed") 199 | else: 200 | self.mod.lcd.draw_info_message("Failed") 201 | failed = failed + 1 202 | time.sleep(1.2) 203 | 204 | # Analog Knobs 205 | self.mod.lcd.draw_info_message("Turn the Tweak knob") 206 | c = ANALOG_CONTROL[0] 207 | control = AnalogMidiControl.AnalogMidiControl(self.spi, c[0], c[1], c[2], self.midi_channel, 208 | self.midiout, c[3]) 209 | self.test_pass = False 210 | timeout = 1000 211 | initial_value = control.readChannel() 212 | while self.test_pass is False and timeout > 0: 213 | time.sleep(0.01) 214 | pot_adjust = abs(control.readChannel() - initial_value) 215 | if pot_adjust > c[1]: 216 | break 217 | timeout = timeout - 1 218 | del control 219 | if timeout > 0: 220 | self.mod.lcd.draw_info_message("Passed") 221 | else: 222 | self.mod.lcd.draw_info_message("Failed") 223 | failed = failed + 1 224 | time.sleep(1.2) 225 | 226 | if failed > 0: 227 | self.mod.lcd.draw_info_message("%d control(s) failed" % failed) 228 | time.sleep(3) 229 | else: 230 | # create sentinel file so test procedure is skipped next boot 231 | f = Path(self.test_sentinel) 232 | f.touch() 233 | self.mod.lcd.draw_info_message("Restarting...") 234 | time.sleep(1.2) 235 | 236 | except KeyboardInterrupt: 237 | return 238 | 239 | finally: 240 | self.mod.lcd.cleanup() 241 | GPIO.cleanup() 242 | sys.exit() 243 | 244 | def test_passed(self, data = None): 245 | self.test_pass = True 246 | -------------------------------------------------------------------------------- /pistomp/pistompcore.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | # This subclass defines hardware specific to pi-Stomp Core 17 | # 3 Footswitches 18 | # 1 Analog Pot 19 | # 1 Expression Pedal 20 | # 2 Encoders with switches 21 | # 22 | # A new version with different controls should have a new separate subclass 23 | 24 | import RPi.GPIO as GPIO 25 | 26 | import common.token as Token 27 | import common.util as Util 28 | 29 | import pistomp.analogmidicontrol as AnalogMidiControl 30 | import pistomp.encoder as Encoder 31 | import pistomp.encoderswitch as EncoderSwitch 32 | import pistomp.footswitch as Footswitch 33 | import pistomp.hardware as hardware 34 | import pistomp.relay as Relay 35 | 36 | import pistomp.lcdili9341 as Lcd 37 | #import pistomp.lcd128x64 as Lcd 38 | #import pistomp.lcd135x240 as Lcd 39 | #import pistomp.lcdsy7789 as Lcd 40 | 41 | # Pins (Unless the hardware has been changed, these should not be altered) 42 | TOP_ENC_PIN_D = 17 43 | TOP_ENC_PIN_CLK = 4 44 | TOP_ENC_SWITCH_CHANNEL = 7 45 | ENC_SW_THRESHOLD = 512 46 | 47 | RELAY_RESET_PIN = 16 48 | RELAY_SET_PIN = 12 49 | 50 | # Map of Debounce chip pin (user friendly) to GPIO (code friendly) 51 | DEBOUNCE_MAP = {0: 27, 1: 23, 2: 22, 3: 24, 4: 25} 52 | 53 | 54 | class Pistompcore(hardware.Hardware): 55 | __single = None 56 | 57 | def __init__(self, cfg, mod, midiout, refresh_callback): 58 | super(Pistompcore, self).__init__(cfg, mod, midiout, refresh_callback) 59 | if Pistompcore.__single: 60 | raise Pistompcore.__single 61 | Pistompcore.__single = self 62 | 63 | self.mod = mod 64 | self.midiout = midiout 65 | self.debounce_map = DEBOUNCE_MAP 66 | 67 | GPIO.setmode(GPIO.BCM) 68 | 69 | self.init_spi() 70 | 71 | self.init_lcd() 72 | 73 | self.init_relays() 74 | 75 | self.init_encoders() 76 | 77 | self.init_footswitches() 78 | 79 | self.init_analog_controls() 80 | 81 | self.reinit(None) 82 | 83 | def init_lcd(self): 84 | self.mod.add_lcd(Lcd.Lcd(self.mod.homedir)) 85 | 86 | def init_encoders(self): 87 | top_enc = Encoder.Encoder(TOP_ENC_PIN_D, TOP_ENC_PIN_CLK, callback=self.mod.universal_encoder_select) 88 | self.encoders.append(top_enc) 89 | enc_sw = EncoderSwitch.EncoderSwitch(1, callback=self.mod.universal_encoder_sw) 90 | self.encoder_switches.append(enc_sw) 91 | 92 | def init_relays(self): 93 | self.relay = Relay.Relay(RELAY_SET_PIN, RELAY_RESET_PIN) 94 | 95 | def init_analog_controls(self): 96 | cfg = self.default_cfg.copy() 97 | if len(self.analog_controls) == 0: 98 | self.create_analog_controls(cfg) 99 | 100 | def init_footswitches(self): 101 | cfg = self.default_cfg.copy() 102 | if len(self.footswitches) == 0: 103 | self.create_footswitches(cfg) 104 | -------------------------------------------------------------------------------- /pistomp/relay.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import logging 17 | import os 18 | from pathlib import Path 19 | import RPi.GPIO as GPIO 20 | import shutil 21 | import time 22 | 23 | 24 | class Relay: 25 | 26 | def __init__(self, set_pin, reset_pin): 27 | self.enabled = False 28 | self.set_pin = set_pin 29 | self.reset_pin = reset_pin 30 | 31 | # The existence of this file indicates that the pi-stomp should be true-bypassed 32 | # Non-existence indicates the pi-stomp should process audio 33 | self.sentinel_file = os.path.join(os.path.expanduser("~"), ".relay_bypass%d" % set_pin) 34 | 35 | GPIO.setup(reset_pin, GPIO.OUT) 36 | GPIO.output(reset_pin, GPIO.LOW) 37 | GPIO.setup(set_pin, GPIO.OUT) 38 | GPIO.output(set_pin, GPIO.LOW) 39 | 40 | def init_state(self): 41 | bypass = os.path.isfile(self.sentinel_file) 42 | if bypass: 43 | self.disable() 44 | else: 45 | self.enable() 46 | return not bypass 47 | 48 | def enable(self): 49 | GPIO.output(self.set_pin, GPIO.HIGH) 50 | time.sleep(0.04) 51 | self.enabled = True 52 | GPIO.output(self.set_pin, GPIO.LOW) 53 | logging.debug("Relay on: %d" % self.set_pin) 54 | 55 | if os.path.isfile(self.sentinel_file): 56 | os.remove(self.sentinel_file) 57 | 58 | def disable(self): 59 | GPIO.output(self.reset_pin, GPIO.HIGH) 60 | time.sleep(0.04) 61 | self.enabled = False 62 | GPIO.output(self.reset_pin, GPIO.LOW) 63 | logging.debug("Relay off: %d" % self.reset_pin) 64 | 65 | f = Path(self.sentinel_file) 66 | f.touch() 67 | shutil.chown(f, user="pistomp", group=None) 68 | 69 | -------------------------------------------------------------------------------- /pistomp/relaynonlatching.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | import logging 17 | import RPi.GPIO as GPIO 18 | 19 | import pistomp.relay as relay 20 | 21 | 22 | class Relay(relay.Relay): 23 | 24 | def __init__(self, set_pin, reset_pin): 25 | self.enabled = False 26 | self.set_pin = set_pin 27 | GPIO.setup(set_pin, GPIO.OUT) 28 | GPIO.output(set_pin, GPIO.LOW) 29 | 30 | def enable(self): 31 | self.enabled = True 32 | GPIO.output(self.set_pin, self.enabled) 33 | logging.debug("Relay on: %d" % self.set_pin) 34 | 35 | def disable(self): 36 | self.enabled = False 37 | GPIO.output(self.set_pin, self.enabled) 38 | logging.debug("Relay off: %d" % self.set_pin) 39 | -------------------------------------------------------------------------------- /pistomp/tool.py: -------------------------------------------------------------------------------- 1 | # This file is part of pi-stomp. 2 | # 3 | # pi-stomp is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # pi-stomp is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with pi-stomp. If not, see . 15 | 16 | from PIL import Image 17 | 18 | 19 | class Tool: 20 | 21 | def __init__(self, tool_type, x, y, img_path = None): 22 | self.tool_type = tool_type 23 | self.x = x 24 | self.y = y 25 | self.image = Image.open(img_path) if img_path else None 26 | 27 | def update_img(self, img_path): 28 | self.image = Image.open(img_path) 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | 19 | # 20 | # Usage and options 21 | # 22 | usage() 23 | { 24 | echo "Usage: $(basename $0) [-a ] [-v ] [-p] [-m]" 25 | echo "" 26 | echo "Options:" 27 | echo " -a Specify audio card (audioinjector-wm8731-audio | iqaudio-codec | hifiberry-dacplusadc)" 28 | echo " -v Specify hardware version" 29 | echo " 1.0 : original pi-Stomp hardware (PCB v1)" 30 | echo " 2.0 : most hardware (default)" 31 | echo " -p Do not install default plugins and pedalboards" 32 | echo " -m Enable MIDI via UART" 33 | echo " -h Display this message" 34 | } 35 | 36 | hardware_version=2.0 37 | has_ttymidi=false 38 | plugins=true 39 | 40 | while getopts 'a:v:pmh' o; do 41 | case "${o}" in 42 | a) 43 | audio_card=${OPTARG} 44 | ;; 45 | v) 46 | hardware_version=${OPTARG} 47 | ;; 48 | p) 49 | plugins=false 50 | ;; 51 | m) 52 | has_ttymidi=true 53 | ;; 54 | h) 55 | usage 56 | exit 0 57 | ;; 58 | *) 59 | usage 1>&2 60 | exit 1 61 | ;; 62 | esac 63 | done 64 | 65 | export has_ttymidi 66 | 67 | # 68 | # Hardware specific 69 | # 70 | if [ -z ${hardware_version+x} ]; then 71 | printf "\nUsing default hardware configuration\n"; 72 | else 73 | printf "\n===== pi-Stomp mods for hardware version specified =====\n" 74 | ${HOME}/pi-stomp/setup/pi-stomp-tweaks/modify_version.sh ${hardware_version} 75 | fi 76 | 77 | printf "\n===== OS update =====\n" 78 | sudo apt-get update -y --allow-releaseinfo-change --fix-missing 79 | 80 | printf "\n===== Audio card setup =====\n" 81 | setup/audio/audiocard-setup.sh 82 | if [ ! -z ${audio_card+x} ]; then 83 | util/change-audio-card.sh ${audio_card} || (usage; exit 1) 84 | fi 85 | 86 | printf "\n===== Mod software install =====\n" 87 | setup/mod/install.sh 88 | 89 | printf "\n===== Mod software tweaks =====\n" 90 | setup/mod-tweaks/mod-tweaks.sh 91 | 92 | printf "\n===== Install pi-stomp package dependencies =====\n" 93 | setup/pkgs/simple_install.sh 94 | setup/pkgs/lilv_install.sh 95 | setup/pkgs/mod-ttymidi_install.sh 96 | if awk "BEGIN {exit !($hardware_version < 2.0)}"; then 97 | printf "\n===== GFX HAT LCD support install =====\n" 98 | setup/pkgs/gfxhat_install.sh 99 | fi 100 | 101 | if [[ $plugins == true ]]; then 102 | printf "\n===== Get extra plugins =====\n" 103 | setup/plugins/get_plugins.sh 104 | 105 | printf "\n===== Get example pedalboards =====\n" 106 | setup/pedalboards/get_pedalboards.sh 107 | fi 108 | 109 | printf "\n===== System tweaks =====\n" 110 | setup/sys/config_tweaks.sh 111 | cp setup/sys/bash_aliases ~/.bash_aliases 112 | 113 | printf "\n===== Manage services =====\n" 114 | setup/services/create_services.sh 115 | 116 | printf "\n===== RT Kernel Install =====\n" 117 | setup/sys/rtkernel.sh 118 | 119 | printf "\n===== pi-stomp setup complete - rebooting =====\n" 120 | sudo reboot now 121 | -------------------------------------------------------------------------------- /setup/.install_packages.sh.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/setup/.install_packages.sh.swp -------------------------------------------------------------------------------- /setup/audio/audiocard-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | # check the device tree overlay is setup correctly ... 19 | # firstly disable PWM audio 20 | sudo bash -c "sed -i \"s/^\s*dtparam=audio/#dtparam=audio/\" /boot/config.txt" 21 | 22 | # add alsa restore to rc.local 23 | sudo patch -b -N -u /etc/rc.local -i setup/audio/rclocal.diff 24 | 25 | # append lines to config.txt 26 | cnt=$(grep -c "dtoverlay=audioinjector-wm8731-audio" /boot/config.txt) 27 | if [[ "$cnt" -eq "0" ]]; then 28 | sudo bash -c "cat >> /boot/config.txt <) 4 | 5 | --- 6 | hardware: 7 | # Hardware version (1.0 for original pi-Stomp, 2.0 for pi-Stomp Core) 8 | version: 2.0 9 | 10 | # midi definition 11 | # channel: midi channel used for midi messages 12 | midi: 13 | channel: 14 14 | 15 | # footswitches definition 16 | # bypass: relay(s) to toggle (LEFT, RIGHT or LEFT_RIGHT) 17 | # color: color to use for enable status halo on LCD 18 | # debounce_input: debounce chip pin to which switch is connected 19 | # disable: disable the switch 20 | # gpio_input: gpio pin if not using debounce 21 | # gpio_output: gpio pin used to drive indicator (LED, etc.) 22 | # id: integer identifier 23 | # midi_CC: msg to send (0 - 127 or None) 24 | # 25 | footswitches: 26 | - id: 0 27 | debounce_input: 0 28 | gpio_output: 0 29 | bypass: LEFT 30 | preset: UP 31 | - id: 1 32 | debounce_input: 1 33 | gpio_output: 13 34 | midi_CC: 62 35 | color: lime 36 | - id: 2 37 | debounce_input: 2 38 | gpio_output: 26 39 | midi_CC: 63 40 | color: blue 41 | 42 | # analog control definition 43 | # adc_input: adc chip pin to which control is connected 44 | # disable: disable the control 45 | # midi_CC: msg to send (0 - 127 or None) 46 | # threshold: minimum value change to trigger a midi msg (16 default, 1024 full scale) 47 | # type: control type (KNOB, EXPRESSION) 48 | # 49 | analog_controllers: 50 | - adc_input: 0 51 | midi_CC: 70 52 | type: KNOB 53 | - adc_input: 1 54 | midi_CC: 71 55 | type: KNOB 56 | -------------------------------------------------------------------------------- /setup/config_templates/default_config_3fs_2knob.yml: -------------------------------------------------------------------------------- 1 | # This file provides some default configuration for the system 2 | # Most of this configuration can be overriden by pedalboard specific configuration. To accomplish that, add 3 | # a file, named config.yml to the pedalboard directory (ie. ~/data/.pedalboards/) 4 | 5 | --- 6 | hardware: 7 | # Hardware version (1.0 for original pi-Stomp, 2.0 for pi-Stomp Core) 8 | version: 2.0 9 | 10 | # midi definition 11 | # channel: midi channel used for midi messages 12 | midi: 13 | channel: 14 14 | 15 | # footswitches definition 16 | # bypass: relay(s) to toggle (LEFT, RIGHT or LEFT_RIGHT) 17 | # color: color to use for enable status halo on LCD 18 | # debounce_input: debounce chip pin to which switch is connected 19 | # disable: disable the switch 20 | # gpio_input: gpio pin if not using debounce 21 | # gpio_output: gpio pin used to drive indicator (LED, etc.) 22 | # id: integer identifier 23 | # midi_CC: msg to send (0 - 127 or None) 24 | # 25 | footswitches: 26 | - id: 0 27 | debounce_input: 0 28 | gpio_output: 0 29 | bypass: LEFT 30 | preset: UP 31 | - id: 1 32 | debounce_input: 1 33 | gpio_output: 13 34 | midi_CC: 62 35 | color: lime 36 | - id: 2 37 | debounce_input: 2 38 | gpio_output: 26 39 | midi_CC: 63 40 | color: blue 41 | 42 | # analog control definition 43 | # adc_input: adc chip pin to which control is connected 44 | # disable: disable the control 45 | # midi_CC: msg to send (0 - 127 or None) 46 | # threshold: minimum value change to trigger a midi msg (16 default, 1024 full scale) 47 | # type: control type (KNOB, EXPRESSION) 48 | # 49 | analog_controllers: 50 | - adc_input: 0 51 | midi_CC: 70 52 | type: KNOB 53 | - adc_input: 1 54 | midi_CC: 71 55 | type: KNOB 56 | -------------------------------------------------------------------------------- /setup/config_templates/default_config_3fs_2knob_exp.yml: -------------------------------------------------------------------------------- 1 | # This file provides some default configuration for the system 2 | # Most of this configuration can be overriden by pedalboard specific configuration. To accomplish that, add 3 | # a file, named config.yml to the pedalboard directory (ie. ~/data/.pedalboards/) 4 | 5 | --- 6 | hardware: 7 | # Hardware version (1.0 for original pi-Stomp, 2.0 for pi-Stomp Core) 8 | version: 2.0 9 | 10 | # midi definition 11 | # channel: midi channel used for midi messages 12 | midi: 13 | channel: 14 14 | 15 | # footswitches definition 16 | # bypass: relay(s) to toggle (LEFT, RIGHT or LEFT_RIGHT) 17 | # color: color to use for enable status halo on LCD 18 | # debounce_input: debounce chip pin to which switch is connected 19 | # disable: disable the switch 20 | # gpio_input: gpio pin if not using debounce 21 | # gpio_output: gpio pin used to drive indicator (LED, etc.) 22 | # id: integer identifier 23 | # midi_CC: msg to send (0 - 127 or None) 24 | # 25 | footswitches: 26 | - id: 0 27 | debounce_input: 0 28 | gpio_output: 0 29 | bypass: LEFT 30 | preset: UP 31 | - id: 1 32 | debounce_input: 1 33 | gpio_output: 13 34 | midi_CC: 62 35 | color: lime 36 | - id: 2 37 | debounce_input: 2 38 | gpio_output: 26 39 | midi_CC: 63 40 | color: blue 41 | 42 | # analog control definition 43 | # adc_input: adc chip pin to which control is connected 44 | # disable: disable the control 45 | # midi_CC: msg to send (0 - 127 or None) 46 | # threshold: minimum value change to trigger a midi msg (16 default, 1024 full scale) 47 | # type: control type (KNOB, EXPRESSION) 48 | # 49 | analog_controllers: 50 | - adc_input: 0 51 | midi_CC: 70 52 | type: KNOB 53 | - adc_input: 1 54 | midi_CC: 71 55 | type: KNOB 56 | - adc_input: 7 57 | midi_CC: 77 58 | type: EXPRESSION 59 | -------------------------------------------------------------------------------- /setup/config_templates/default_config_pistomp.yml: -------------------------------------------------------------------------------- 1 | # This file provides some default configuration for the system 2 | # Unless you have altered the hardware or really know what you're doing, changing this file is not recommended 3 | # and can result in a malfunctioning system. 4 | # Most of this configuration can be overriden by pedalboard specific configuration. To accomplish that, add 5 | # a file, named config.yml to the pedalboard directory (ie. /var/modep/pedalboards/) 6 | 7 | --- 8 | hardware: 9 | version: 1.0 10 | midi: 11 | channel: 14 12 | footswitches: 13 | - id: 0 14 | bypass: LEFT_RIGHT 15 | preset: UP 16 | midi_CC: None 17 | - id: 1 18 | midi_CC: 62 19 | - id: 2 20 | midi_CC: 63 21 | -------------------------------------------------------------------------------- /setup/config_templates/default_config_pistompcore.yml: -------------------------------------------------------------------------------- 1 | # This file provides some default configuration for the system 2 | # Most of this configuration can be overriden by pedalboard specific configuration. To accomplish that, add 3 | # a file, named config.yml to the pedalboard directory (ie. ~/data/.pedalboards/) 4 | 5 | --- 6 | hardware: 7 | # Hardware version (1.0 for original pi-Stomp, 2.0 for pi-Stomp Core) 8 | version: 2.0 9 | 10 | # midi definition 11 | # channel: midi channel used for midi messages 12 | midi: 13 | channel: 14 14 | 15 | # footswitches definition 16 | # bypass: relay(s) to toggle (LEFT, RIGHT or LEFT_RIGHT) 17 | # color: color to use for enable status halo on LCD 18 | # debounce_input: debounce chip pin to which switch is connected 19 | # disable: disable the switch 20 | # gpio_input: gpio pin if not using debounce 21 | # gpio_output: gpio pin used to drive indicator (LED, etc.) 22 | # id: integer identifier 23 | # midi_CC: msg to send (0 - 127 or None) 24 | # 25 | footswitches: 26 | - id: 0 27 | debounce_input: 0 28 | gpio_output: 0 29 | bypass: LEFT 30 | preset: UP 31 | - id: 1 32 | debounce_input: 1 33 | gpio_output: 13 34 | midi_CC: 62 35 | color: lime 36 | - id: 2 37 | debounce_input: 2 38 | gpio_output: 26 39 | midi_CC: 63 40 | color: blue 41 | 42 | # analog control definition 43 | # adc_input: adc chip pin to which control is connected 44 | # disable: disable the control 45 | # midi_CC: msg to send (0 - 127 or None) 46 | # threshold: minimum value change to trigger a midi msg (16 default, 1024 full scale) 47 | # type: control type (KNOB, EXPRESSION) 48 | # 49 | #analog_controllers: 50 | #- adc_input: 0 51 | # midi_CC: 70 52 | # type: KNOB 53 | #- adc_input: 1 54 | # midi_CC: 71 55 | # type: KNOB 56 | #- adc_input: 7 57 | # midi_CC: 77 58 | # type: EXPRESSION 59 | -------------------------------------------------------------------------------- /setup/mod-tweaks/advertise.diff: -------------------------------------------------------------------------------- 1 | --- advertise.py0 2022-07-08 11:34:53.237900811 -0700 2 | +++ advertise.py 2022-07-08 16:47:50.654582076 -0700 3 | @@ -48,7 +48,7 @@ 4 | socket.gethostname(), 5 | TOUCHOSC_BRIDGE 6 | ), 7 | - address=socket.inet_aton(ip), 8 | + addresses=[socket.inet_aton(ip)], 9 | port=PORT, 10 | properties=dict(), 11 | server=socket.gethostname() + '.local.') 12 | @@ -97,7 +97,7 @@ 13 | def get_ip(self): 14 | """:return: the service's IP as a string. 15 | """ 16 | - return socket.inet_ntoa(self.info.address) 17 | + return socket.inet_ntoa(self.info.addresses[0]) 18 | 19 | ip = property(get_ip) 20 | 21 | -------------------------------------------------------------------------------- /setup/mod-tweaks/host.diff: -------------------------------------------------------------------------------- 1 | --- host.py 2018-09-11 15:39:28.874253398 +0000 2 | +++ /home/modep/host.new 2020-06-11 22:36:34.571706506 +0000 3 | @@ -1439,6 +1439,20 @@ 4 | pluginData['ports'][symbol] = value 5 | self.send_modified("param_set %d %s %f" % (instance_id, symbol, value), callback, datatype='boolean') 6 | 7 | + def pi_stomp_param_get(self, port): 8 | + instance, symbol = port.rsplit("/", 1) 9 | + instance_id = self.mapper.get_id_without_creating(instance) 10 | + pluginData = self.plugins[instance_id] 11 | + 12 | + if symbol == ":bypass": 13 | + return pluginData['bypassed'] 14 | + 15 | + if symbol in pluginData['designations']: 16 | + print("ERROR: Trying to modify a specially designated port '%s', stop!" % symbol) 17 | + return 18 | + 19 | + return pluginData['ports'][symbol] 20 | + 21 | def set_position(self, instance, x, y): 22 | instance_id = self.mapper.get_id_without_creating(instance) 23 | pluginData = self.plugins[instance_id] 24 | -------------------------------------------------------------------------------- /setup/mod-tweaks/index.diff: -------------------------------------------------------------------------------- 1 | --- /usr/share/mod/html/index.html 2020-06-15 11:31:53.000000000 +0100 2 | +++ /home/patch/index.html 2020-08-16 01:31:07.871271781 +0100 3 | @@ -614,7 +614,7 @@ 4 | 5 |
0 GHz / 0 °C
6 |
0 Xruns
7 | -
{{bufferSize}} frames
8 | +
{{bufferSize}} frames
9 |
Bypass 2
10 |
Bypass 1
11 |
MIDI Ports
12 | -------------------------------------------------------------------------------- /setup/mod-tweaks/mod-tweaks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | set +e 19 | 20 | TOUCHOSC2MIDI_ROOT=/usr/local/lib/python3.9/dist-packages/touchosc2midi 21 | MOD_SCRIPTS=/usr/mod/scripts 22 | 23 | sudo cp setup/mod-tweaks/start_touchosc2midi.sh $MOD_SCRIPTS 24 | 25 | # This is kindof LAME and fragile. Possibly should fork the blokasio lib instead of patching. 26 | # The fix is required because the latest zeroconf.ServiceInfo constructor requries a list of 27 | # addresses instead of the previous single address 28 | sudo patch -b -N -u $TOUCHOSC2MIDI_ROOT/advertise.py -i setup/mod-tweaks/advertise.diff 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /setup/mod-tweaks/session.diff: -------------------------------------------------------------------------------- 1 | --- session.py0 2020-06-11 20:03:19.296719714 +0000 2 | +++ session.py 2020-06-11 22:20:07.087468576 +0000 3 | @@ -145,6 +145,16 @@ 4 | instance, portsymbol = port.rsplit("/",1) 5 | self.host.address(instance, portsymbol, actuator_uri, label, minimum, maximum, value, steps, callback) 6 | 7 | + # Set a plugin parameter via pi-stomp 8 | + # We use ":bypass" symbol for on/off state 9 | + def pi_stomp_parameter_set(self, port, value, callback): 10 | + instance, portsymbol = port.rsplit("/",1) 11 | + if portsymbol == ":bypass": 12 | + bvalue = value >= 0.5 13 | + self.host.bypass(instance, bvalue, callback) 14 | + else: 15 | + self.host.param_set(port, value, callback) 16 | + 17 | # Connect 2 ports 18 | def web_connect(self, port_from, port_to, callback): 19 | self.host.connect(port_from, port_to, callback) 20 | -------------------------------------------------------------------------------- /setup/mod-tweaks/start_touchosc2midi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | IN_PORT_ID=$(/usr/bin/touchosc2midi list ports 2>&1 | grep touchosc | head -n 1 | egrep -o "\s+[0-9]+: " | egrep -o "[0-9]+") 3 | OUT_PORT_ID=$(/usr/bin/touchosc2midi list ports 2>&1 | grep touchosc | tail -n 1 | egrep -o "\s+[0-9]+: " | egrep -o "[0-9]+") 4 | exec touchosc2midi --midi-in=$IN_PORT_ID --midi-out=$OUT_PORT_ID 5 | -------------------------------------------------------------------------------- /setup/mod-tweaks/webserver.diff: -------------------------------------------------------------------------------- 1 | --- webserver.py.orig 2020-06-15 11:31:53.000000000 +0100 2 | +++ webserver.py 2020-08-14 21:56:54.429424077 +0100 3 | @@ -938,6 +938,25 @@ 4 | 5 | self.write(ok) 6 | 7 | +class EffectParameterSetPiStomp(JsonRequestHandler): 8 | + @web.asynchronous 9 | + @gen.engine 10 | + 11 | + def post(self, port): 12 | + data = json.loads(self.request.body.decode("utf-8", errors="ignore")) 13 | + value = float(data['value']) 14 | + 15 | + ok = yield gen.Task(SESSION.pi_stomp_parameter_set, port, value) 16 | + self.write(ok) 17 | + 18 | +class EffectParameterGetPiStomp(JsonRequestHandler): 19 | + @web.asynchronous 20 | + @gen.engine 21 | + 22 | + def get(self, port): 23 | + value = SESSION.host.pi_stomp_param_get(port) 24 | + self.write(value) 25 | + 26 | class EffectPresetLoad(JsonRequestHandler): 27 | @web.asynchronous 28 | @gen.engine 29 | @@ -2101,6 +2120,9 @@ 30 | elif filetype == "sfz": 31 | return ("SFZ Instruments", (".sfz",)) 32 | 33 | + elif filetype == "tapf": 34 | + return ("Amplifier Profiles", (".tapf",)) 35 | + 36 | else: 37 | return (None, ()) 38 | 39 | @@ -2167,6 +2189,8 @@ 40 | # plugin parameters 41 | (r"/effect/parameter/address/*(/[A-Za-z0-9_:/]+[^/])/?", EffectParameterAddress), 42 | (r"/effect/parameter/set/?", EffectParameterSet), 43 | + (r"/effect/parameter/pi_stomp_set/*(/[A-Za-z0-9_:/]+[^/])/?", EffectParameterSetPiStomp), 44 | + (r"/effect/parameter/pi_stomp_get/*(/[A-Za-z0-9_:/]+[^/])/?", EffectParameterGetPiStomp), 45 | 46 | # plugin presets 47 | (r"/effect/preset/load/*(/[A-Za-z0-9_/]+[^/])/?", EffectPresetLoad), -------------------------------------------------------------------------------- /setup/mod/80: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/setup/mod/80 -------------------------------------------------------------------------------- /setup/mod/browsepy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=browsepy 3 | 4 | [Service] 5 | #Environment=HOME=/root 6 | Environment=BROWSEPY_HOST=0.0.0.0 7 | Environment=BROWSEPY_PORT=8081 8 | 9 | WorkingDirectory=/home/pistomp/data/user-files/ 10 | ExecStart=/usr/local/bin/browsepy 0.0.0.0 8081 --directory /home/pistomp/data/user-files --upload /home/pistomp/data/user-files --removable /home/pistomp/data/user-files 11 | User=pistomp 12 | Group=pistomp 13 | Restart=always 14 | RestartSec=2 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /setup/mod/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | set -x 19 | 20 | #Install Dependancies 21 | sudo apt-get -y install virtualenv python3-pip python3-dev python3-zeroconf build-essential libasound2-dev libjack-jackd2-dev liblilv-dev libjpeg-dev \ 22 | zlib1g-dev cmake debhelper dh-autoreconf dh-python gperf intltool ladspa-sdk libarmadillo-dev libavahi-gobject-dev \ 23 | libavcodec-dev libavutil-dev libbluetooth-dev libboost-dev libeigen3-dev libfftw3-dev libglib2.0-dev libglibmm-2.4-dev \ 24 | libgtk2.0-dev libgtkmm-2.4-dev liblrdf0-dev libsamplerate0-dev libsigc++-2.0-dev libsndfile1-dev libzita-convolver-dev \ 25 | libzita-resampler-dev lv2-dev p7zip-full python3-all python3-setuptools libreadline-dev zita-alsa-pcmi-utils hostapd \ 26 | dnsmasq iptables python3-smbus liblo-dev python3-liblo libzita-alsa-pcmi-dev authbind rcconf libfluidsynth-dev lockfile-progs 27 | 28 | #Install Python Dependancies 29 | sudo pip3 install pyserial==3.0 pystache==0.5.4 aggdraw==1.3.11 scandir backports.shutil-get-terminal-size 30 | sudo pip3 install pycrypto 31 | sudo pip3 install tornado==4.3 32 | sudo pip3 install Pillow==8.4.0 33 | sudo pip3 install cython 34 | 35 | #Install Mod Software 36 | mkdir -p /home/pistomp/data/.pedalboards 37 | mkdir -p /home/pistomp/data/user-files 38 | sudo mkdir -p /usr/mod/scripts 39 | cd /home/pistomp/data/user-files 40 | mkdir -p "Speaker Cabinets IRs" 41 | mkdir -p "Reverb IRs" 42 | mkdir -p "Audio Loops" 43 | mkdir -p "Audio Recordings" 44 | mkdir -p "Audio Samples" 45 | mkdir -p "Audio Tracks" 46 | mkdir -p "MIDI Clips" 47 | mkdir -p "MIDI Songs" 48 | mkdir -p "Hydrogen Drumkits" 49 | mkdir -p "SF2 Instruments" 50 | mkdir -p "SFZ Instruments" 51 | mkdir -p "Amplifier Profiles" 52 | mkdir -p "Aida DSP Models" 53 | mkdir -p "NAM Models" 54 | 55 | #Jack2 56 | pushd $(mktemp -d) && git clone https://github.com/moddevices/jack2.git 57 | pushd jack2 58 | ./waf configure 59 | ./waf build 60 | sudo ./waf install 61 | 62 | #Browsepy 63 | pushd $(mktemp -d) && git clone https://github.com/micahvdm/browsepy.git 64 | pushd browsepy 65 | sudo pip3 install ./ 66 | 67 | #Mod-host 68 | pushd $(mktemp -d) && git clone https://github.com/moddevices/mod-host.git 69 | pushd mod-host 70 | make 71 | sudo make install 72 | 73 | #Mod-ui 74 | pushd $(mktemp -d) && git clone https://github.com/micahvdm/mod-ui.git 75 | pushd mod-ui 76 | chmod +x setup.py 77 | cd utils 78 | make 79 | cd .. 80 | sudo ./setup.py install 81 | cp -r default.pedalboard /home/pistomp/data/.pedalboards 82 | 83 | #Touchosc2midi 84 | pushd $(mktemp -d) && git clone https://github.com/BlokasLabs/amidithru.git 85 | pushd amidithru 86 | sed -i 's/CXX=g++.*/CXX=g++/' Makefile 87 | sudo make install 88 | 89 | pushd $(mktemp -d) && git clone https://github.com/micahvdm/touchosc2midi.git 90 | pushd touchosc2midi 91 | sudo pip3 install ./ 92 | 93 | pushd $(mktemp -d) && git clone https://github.com/micahvdm/mod-midi-merger.git 94 | pushd mod-midi-merger 95 | mkdir build && cd build 96 | cmake .. 97 | make 98 | sudo make install 99 | 100 | cd /home/pistomp 101 | 102 | ln -s /home/pistomp/data/.pedalboards /home/pistomp/.pedalboards 103 | ln -s /home/pistomp/.lv2 /home/pistomp/data/.lv2 104 | 105 | cd /home/pistomp/pi-stomp/setup/mod 106 | 107 | #Create Services 108 | sudo cp *.service /usr/lib/systemd/system/ 109 | sudo ln -sf /usr/lib/systemd/system/browsepy.service /etc/systemd/system/multi-user.target.wants 110 | sudo ln -sf /usr/lib/systemd/system/jack.service /etc/systemd/system/multi-user.target.wants 111 | sudo ln -sf /usr/lib/systemd/system/mod-host.service /etc/systemd/system/multi-user.target.wants 112 | sudo ln -sf /usr/lib/systemd/system/mod-ui.service /etc/systemd/system/multi-user.target.wants 113 | sudo ln -sf /usr/lib/systemd/system/mod-amidithru.service /etc/systemd/system/multi-user.target.wants 114 | sudo ln -sf /usr/lib/systemd/system/mod-touchosc2midi.service /etc/systemd/system/multi-user.target.wants 115 | sudo ln -sf /usr/lib/systemd/system/mod-midi-merger.service /etc/systemd/system/multi-user.target.wants 116 | sudo ln -sf /usr/lib/systemd/system/mod-midi-merger-broadcaster.service /etc/systemd/system/multi-user.target.wants 117 | 118 | #Create users and groups so services can run as user instead of root 119 | sudo adduser --no-create-home --system --group jack 120 | sudo adduser pistomp jack --quiet 121 | sudo adduser root jack --quiet 122 | sudo adduser jack audio --quiet 123 | sudo cp jackdrc /etc/ 124 | sudo chmod +x /etc/jackdrc 125 | sudo chown jack:jack /etc/jackdrc 126 | sudo cp 80 /etc/authbind/byport/ 127 | sudo chmod 500 /etc/authbind/byport/80 128 | sudo chown pistomp:pistomp /etc/authbind/byport/80 129 | -------------------------------------------------------------------------------- /setup/mod/jack.service: -------------------------------------------------------------------------------- 1 | 2 | [Unit] 3 | Description=JACK2 Audio Server 4 | #After=sound.target 5 | 6 | [Service] 7 | Environment=LV2_PATH=/home/pistomp/.lv2 8 | Environment=JACK_NO_AUDIO_RESERVATION=1 9 | Environment=JACK_PROMISCUOUS_SERVER=jack 10 | LimitRTPRIO=infinity 11 | LimitMEMLOCK=infinity 12 | ExecStart=/etc/jackdrc 13 | User=jack 14 | Group=jack 15 | Restart=always 16 | RestartSec=1 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /setup/mod/jackdrc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | exec env JACK_DRIVER_DIR=/usr/local/lib/jack /usr/local/bin/jackd -t 2000 -R -P 95 -d alsa -d hw:0 -r 48000 -p 256 -n 2 -X seq -s 4 | -------------------------------------------------------------------------------- /setup/mod/mod-amidithru.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=amidithru service for touchosc2midi 3 | After=sound.target 4 | Wants=sound.target 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/amidithru touchosc 8 | Restart=always 9 | RestartSec=2 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /setup/mod/mod-host.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=MOD-host 3 | After=jack.service 4 | BindsTo=jack.service 5 | 6 | [Service] 7 | LimitRTPRIO=95 8 | LimitMEMLOCK=infinity 9 | Type=forking 10 | Environment=LV2_PATH=/home/pistomp/.lv2 11 | Environment=JACK_PROMISCUOUS_SERVER=jack 12 | ExecStart=/usr/local/bin/mod-host -p 5555 -f 5556 13 | User=pistomp 14 | Group=pistomp 15 | Restart=always 16 | RestartSec=2 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /setup/mod/mod-midi-merger-broadcaster.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=MOD MIDI Broadcaster 3 | After=jack.service 4 | Requires=jack.service 5 | BindsTo=jack.service 6 | [Service] 7 | RemainAfterExit=yes 8 | Environment=JACK_PROMISCUOUS_SERVER=jack 9 | ExecStart=/usr/local/bin/jack_load mod-midi-broadcaster 10 | ExecStop=/usr/local/bin/jack_unload mod-midi-broadcaster 11 | Restart=always 12 | RestartSec=2 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /setup/mod/mod-midi-merger.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=MOD MIDI Merger 3 | After=jack.service 4 | Requires=jack.service 5 | BindsTo=jack.service 6 | [Service] 7 | RemainAfterExit=yes 8 | Environment=JACK_PROMISCUOUS_SERVER=jack 9 | ExecStart=/usr/local/bin/jack_load mod-midi-merger 10 | ExecStop=/usr/local/bin/jack_unload mod-midi-merger 11 | Restart=always 12 | RestartSec=2 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /setup/mod/mod-touchosc2midi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=TouchOsc2Midi service 3 | After=network-online.target mod-amidithru.service 4 | Wants=network-online.target 5 | BindsTo=mod-amidithru.service network-online.target jack.service 6 | Conflicts=touchosc2midi.service 7 | 8 | [Service] 9 | ExecStart=/bin/bash /usr/mod/scripts/start_touchosc2midi.sh 10 | Restart=always 11 | RestartSec=2 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /setup/mod/mod-ui.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=MOD-UI 3 | After=mod-host.service 4 | After=browsepy.service 5 | Requires=mod-host.service 6 | Requires=browsepy.service 7 | 8 | [Service] 9 | LimitRTPRIO=95 10 | LimitMEMLOCK=infinity 11 | Environment=HOME=/home/pistomp/data 12 | Environment=LV2_PATH=/home/pistomp/.lv2 13 | Environment=LV2_PLUGIN_DIR=/home/pistomp/.lv2 14 | Environment=LV2_PEDALBOARDS_DIR=/home/pistomp/data/.pedalboards 15 | Environment=MOD_DEV_ENVIRONMENT=0 16 | Environment=MOD_DEVICE_WEBSERVER_PORT=80 17 | Environment=MOD_LOG=0 18 | Environment=MOD_APP=0 19 | Environment=MOD_LIVE_ISO=0 20 | Environment=MOD_SYSTEM_OUTPUT=1 21 | Environment=MOD_DATA_DIR=/home/pistomp/data 22 | Environment=MOD_USER_FILES_DIR=/home/pistomp/data/user-files 23 | Environment=MOD_HTML_DIR=/usr/local/share/mod/html 24 | Environment=JACK_PROMISCUOUS_SERVER=jack 25 | Environment=PATCHSTORAGE_API_URL=https://patchstorage.com/api/beta/patches 26 | Environment=PATCHSTORAGE_PLATFORM_ID=8046 27 | Environment=PATCHSTORAGE_TARGET_ID=8280 28 | 29 | ExecStart=/usr/bin/authbind /usr/local/bin/mod-ui 30 | User=pistomp 31 | Group=pistomp 32 | Restart=always 33 | RestartSec=2 34 | 35 | [Install] 36 | WantedBy=multi-user.target 37 | -------------------------------------------------------------------------------- /setup/pedalboards/get_pedalboards.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | DEST=/home/pistomp/data/.pedalboards 19 | 20 | # Get example pedalboards, copy to pedalboard directory 21 | pushd $(mktemp -d) && git clone https://github.com/TreeFallSound/pi-stomp-pedalboards.git 22 | 23 | sudo cp -rT pi-stomp-pedalboards $DEST 24 | 25 | sudo chown -R pistomp $DEST 26 | sudo chgrp -R pistomp $DEST 27 | -------------------------------------------------------------------------------- /setup/pi-stomp-tweaks/modify_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | if [ -z "$1" ] 19 | then 20 | echo "Requires version" 21 | echo "Usage: $(basename $0) " 22 | exit 23 | fi 24 | 25 | config_dir="$HOME/data/config" 26 | config_file="$config_dir/default_config.yml" 27 | 28 | template_dir="$HOME/pi-stomp/setup/config_templates" 29 | pistomp_orig_config_file="$template_dir/default_config_pistomp.yml" 30 | pistomp_core_config_file="$template_dir/default_config_pistompcore.yml" 31 | 32 | mkdir -p $config_dir 33 | 34 | 35 | if awk "BEGIN {exit !($1 < 2.0 )}"; then 36 | cp $pistomp_orig_config_file $config_file 37 | else 38 | cp $pistomp_core_config_file $config_file 39 | fi 40 | 41 | sed -i "s/version: [0-9]\.*[0-9]*\.*[0-9]*/version: $1/" $config_file 42 | 43 | printf "\nHardware version changed to: $1\n" 44 | -------------------------------------------------------------------------------- /setup/pkgs/gfxhat_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | # GFXHat LCD 19 | pushd $(mktemp -d) 20 | curl -k https://get.pimoroni.com/gfxhat > gfxhat.sh 21 | sed -i 's/pip2support="yes"/pip2support="no"/' gfxhat.sh 22 | chmod a+x gfxhat.sh 23 | ./gfxhat.sh -y 24 | 25 | -------------------------------------------------------------------------------- /setup/pkgs/lilv_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | # Dependencies 19 | if (which python3 > /dev/null); then true; else 20 | echo "python3 not found, please install it" 21 | exit 22 | fi 23 | 24 | if (which pip3 > /dev/null); then true; else 25 | echo "pip3 not found, please install it" 26 | exit 27 | fi 28 | 29 | sudo pip3 install python-config 30 | 31 | sudo apt-get -y install liblilv-dev lv2-dev libserd-dev libsord-dev libsratom-dev 32 | 33 | # Get it 34 | pushd $(mktemp -d) 35 | wget http://download.drobilla.net/lilv-0.24.12.tar.bz2 36 | tar xvf lilv-0.24.12.tar.bz2 37 | pushd lilv-0.24.12 38 | 39 | # configure, build, install 40 | python3 ./waf configure --prefix=/usr/local --static --static-progs --no-shared --no-utils --no-bash-completion --pythondir=/usr/local/lib/python3.9/dist-packages 41 | python3 ./waf build 42 | sudo python3 ./waf install 43 | -------------------------------------------------------------------------------- /setup/pkgs/mod-ttymidi_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | pushd $(mktemp -d) && git clone https://github.com/moddevices/mod-ttymidi.git 19 | pushd mod-ttymidi 20 | sudo make install 21 | -------------------------------------------------------------------------------- /setup/pkgs/simple_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | # pip3 19 | if (which pip3 > /dev/null); then true; else 20 | sudo apt-get install --fix-broken --fix-missing -y 21 | sudo apt-get install -y python3-pip 22 | fi 23 | 24 | # Pyyml 25 | sudo /usr/bin/pip3 install pyyaml 26 | 27 | # For diagnostic test mode 28 | sudo /usr/bin/pip3 install pyalsaaudio 29 | 30 | # Midi 31 | sudo /usr/bin/pip3 install python-rtmidi 32 | 33 | # Requests 34 | sudo /usr/bin/pip3 install requests 35 | 36 | # GPIO 37 | sudo /usr/bin/pip3 install RPi.GPIO 38 | 39 | #GFXHat 40 | sudo /usr/bin/pip3 install gfxhat 41 | 42 | # LEDstring 43 | sudo /usr/bin/pip3 install matplotlib rpi_ws281x adafruit-circuitpython-neopixel 44 | 45 | # LCD 46 | sudo /usr/bin/pip3 install adafruit-circuitpython-rgb-display 47 | sudo apt install -y python3-numpy 48 | 49 | # MCP3xxx (ADC support) 50 | pushd $(mktemp -d) && curl https://files.pythonhosted.org/packages/57/3a/2d62e66b60619d6f15a2ebf08ad77fcc4196c924e489ec22b66e1977d88b/adafruit-circuitpython-mcp3xxx-1.4.1.tar.gz > mcp.tgz 51 | sudo /usr/bin/pip3 install mcp.tgz 52 | -------------------------------------------------------------------------------- /setup/plugins/build_extra_plugins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | # Install libraries required for build 19 | sudo apt install -y libcairo2-dev libx11-dev lv2-dev 20 | 21 | # Build and install into ~/.lv2 22 | pushd $(mktemp -d) && git clone https://github.com/brummer10/GxPlugins.lv2.git 23 | cd * 24 | git submodule init 25 | git submodule update 26 | make 27 | make install 28 | 29 | -------------------------------------------------------------------------------- /setup/plugins/get_plugins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | set -x 19 | 20 | wget https://www.treefallsound.com/downloads/lv2plugins.tar.gz 21 | tar -zxf lv2plugins.tar.gz -C $HOME 22 | rm lv2plugins.tar.gz 23 | -------------------------------------------------------------------------------- /setup/services/create_services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | sudo cp setup/services/*.service /usr/lib/systemd/system/ 19 | sudo ln -sf /usr/lib/systemd/system/mod-ala-pi-stomp.service /etc/systemd/system/multi-user.target.wants 20 | 21 | if [ x"$has_ttymidi" == x"true" ]; then 22 | echo "Enabling ttymidi service" 23 | sudo ln -sf /usr/lib/systemd/system/ttymidi.service /etc/systemd/system/multi-user.target.wants 24 | fi 25 | 26 | #Copy WiFi hotspot files 27 | sudo cp setup/services/hotspot/etc/default/hostapd.pistomp /etc/default 28 | sudo cp setup/services/hotspot/etc/dnsmasq.d/wifi-hotspot.conf /etc/dnsmasq.d 29 | sudo cp setup/services/hotspot/etc/hostapd/hostapd.conf /etc/hostapd 30 | sudo cp -R setup/services/hotspot/usr/lib/pistomp-wifi /usr/lib 31 | sudo cp setup/services/hotspot/usr/lib/systemd/system/wifi-hotspot.service /usr/lib/systemd/system 32 | sudo chown -R pistomp:pistomp /usr/lib/pistomp-wifi 33 | sudo chmod +x -R /usr/lib/pistomp-wifi 34 | 35 | # USB automounter 36 | sudo dpkg -i setup/services/usbmount.deb 37 | 38 | # Disable wait for network on boot 39 | sudo raspi-config nonint do_boot_wait 1 40 | 41 | # Copy wifi_check script 42 | sudo cp setup/services/wifi_check.sh /etc/wpa_supplicant/ 43 | 44 | # Copy wlan0.conf to prevent wifi power save mode 45 | sudo cp setup/services/wlan0.conf /etc/network/interfaces.d/ 46 | -------------------------------------------------------------------------------- /setup/services/hotspot/etc/default/hostapd.pistomp: -------------------------------------------------------------------------------- 1 | # Defaults for hostapd initscript 2 | # 3 | # See /usr/share/doc/hostapd/README.Debian for information about alternative 4 | # methods of managing hostapd. 5 | # 6 | # Uncomment and set DAEMON_CONF to the absolute path of a hostapd configuration 7 | # file and hostapd will be started during system boot. An example configuration 8 | # file can be found at /usr/share/doc/hostapd/examples/hostapd.conf.gz 9 | # 10 | DAEMON_CONF="/etc/hostapd/hostapd.conf" 11 | 12 | # Additional daemon options to be appended to hostapd command:- 13 | # -d show more debug messages (-dd for even more) 14 | # -K include key data in debug messages 15 | # -t include timestamps in some debug messages 16 | # 17 | # Note that -B (daemon mode) and -P (pidfile) options are automatically 18 | # configured by the init.d script and must not be added to DAEMON_OPTS. 19 | # 20 | #DAEMON_OPTS="" 21 | -------------------------------------------------------------------------------- /setup/services/hotspot/etc/dnsmasq.d/wifi-hotspot.conf: -------------------------------------------------------------------------------- 1 | interface=wlan0 2 | listen-address=172.24.1.1 3 | bind-interfaces 4 | server=8.8.8.8 5 | domain-needed 6 | bogus-priv 7 | dhcp-range=172.24.1.50,172.24.1.150,12h 8 | -------------------------------------------------------------------------------- /setup/services/hotspot/etc/hostapd/hostapd.conf: -------------------------------------------------------------------------------- 1 | # This is the name of the WiFi interface we configured above 2 | interface=wlan0 3 | 4 | # Use the nl80211 driver with the brcmfmac driver 5 | driver=nl80211 6 | 7 | # This is the name of the network 8 | ssid=pistomp 9 | 10 | # Use the 2.4GHz band 11 | hw_mode=g 12 | 13 | # Use channel 6 14 | channel=6 15 | 16 | # Enable 802.11n 17 | ieee80211n=1 18 | 19 | # Enable WMM 20 | wmm_enabled=1 21 | 22 | # Enable 40MHz channels with 20ns guard interval 23 | ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40] 24 | 25 | # Accept all MAC addresses 26 | macaddr_acl=0 27 | 28 | # Use WPA authentication 29 | auth_algs=1 30 | 31 | # Require clients to know the network name 32 | ignore_broadcast_ssid=0 33 | 34 | # Use WPA2 35 | wpa=2 36 | 37 | # Use a pre-shared key 38 | wpa_key_mgmt=WPA-PSK 39 | 40 | # The network passphrase 41 | wpa_passphrase=pistompwifi 42 | 43 | # Use AES, instead of TKIP 44 | rsn_pairwise=CCMP 45 | -------------------------------------------------------------------------------- /setup/services/hotspot/usr/lib/pistomp-wifi/disable_wifi_hotspot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # patchbox-wifi scripts for enabling/disabling wifi hotspot 4 | # 5 | # Copyright (C) 2017 Vilniaus Blokas UAB, https://blokas.io/pisound 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; version 2 of the 10 | # License. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 | # 21 | 22 | if [[ "$(systemctl is-system-running || true)" == "stopping" ]]; then 23 | exit 24 | fi 25 | 26 | rfkill unblock wifi 27 | dhcpcd --allowinterfaces wlan0 28 | systemctl stop hostapd 29 | systemctl stop dnsmasq 30 | systemctl disable hostapd 31 | systemctl disable dnsmasq 32 | ifconfig wlan0 0.0.0.0 33 | echo | iptables-restore 34 | echo 0 > /proc/sys/net/ipv4/ip_forward 35 | iwlist wlan0 scan > /dev/null 2>&1 36 | ifconfig wlan0 up 37 | systemctl restart avahi-daemon 38 | wpa_cli -i wlan0 reconnect 39 | -------------------------------------------------------------------------------- /setup/services/hotspot/usr/lib/pistomp-wifi/enable_wifi_hotspot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # patchbox-wifi scripts for enabling/disabling wifi hotspot 4 | # 5 | # Copyright (C) 2017 Vilniaus Blokas UAB, https://blokas.io/pisound 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; version 2 of the 10 | # License. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 | # 21 | 22 | rfkill unblock wifi 23 | wpa_cli -i wlan0 disconnect 24 | dhcpcd --denyinterfaces wlan0 25 | ifconfig wlan0 down 26 | ifconfig wlan0 172.24.1.1 netmask 255.255.255.0 broadcast 172.24.1.255 27 | systemctl stop dnsmasq 28 | iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE 29 | iptables -A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT 30 | iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT 31 | echo 1 > /proc/sys/net/ipv4/ip_forward 32 | systemctl start dnsmasq 33 | (sleep 15 && systemctl restart avahi-daemon) & 34 | systemctl unmask hostapd 35 | systemctl start hostapd 36 | 37 | systemctl restart mod-touchosc2midi 2>/dev/null 38 | -------------------------------------------------------------------------------- /setup/services/hotspot/usr/lib/systemd/system/wifi-hotspot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WiFi Hotspot 3 | After=network.target 4 | 5 | [Service] 6 | RemainAfterExit=yes 7 | ExecStart=/bin/bash /usr/lib/pistomp-wifi/enable_wifi_hotspot.sh 8 | ExecStop=/bin/bash /usr/lib/pistomp-wifi/disable_wifi_hotspot.sh 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /setup/services/mod-ala-pi-stomp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=MOD-ALA-PI-STOMP 3 | After=mod-ui.service 4 | Requires=mod-ui.service 5 | 6 | [Service] 7 | ExecStart=/usr/bin/python3 /home/pistomp/pi-stomp/modalapistomp.py 8 | Restart=always 9 | RestartSec=2 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /setup/services/stop_services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | sudo systemctl disable hciuart.service 19 | sudo systemctl stop hciuart.service 20 | #sudo systemctl mask --now hciuart.service 21 | -------------------------------------------------------------------------------- /setup/services/ttymidi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=TTYMIDI 3 | After=mod-host.service 4 | Requires=mod-host.service 5 | 6 | [Service] 7 | Environment=HOME=/home/pistomp 8 | WorkingDirectory=/home/pistomp 9 | Environment=JACK_PROMISCUOUS_SERVER=jack 10 | ExecStart=/usr/local/bin/ttymidi -s /dev/ttyAMA0 -b 38400 11 | Restart=always 12 | RestartSec=2 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /setup/services/usbmount.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreeFallSound/pi-stomp/b610f0e9c4ac2ef13545093fa08d0dcd49d679fd/setup/services/usbmount.deb -------------------------------------------------------------------------------- /setup/services/wifi_check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | LOG="/var/log/wifi.log" 19 | CURRENTDATE=$( date '+%F_%H:%M:%S' ) 20 | 21 | iwgetid -r &>/dev/null 22 | 23 | if [ $? -eq 0 ]; then 24 | echo "${CURRENTDATE} Wifi is connected." >> "$LOG" 25 | else 26 | sudo systemctl restart wifi-hotspot.service 27 | echo "${CURRENTDATE} Wifi not connected. Starting hotspot." >> "$LOG" 28 | fi 29 | -------------------------------------------------------------------------------- /setup/services/wlan0.conf: -------------------------------------------------------------------------------- 1 | auto lo 2 | 3 | auto wlan0 4 | iface wlan0 inet dhcp 5 | post-up iwconfig wlan0 power off 6 | -------------------------------------------------------------------------------- /setup/sys/bash_aliases: -------------------------------------------------------------------------------- 1 | # aliass for common pi-stomp operations (only intended to aid the memory of humans) 2 | alias ps-restart='sudo systemctl restart mod-ala-pi-stomp' 3 | alias ps-stop='sudo systemctl stop mod-ala-pi-stomp' 4 | alias ps-run='sudo $HOME/pi-stomp/modalapistomp.py' 5 | alias ps-journal='sudo journalctl -f -u mod-ala-pi-stomp' 6 | alias ttymidi-enable='sudo ln -sf /usr/lib/systemd/system/ttymidi.service /etc/systemd/system/multi-user.target.wants; sudo sys 7 | temctl restart ttymidi' 8 | alias ttymidi-disable='sudo systemctl stop ttymidi; sudo rm /etc/systemd/system/multi-user.target.wants/ttymidi.service' 9 | -------------------------------------------------------------------------------- /setup/sys/config_tweaks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | set +e 19 | 20 | sudo sed -i 's/console=serial0,115200//' /boot/cmdline.txt 21 | 22 | # Remove devices not needed for audio 23 | sudo bash -c "sed -i \"s/^\s*hdmi_force_hotplug=/#hdmi_force_hotplug=/\" /boot/config.txt" 24 | sudo bash -c "sed -i \"s/^\s*camera_auto_detect=/#camera_auto_detect=/\" /boot/config.txt" 25 | sudo bash -c "sed -i \"s/^\s*display_auto_detect=/#display_auto_detect=/\" /boot/config.txt" 26 | sudo bash -c "sed -i \"s/^\s*dtoverlay=vc4-kms-v3d/#dtoverlay=vc4-kms-v3d/\" /boot/config.txt" 27 | 28 | # Enable SPI 29 | sudo bash -c "sed -i \"s/^\s*#dtparam=spi=on/dtparam=spi=on/\" /boot/config.txt" 30 | 31 | # append lines to config.txt 32 | cnt=$(grep -c "dtoverlay=pi3-disable-bt" /boot/config.txt) 33 | if [[ "$cnt" -eq "0" ]]; then 34 | sudo bash -c "cat >> /boot/config.txt <. 17 | 18 | set -x 19 | set -e 20 | 21 | sudo dpkg -i setup/sys/linux-image-5.15.65-rt49-v8+_5.15.65-rt49-v8+-2_arm64.deb 22 | 23 | KERN=5.15.65-rt49-v8+ 24 | sudo mkdir -p /boot/$KERN/o/ 25 | sudo cp -d /usr/lib/linux-image-$KERN/overlays/* /boot/$KERN/o/ 26 | sudo cp -dr /usr/lib/linux-image-$KERN/* /boot/$KERN/ 27 | sudo cp -d /usr/lib/linux-image-$KERN/broadcom/* /boot/$KERN/ 28 | sudo touch /boot/$KERN/o/README 29 | sudo mv /boot/vmlinuz-$KERN /boot/$KERN/ 30 | sudo mv /boot/initrd.img-$KERN /boot/$KERN/ 31 | sudo mv /boot/System.map-$KERN /boot/$KERN/ 32 | sudo cp /boot/config-$KERN /boot/$KERN/ 33 | sudo bash -c "cat >> /boot/config.txt << EOF 34 | [all] 35 | kernel=vmlinuz-$KERN 36 | # initramfs initrd.img-$KERN 37 | os_prefix=$KERN/ 38 | overlay_prefix=o/ 39 | arm_64bit=1 40 | [all] 41 | EOF" 42 | 43 | #Turn off raspi-config service and set performance governor 44 | sudo rcconf --off raspi-config 45 | sudo bash -c "echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor" 46 | -------------------------------------------------------------------------------- /util/change-audio-card.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cards=("audioinjector-wm8731-audio" "iqaudio-codec" "hifiberry-dacplusadc") 4 | config_file=/boot/config.txt 5 | state_file=/var/lib/alsa/asound.state 6 | 7 | if [ $# -eq 0 ]; then 8 | PS3="Select a card: " 9 | select opt in ${cards[@]}; do 10 | if [[ " ${cards[*]} " =~ " ${opt} " ]]; then 11 | break 12 | fi 13 | done 14 | else 15 | opt=$1 16 | fi 17 | 18 | # Enable the dtoverlay for the selected card, comment out the others 19 | card_found=0 20 | for c in ${cards[@]}; do 21 | if [[ "$opt" == "$c" ]]; then 22 | sudo sed -i "s/^\s*#dtoverlay=$c/dtoverlay=$c/" ${config_file} 23 | echo "$c card enabled in ${config_file}" 24 | card_found=1 25 | else 26 | sudo sed -i "s/^\s*dtoverlay=$c/#dtoverlay=$c/" ${config_file} 27 | fi 28 | done 29 | 30 | if [[ ${card_found} -eq 1 ]]; then 31 | # remove the state file so that the card specific state file will be loaded next time modalapistomp starts 32 | sudo rm -f ${state_file} 33 | 34 | echo "*******************************" 35 | echo "* Reconfiguration complete. *" 36 | echo "* You can now: *" 37 | echo "* 1) Manually power down *" 38 | echo "* 2) Attach new card *" 39 | echo "* 3) Restart *" 40 | echo "*******************************" 41 | else 42 | echo "$opt is not a known card" 43 | exit 1 44 | fi 45 | -------------------------------------------------------------------------------- /util/monitor_din_midi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import serial 4 | 5 | #ser = serial.Serial('/dev/ttyAMA0', baudrate=31250) 6 | ser = serial.Serial('/dev/ttyAMA0', baudrate=38400) 7 | 8 | message = [0, 0, 0] 9 | while True: 10 | i = 0 11 | while i < 3: 12 | data = ord(ser.read(1)) # read a byte 13 | if data >> 7 != 0: 14 | i = 0 # status byte! this is the beginning of a midi message! 15 | message[i] = data 16 | i += 1 17 | if i == 2 and message[0] >> 4 == 12: # program change: don't wait for a 18 | message[2] = 0 # third byte: it has only 2 bytes 19 | i = 3 20 | 21 | messagetype = message[0] >> 4 22 | messagechannel = (message[0] & 15) + 1 23 | note = message[1] if len(message) > 1 else None 24 | velocity = message[2] if len(message) > 2 else None 25 | print('msg %d' % velocity) 26 | if messagetype == 9: # Note on 27 | print('Note on') 28 | elif messagetype == 8: # Note off 29 | print( 'Note off') 30 | elif messagetype == 12: # Program change 31 | print( 'Program change') 32 | -------------------------------------------------------------------------------- /util/relay_toggle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of pi-stomp. 4 | # 5 | # pi-stomp is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # pi-stomp is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with pi-stomp. If not, see . 17 | 18 | import sys, os 19 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 20 | 21 | import RPi.GPIO as GPIO 22 | import pistomp.relay as Relay 23 | 24 | 25 | # Warning these pin assignments must match pistomp/pistomp.py 26 | RELAY_RESET_PIN = 16 27 | RELAY_SET_PIN = 12 28 | 29 | 30 | def main(): 31 | mode_previously_unset = False 32 | if GPIO.getmode() is None: 33 | print ("set GPIO mode") 34 | mode_previously_unset = True 35 | GPIO.setmode(GPIO.BCM) 36 | 37 | relay = Relay.Relay(RELAY_SET_PIN, RELAY_RESET_PIN) 38 | relay.init_state() 39 | if relay.enabled is True: 40 | print("disabling...") 41 | relay.disable() 42 | else: 43 | print("enabling...") 44 | relay.enable() 45 | 46 | if mode_previously_unset is True: 47 | print ("cleanup GPIO") 48 | GPIO.cleanup() 49 | 50 | if __name__ == '__main__': 51 | main() 52 | --------------------------------------------------------------------------------