├── .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 |
--------------------------------------------------------------------------------