squishbox 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- squishbox/__init__.py +25 -0
- squishbox/apps/__init__.py +0 -0
- squishbox/apps/amsynthbox.py +488 -0
- squishbox/apps/fpatcherbox.py +489 -0
- squishbox/apps/glyphedit.py +100 -0
- squishbox/apps/launcher.py +74 -0
- squishbox/apps/sbcommander.py +218 -0
- squishbox/apps/sbedit.py +122 -0
- squishbox/apps/trackbox.py +438 -0
- squishbox/config.py +80 -0
- squishbox/data/__init__.py +1 -0
- squishbox/data/defaults/amsynthboxconf.yaml +9 -0
- squishbox/data/defaults/fpatcherboxconf.yaml +20 -0
- squishbox/data/defaults/squishboxconf.yaml +86 -0
- squishbox/data/defaults/trackboxconf.yaml +3 -0
- squishbox/data/hardware/v2.yaml +16 -0
- squishbox/data/hardware/v3.yaml +15 -0
- squishbox/data/hardware/v4.yaml +21 -0
- squishbox/data/hardware/v6.yaml +21 -0
- squishbox/hardware.py +579 -0
- squishbox/keys.py +101 -0
- squishbox/midi.py +43 -0
- squishbox/squishbox.py +670 -0
- squishbox-1.0.0.dist-info/METADATA +109 -0
- squishbox-1.0.0.dist-info/RECORD +28 -0
- squishbox-1.0.0.dist-info/WHEEL +5 -0
- squishbox-1.0.0.dist-info/licenses/LICENSE +21 -0
- squishbox-1.0.0.dist-info/top_level.txt +1 -0
squishbox/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""SquishBox Raspberry Pi interface
|
|
2
|
+
|
|
3
|
+
This module provides classes and functions for creating python applications
|
|
4
|
+
for the `SquishBox <https://www.geekfunklabs.com/products/squishbox>`_ ,
|
|
5
|
+
a Raspberry Pi add-on that provides an LCD, pushbutton rotary encoder,
|
|
6
|
+
sound card, and MIDI input/output.
|
|
7
|
+
|
|
8
|
+
Requires:
|
|
9
|
+
- gpiod
|
|
10
|
+
- yaml
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
14
|
+
|
|
15
|
+
from .config import CONFIG
|
|
16
|
+
from .squishbox import SquishBox
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["SquishBox", "CONFIG"]
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
__version__ = version("squishbox")
|
|
23
|
+
except PackageNotFoundError:
|
|
24
|
+
__version__ = "0.0.0-dev"
|
|
25
|
+
|
|
File without changes
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A SquishBox wrapper for amsynth"""
|
|
3
|
+
|
|
4
|
+
from math import log
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from subprocess import Popen, PIPE
|
|
8
|
+
import sys
|
|
9
|
+
from threading import Thread
|
|
10
|
+
|
|
11
|
+
import alsa_midi
|
|
12
|
+
|
|
13
|
+
import squishbox
|
|
14
|
+
from squishbox.config import load_config, save_state
|
|
15
|
+
from squishbox.midi import midi_ports, midi_connect
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
COLS = squishbox.CONFIG["lcd_cols"]
|
|
19
|
+
ROWS = squishbox.CONFIG["lcd_rows"]
|
|
20
|
+
MENU_TIME = squishbox.CONFIG["menu_timeout"]
|
|
21
|
+
|
|
22
|
+
AMSPORT = "amsynth:0(MIDI IN)"
|
|
23
|
+
PARS = {}
|
|
24
|
+
# name cc type default pmin pmax base offset
|
|
25
|
+
for name, cc, typ, default, *s in [r.split() for r in """\
|
|
26
|
+
amp_attack 12 pow 0 0 2.5 3 0.0005
|
|
27
|
+
amp_decay 13 pow 0 0 2.5 3 0.0005
|
|
28
|
+
amp_sustain 14 lin 1 0 1 1 0
|
|
29
|
+
amp_release 15 pow 0 0 2.5 3 0.0005
|
|
30
|
+
osc1_waveform 16 stp 2 0 4 1 0
|
|
31
|
+
filter_attack 17 pow 0 0 2.5 3 0.0005
|
|
32
|
+
filter_decay 18 pow 0 0 2.5 3 0.0005
|
|
33
|
+
filter_sustain 19 lin 1 0 1 1 0
|
|
34
|
+
filter_release 20 pow 0 0 2.5 3 0.0005
|
|
35
|
+
filter_resonance 21 lin 0 0 0.97 1 0
|
|
36
|
+
filter_env_amount 22 lin 0 -16 16 1 0
|
|
37
|
+
filter_cutoff 23 exp 1.5 -0.5 1.5 16 0
|
|
38
|
+
osc2_detune 24 exp 0 -1 1 1.25 0
|
|
39
|
+
osc2_waveform 25 stp 2 0 4 1 0
|
|
40
|
+
master_vol 26 pow 0.67 0 1 2 0
|
|
41
|
+
lfo_freq 27 pow 0 0 7.5 2 0
|
|
42
|
+
lfo_waveform 28 stp 0 0 6 1 0
|
|
43
|
+
osc2_range 29 stp 0 -3 4 2 0
|
|
44
|
+
osc_mix 30 lin 0 -1 1 1 0
|
|
45
|
+
freq_mod_amount 44 pow 0 0 1.26 3 -1
|
|
46
|
+
filter_mod_amount 45 lin -1 -1 1 1 0
|
|
47
|
+
amp_mod_amount 46 lin -1 -1 1 1 0
|
|
48
|
+
osc_mix_mode 47 lin 0 0 1 1 0
|
|
49
|
+
osc1_pulsewidth 48 lin 1 0 1 1 0
|
|
50
|
+
osc2_pulsewidth 49 lin 1 0 1 1 0
|
|
51
|
+
reverb_roomsize 50 lin 0 0 1 1 0
|
|
52
|
+
reverb_damp 51 lin 0 0 1 1 0
|
|
53
|
+
reverb_wet 52 lin 0 0 1 1 0
|
|
54
|
+
reverb_width 53 lin 1 0 1 1 0
|
|
55
|
+
distortion_crunch 54 lin 0 0 0.9 1 0
|
|
56
|
+
osc2_sync 55 stp 0 0 1 1 0
|
|
57
|
+
portamento_time 56 lin 0 0 1 1 0
|
|
58
|
+
keyboard_mode 57 stp 0 0 2 1 0
|
|
59
|
+
osc2_pitch 58 stp 0 -12 12 1 0
|
|
60
|
+
filter_type 59 stp 0 0 4 1 0
|
|
61
|
+
filter_slope 60 stp 1 0 1 1 0
|
|
62
|
+
freq_mod_osc 61 stp 0 0 2 1 0
|
|
63
|
+
filter_kbd_track 62 lin 1 0 1 1 0
|
|
64
|
+
filter_vel_sens 63 lin 1 0 1 1 0
|
|
65
|
+
amp_vel_sens 84 lin 1 0 1 1 0
|
|
66
|
+
portamento_mode 85 stp 0 0 1 1 0
|
|
67
|
+
""".splitlines()]:
|
|
68
|
+
PARS[name] = {
|
|
69
|
+
"cc": int(cc), "default": float(default), "vals": [], "display": []
|
|
70
|
+
}
|
|
71
|
+
pmin, pmax, base, offset = map(float, s)
|
|
72
|
+
for i in range(128):
|
|
73
|
+
val = pmin + (pmax - pmin) * i / 127
|
|
74
|
+
if typ == "stp":
|
|
75
|
+
val = round(val)
|
|
76
|
+
PARS[name]["display"].append(val)
|
|
77
|
+
elif typ == "lin":
|
|
78
|
+
PARS[name]["display"].append(base * val + offset)
|
|
79
|
+
elif typ == "exp":
|
|
80
|
+
PARS[name]["display"].append(base ** val + offset)
|
|
81
|
+
elif typ == "pow":
|
|
82
|
+
PARS[name]["display"].append(val ** base + offset)
|
|
83
|
+
PARS[name]["vals"].append(val)
|
|
84
|
+
# make displayed values pretty
|
|
85
|
+
for name in """\
|
|
86
|
+
osc_mix_mode
|
|
87
|
+
osc1_pulsewidth
|
|
88
|
+
osc2_pulsewidth
|
|
89
|
+
amp_sustain
|
|
90
|
+
filter_resonance
|
|
91
|
+
filter_cutoff
|
|
92
|
+
filter_sustain
|
|
93
|
+
freq_mod_amount
|
|
94
|
+
filter_mod_amount
|
|
95
|
+
amp_mod_amount
|
|
96
|
+
reverb_roomsize
|
|
97
|
+
reverb_damp
|
|
98
|
+
reverb_wet
|
|
99
|
+
reverb_width
|
|
100
|
+
distortion_crunch
|
|
101
|
+
filter_kbd_track
|
|
102
|
+
filter_vel_sens
|
|
103
|
+
amp_vel_sens""".split():
|
|
104
|
+
PARS[name]["display"] = [
|
|
105
|
+
f"{i / 1.27:.0f}%" for i in range(128)
|
|
106
|
+
]
|
|
107
|
+
for name, *display in [r.split() for r in """\
|
|
108
|
+
osc1_waveform sine square triangle noise noise+SH
|
|
109
|
+
osc2_waveform sine square triangle noise noise+SH
|
|
110
|
+
lfo_waveform sine square triangle noise noise+SH saw+ saw-
|
|
111
|
+
osc2_sync off on
|
|
112
|
+
keyboard_mode poly mono legato
|
|
113
|
+
filter_type lowpass highpass bandpass notch bypass
|
|
114
|
+
filter_slope 12dB/oct 24dB/oct
|
|
115
|
+
freq_mod_osc osc1+2 osc1 osc2
|
|
116
|
+
portamento_mode always legato
|
|
117
|
+
""".splitlines()]:
|
|
118
|
+
PARS[name]["display"] = [
|
|
119
|
+
display[round((len(display) - 1) * i / 127)]
|
|
120
|
+
for i in range(128)
|
|
121
|
+
]
|
|
122
|
+
for name in """\
|
|
123
|
+
amp_attack
|
|
124
|
+
amp_decay
|
|
125
|
+
amp_release
|
|
126
|
+
filter_attack
|
|
127
|
+
filter_decay
|
|
128
|
+
filter_release
|
|
129
|
+
portamento_time""".split():
|
|
130
|
+
PARS[name]["display"] = [
|
|
131
|
+
f"{v * 1000:.0f} ms" if v < 1.0 else f"{v:.1f} s"
|
|
132
|
+
for v in PARS[name]["display"]
|
|
133
|
+
]
|
|
134
|
+
PARS["lfo_freq"]["display"] = [
|
|
135
|
+
f"{v:.1f} Hz" for v in PARS["lfo_freq"]["display"]
|
|
136
|
+
]
|
|
137
|
+
PARS["osc2_detune"]["display"] = [
|
|
138
|
+
f"{1200 * log(v, 2):.1f} cents"
|
|
139
|
+
for v in PARS["osc2_detune"]["display"]
|
|
140
|
+
]
|
|
141
|
+
PARS["osc2_pitch"]["display"] = [
|
|
142
|
+
f"{v:+} semitones" for v in PARS["osc2_pitch"]["display"]
|
|
143
|
+
]
|
|
144
|
+
PARS["osc2_range"]["display"] = [
|
|
145
|
+
f"{v:+} octaves" for v in PARS["osc2_range"]["display"]
|
|
146
|
+
]
|
|
147
|
+
PARS["master_vol"]["display"] = [
|
|
148
|
+
f"{20 * log(v, 10):+.1f} dB" if v else "-inf dB"
|
|
149
|
+
for v in PARS["master_vol"]["display"]
|
|
150
|
+
]
|
|
151
|
+
PARS["filter_env_amount"]["display"] = [
|
|
152
|
+
f"{v / 16 * 100:.0f}%"
|
|
153
|
+
for v in PARS["filter_env_amount"]["display"]
|
|
154
|
+
]
|
|
155
|
+
PARS["osc_mix"]["display"] = [
|
|
156
|
+
f"1:{(127 - i) / 1.27:3.0f}% 2:{i / 1.27:3.0f}%"
|
|
157
|
+
for i in range(128)
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def read_bankfile(path):
|
|
162
|
+
presets = {}
|
|
163
|
+
for line in path.read_text().splitlines():
|
|
164
|
+
if line.startswith("<preset> <name>"):
|
|
165
|
+
presetname = line.split(maxsplit=2)[-1]
|
|
166
|
+
presets[presetname] = {
|
|
167
|
+
name: min(
|
|
168
|
+
range(128),
|
|
169
|
+
key=lambda i: abs(
|
|
170
|
+
PARS[name]["vals"][i] - PARS[name]["default"]
|
|
171
|
+
)
|
|
172
|
+
) for name in PARS
|
|
173
|
+
}
|
|
174
|
+
elif line.startswith("<parameter>"):
|
|
175
|
+
name, val = line.split()[1:3]
|
|
176
|
+
presets[presetname][name] = min(
|
|
177
|
+
range(128),
|
|
178
|
+
key=lambda i: abs(PARS[name]["vals"][i] - float(val))
|
|
179
|
+
)
|
|
180
|
+
return presets
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def write_bankfile(path, presets):
|
|
184
|
+
lines = ["amSynth"]
|
|
185
|
+
for presetname, preset in presets.items():
|
|
186
|
+
lines.append(f"<preset> <name> {presetname}")
|
|
187
|
+
for name, val in preset.items():
|
|
188
|
+
lines.append(
|
|
189
|
+
f"<parameter> {name} {PARS[name]['vals'][val]}"
|
|
190
|
+
)
|
|
191
|
+
lines.append("EOF")
|
|
192
|
+
path.write_text("\n".join(lines))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def setup_amsynth():
|
|
196
|
+
"""Shadow config to amsynth configs
|
|
197
|
+
"""
|
|
198
|
+
amscfg = Path("~/.config/amsynth/config").expanduser()
|
|
199
|
+
amscfg.write_text(
|
|
200
|
+
"\n".join(
|
|
201
|
+
[f"{k} {v}" for k, v in CONFIG.items() if k in (
|
|
202
|
+
"midi_channel",
|
|
203
|
+
"sample_rate",
|
|
204
|
+
"polyphony",
|
|
205
|
+
"pitch_bend_range",
|
|
206
|
+
"audio_driver",
|
|
207
|
+
"alsa_audio_device",
|
|
208
|
+
)
|
|
209
|
+
]
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
amsctrl = Path("~/.config/amsynth/controllers").expanduser()
|
|
213
|
+
controllers = ["null"] * 128
|
|
214
|
+
for name in PARS:
|
|
215
|
+
controllers[PARS[name]["cc"]] = name
|
|
216
|
+
amsctrl.write_text("\n".join(controllers))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def start_wrapper():
|
|
220
|
+
"""Create a client for observing/routing MIDI messages
|
|
221
|
+
and insert it between amSynth and anything connected to it
|
|
222
|
+
"""
|
|
223
|
+
conns = squishbox.CONFIG.setdefault("midi_connections", [])
|
|
224
|
+
for i, (src, dest) in enumerate((c.split(">") for c in conns)):
|
|
225
|
+
if dest == AMSPORT:
|
|
226
|
+
conns[i] = f"{src}>_amsynth_wrapper:0(in)"
|
|
227
|
+
conns.append(f"_amsynth_wrapper:1(out)>{AMSPORT}")
|
|
228
|
+
client = alsa_midi.SequencerClient("_amsynth_wrapper")
|
|
229
|
+
inport = client.create_port(
|
|
230
|
+
"in",
|
|
231
|
+
caps=alsa_midi.WRITE_PORT,
|
|
232
|
+
type=alsa_midi.PortType.MIDI_GENERIC,
|
|
233
|
+
)
|
|
234
|
+
outport = client.create_port(
|
|
235
|
+
"out",
|
|
236
|
+
caps=alsa_midi.READ_PORT,
|
|
237
|
+
type=alsa_midi.PortType.MIDI_GENERIC,
|
|
238
|
+
)
|
|
239
|
+
return client, inport, outport
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def remove_wrapper(client):
|
|
243
|
+
client.close()
|
|
244
|
+
conns = squishbox.CONFIG["midi_connections"]
|
|
245
|
+
for i, (src, dest) in enumerate((c.split(">") for c in conns)):
|
|
246
|
+
if dest == "_amsynth_wrapper:0(in)":
|
|
247
|
+
conns[i] = f"{src}>{AMSPORT}"
|
|
248
|
+
conns.remove(f"_amsynth_wrapper:1(out)>{AMSPORT}")
|
|
249
|
+
if squishbox.CONFIG.get("midi_connections") == []:
|
|
250
|
+
del squishbox.CONFIG["midi_connections"]
|
|
251
|
+
midi_connect()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def process_events():
|
|
255
|
+
while True:
|
|
256
|
+
try:
|
|
257
|
+
evt = wrapper.event_input(timeout=0.1)
|
|
258
|
+
except TypeError: # occurs if client is closed
|
|
259
|
+
break
|
|
260
|
+
if evt == None:
|
|
261
|
+
continue
|
|
262
|
+
if midi_learn_callback != None:
|
|
263
|
+
if (
|
|
264
|
+
isinstance(evt, alsa_midi.ControlChangeEvent) and
|
|
265
|
+
CONFIG["midi_channel"] in (evt.channel + 1, 0)
|
|
266
|
+
):
|
|
267
|
+
midi_learn_callback(evt.param)
|
|
268
|
+
elif (
|
|
269
|
+
isinstance(evt, alsa_midi.ControlChangeEvent) and
|
|
270
|
+
CONFIG["midi_channel"] in (evt.channel + 1, 0)
|
|
271
|
+
):
|
|
272
|
+
if evt.param in CONFIG.get("controllers", {}):
|
|
273
|
+
name = CONFIG["controllers"][evt.param]
|
|
274
|
+
send_param(name, evt.value)
|
|
275
|
+
if display_callback != None:
|
|
276
|
+
display_callback((name, evt.value))
|
|
277
|
+
else:
|
|
278
|
+
wrapper.event_output(evt, dest=amsynth_port)
|
|
279
|
+
wrapper.drain_output()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def send_param(name, i):
|
|
283
|
+
curvals[name] = i
|
|
284
|
+
evt = alsa_midi.ControlChangeEvent(
|
|
285
|
+
channel=(CONFIG["midi_channel"] or 1) - 1,
|
|
286
|
+
param=PARS[name]["cc"],
|
|
287
|
+
value=i,
|
|
288
|
+
)
|
|
289
|
+
wrapper.event_output(evt, dest=amsynth_port)
|
|
290
|
+
wrapper.drain_output()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def set_preset(presetname):
|
|
294
|
+
for name, i in presets[presetname].items():
|
|
295
|
+
curvals[name] = i
|
|
296
|
+
send_param(name, i)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def refresh_display():
|
|
300
|
+
sb.lcd.write(pname.ljust(COLS), row=0)
|
|
301
|
+
sb.lcd.write(f"patch {pno + 1}/{len(presets)}".rjust(COLS), row=1)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# start squishbox
|
|
305
|
+
sb = squishbox.SquishBox()
|
|
306
|
+
sb.lcd.clear()
|
|
307
|
+
|
|
308
|
+
CONFIG = load_config("amsynthbox.yaml")
|
|
309
|
+
if not CONFIG["banks_path"].exists():
|
|
310
|
+
CONFIG["banks_path"].mkdir(parents=True, exist_ok=True)
|
|
311
|
+
Path(CONFIG["banks_path"] / "presets").symlink_to(
|
|
312
|
+
"/usr/share/amsynth/banks"
|
|
313
|
+
)
|
|
314
|
+
Path("/usr/share/amsynth/banks/amsynth_factory.bank").copy(
|
|
315
|
+
CONFIG["banks_path"] / "amsynth_factory.bank"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# start amsynth
|
|
319
|
+
setup_amsynth()
|
|
320
|
+
try:
|
|
321
|
+
amsynthx = Popen(
|
|
322
|
+
["stdbuf", "-oL", "amsynth", "-x"],
|
|
323
|
+
stdout=PIPE, stderr=PIPE,
|
|
324
|
+
text=True, bufsize=1
|
|
325
|
+
)
|
|
326
|
+
except FileNotFoundError as e:
|
|
327
|
+
sb.display_error(e, "install with 'apt install amsynth'")
|
|
328
|
+
sys.exit()
|
|
329
|
+
for line in amsynthx.stdout:
|
|
330
|
+
if "headless mode" in line:
|
|
331
|
+
break
|
|
332
|
+
amsynth_port = midi_ports()[AMSPORT]
|
|
333
|
+
wrapper, wrapper_in, wrapper_out = start_wrapper()
|
|
334
|
+
midithread = Thread(target=process_events, daemon=True)
|
|
335
|
+
display_callback = sb.add_action
|
|
336
|
+
midi_learn_callback = None
|
|
337
|
+
midithread.start()
|
|
338
|
+
|
|
339
|
+
presets = read_bankfile(
|
|
340
|
+
CONFIG["banks_path"] / CONFIG["currentbank_path"]
|
|
341
|
+
)
|
|
342
|
+
curvals = {name: 0 for name in PARS}
|
|
343
|
+
pno = 0
|
|
344
|
+
set_preset(pname := list(presets)[pno])
|
|
345
|
+
|
|
346
|
+
last = 0
|
|
347
|
+
lastpar = 0
|
|
348
|
+
refresh_display()
|
|
349
|
+
while True:
|
|
350
|
+
match sb.get_action():
|
|
351
|
+
case "inc":
|
|
352
|
+
pno = (pno + 1) % len(presets)
|
|
353
|
+
set_preset(pname := list(presets)[pno])
|
|
354
|
+
refresh_display()
|
|
355
|
+
case "dec":
|
|
356
|
+
pno = (pno - 1) % len(presets)
|
|
357
|
+
set_preset(pname := list(presets)[pno])
|
|
358
|
+
refresh_display()
|
|
359
|
+
case "back":
|
|
360
|
+
if sb.menu_exit() == "shell":
|
|
361
|
+
amsynthx.terminate()
|
|
362
|
+
break
|
|
363
|
+
case name, val:
|
|
364
|
+
sb.lcd.write(
|
|
365
|
+
name[:COLS].ljust(COLS),
|
|
366
|
+
row=0, timeout=MENU_TIME
|
|
367
|
+
)
|
|
368
|
+
sb.lcd.write(
|
|
369
|
+
PARS[name]["display"][val].rjust(COLS),
|
|
370
|
+
row=1, timeout=MENU_TIME
|
|
371
|
+
)
|
|
372
|
+
case "select":
|
|
373
|
+
display_callback = None
|
|
374
|
+
i, choice = sb.menu_choose([
|
|
375
|
+
"Parameters",
|
|
376
|
+
"MIDI Learn..",
|
|
377
|
+
"Save Preset",
|
|
378
|
+
"Delete Preset",
|
|
379
|
+
"Load Bank",
|
|
380
|
+
"Save Bank",
|
|
381
|
+
"System Menu..",
|
|
382
|
+
], row=1, i=last)
|
|
383
|
+
last = i if choice != None else last
|
|
384
|
+
if choice == "Parameters":
|
|
385
|
+
while True:
|
|
386
|
+
i, par = sb.menu_choose(
|
|
387
|
+
[PARS[name]["display"][curvals[name]] for name in PARS],
|
|
388
|
+
row=1, i=lastpar, timeout=0,
|
|
389
|
+
func = lambda i: sb.lcd.write(
|
|
390
|
+
list(PARS)[i].ljust(COLS), row=0
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
if par == None:
|
|
394
|
+
break
|
|
395
|
+
lastpar = i
|
|
396
|
+
name = list(PARS)[i]
|
|
397
|
+
sb.lcd.write(f"{name[-15:]}:".ljust(COLS), row=0)
|
|
398
|
+
opts = list(dict.fromkeys(PARS[name]["display"]))
|
|
399
|
+
res = sb.menu_choose(
|
|
400
|
+
opts, row=1, wrap=False, timeout=0,
|
|
401
|
+
i=opts.index(PARS[name]["display"][curvals[name]]),
|
|
402
|
+
func=lambda i: send_param(
|
|
403
|
+
name, PARS[name]["display"].index(opts[i])
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
if res[1] == None:
|
|
407
|
+
break
|
|
408
|
+
elif choice == "MIDI Learn..":
|
|
409
|
+
ctrls = {v: k for k, v in CONFIG.get("controllers", {}).items()}
|
|
410
|
+
ccs = [str(ctrls.get(par, "not mapped")) for par in PARS]
|
|
411
|
+
i, par = sb.menu_choose(
|
|
412
|
+
list(PARS), row=0, i=lastpar, align="left",
|
|
413
|
+
func=lambda i: sb.lcd.write(
|
|
414
|
+
ccs[i].rjust(COLS), row=1
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
if par != None:
|
|
418
|
+
lastpar=i
|
|
419
|
+
midi_learn_callback = sb.add_action
|
|
420
|
+
i, cc = sb.menu_choose(range(128), row=1, i=ctrls.get(par, 0))
|
|
421
|
+
CONFIG.setdefault("controllers", {})
|
|
422
|
+
if isinstance(cc, int):
|
|
423
|
+
CONFIG["controllers"][cc] = par
|
|
424
|
+
elif i > -1:
|
|
425
|
+
CONFIG["controllers"].pop(cc, None)
|
|
426
|
+
if CONFIG["controllers"] == {}:
|
|
427
|
+
del CONFIG["controllers"]
|
|
428
|
+
save_state(CONFIG_PATH, CONFIG)
|
|
429
|
+
midi_learn_callback = None
|
|
430
|
+
elif choice == "Save Preset":
|
|
431
|
+
sb.lcd.write("Save preset as:".ljust(COLS), row=0)
|
|
432
|
+
newname = sb.menu_entertext(pname).strip()
|
|
433
|
+
if sb.menu_confirm(newname):
|
|
434
|
+
pname = newname
|
|
435
|
+
presets[pname] = curvals.copy()
|
|
436
|
+
pno = list(presets).index(pname)
|
|
437
|
+
elif choice == "Delete Preset":
|
|
438
|
+
sb.lcd.write("Delete preset:".ljust(COLS), row=0)
|
|
439
|
+
if sb.menu_confirm(pname):
|
|
440
|
+
del presets[pname]
|
|
441
|
+
pno = min(pno, len(presets) - 1)
|
|
442
|
+
set_preset(pname := list(presets)[pno])
|
|
443
|
+
elif choice == "Load Bank":
|
|
444
|
+
f = sb.menu_choosefile(
|
|
445
|
+
topdir=CONFIG["banks_path"],
|
|
446
|
+
start=CONFIG["currentbank_path"],
|
|
447
|
+
)
|
|
448
|
+
if f.is_file():
|
|
449
|
+
try:
|
|
450
|
+
presets = read_bankfile(f)
|
|
451
|
+
except Exception as e:
|
|
452
|
+
sb.display_error(e, "bank load error")
|
|
453
|
+
else:
|
|
454
|
+
CONFIG["currentbank_path"] = f
|
|
455
|
+
save_state(CONFIG_PATH, CONFIG)
|
|
456
|
+
pno = 0
|
|
457
|
+
set_preset(pname := list(presets)[pno])
|
|
458
|
+
elif choice == "Save Bank":
|
|
459
|
+
f = sb.menu_choosefile(
|
|
460
|
+
topdir=CONFIG["banks_path"],
|
|
461
|
+
start=CONFIG["currentbank_path"]
|
|
462
|
+
)
|
|
463
|
+
name = sb.menu_entertext(
|
|
464
|
+
f.name if f.is_file() else "", charset=sb.lcd.fnchars()
|
|
465
|
+
).strip()
|
|
466
|
+
if name and sb.menu_confirm(name):
|
|
467
|
+
sb.lcd.write(name.ljust(COLS), row=0)
|
|
468
|
+
try:
|
|
469
|
+
write_bankfile(f.parent / name, presets)
|
|
470
|
+
except Exception as e:
|
|
471
|
+
sb.display_error(e, "bank save error")
|
|
472
|
+
else:
|
|
473
|
+
CONFIG["currentbank_path"] = f.parent / name
|
|
474
|
+
save_state(CONFIG_PATH, CONFIG)
|
|
475
|
+
sb.lcd.write("bank saved".ljust(COLS), row=1)
|
|
476
|
+
sb.get_action(timeout=MENU_TIME)
|
|
477
|
+
elif choice == "System Menu..":
|
|
478
|
+
remove_wrapper(wrapper)
|
|
479
|
+
midithread.join()
|
|
480
|
+
if sb.menu_systemsettings() == "shell":
|
|
481
|
+
amsynthx.terminate()
|
|
482
|
+
break
|
|
483
|
+
wrapper, wrapper_in, wrapper_out = start_wrapper()
|
|
484
|
+
midithread = Thread(target=process_events, daemon=True)
|
|
485
|
+
midithread.start()
|
|
486
|
+
display_callback = sb.add_action
|
|
487
|
+
refresh_display()
|
|
488
|
+
|