diff --git a/icons/audio_input.png b/icons/audio_input.png new file mode 100644 index 000000000..7db5701ca Binary files /dev/null and b/icons/audio_input.png differ diff --git a/icons/audio_options.png b/icons/audio_options.png new file mode 100644 index 000000000..81d1d885e Binary files /dev/null and b/icons/audio_options.png differ diff --git a/icons/audio_output.png b/icons/audio_output.png new file mode 100644 index 000000000..d9d02db80 Binary files /dev/null and b/icons/audio_output.png differ diff --git a/icons/audio_processor.png b/icons/audio_processor.png new file mode 100644 index 000000000..93738d954 Binary files /dev/null and b/icons/audio_processor.png differ diff --git a/icons/delete_icons.xcf b/icons/delete_icons.xcf deleted file mode 100644 index c562de529..000000000 Binary files a/icons/delete_icons.xcf and /dev/null differ diff --git a/icons/folder.png b/icons/folder.png new file mode 100644 index 000000000..937a0f061 Binary files /dev/null and b/icons/folder.png differ diff --git a/icons/metronome.png b/icons/metronome.png new file mode 100644 index 000000000..349b03b68 Binary files /dev/null and b/icons/metronome.png differ diff --git a/icons/metronome.svg b/icons/metronome.svg new file mode 100644 index 000000000..b0339a1ac --- /dev/null +++ b/icons/metronome.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/icons/midi_input.png b/icons/midi_input.png new file mode 100644 index 000000000..b61f223fe Binary files /dev/null and b/icons/midi_input.png differ diff --git a/icons/midi_output.png b/icons/midi_output.png new file mode 100644 index 000000000..8526c2c92 Binary files /dev/null and b/icons/midi_output.png differ diff --git a/icons/midi_processor.png b/icons/midi_processor.png new file mode 100644 index 000000000..1c999eb8c Binary files /dev/null and b/icons/midi_processor.png differ diff --git a/icons/note_range.png b/icons/note_range.png new file mode 100644 index 000000000..8b2830560 Binary files /dev/null and b/icons/note_range.png differ diff --git a/icons/synth_processor.png b/icons/synth_processor.png new file mode 100644 index 000000000..c3bfc606e Binary files /dev/null and b/icons/synth_processor.png differ diff --git a/zynautoconnect/zynthian_autoconnect.py b/zynautoconnect/zynthian_autoconnect.py index 8294d18e0..b01f5c22b 100755 --- a/zynautoconnect/zynthian_autoconnect.py +++ b/zynautoconnect/zynthian_autoconnect.py @@ -25,20 +25,19 @@ import os import re import usb +import json import jack +import psutil +import pexpect import logging import alsaaudio -import json from time import sleep from threading import Thread, Lock -from subprocess import check_output -import pexpect -import psutil # Zynthian specific modules +import zynconf from zyncoder.zyncore import lib_zyncore from zyngui import zynthian_gui_config -import zynconf # ------------------------------------------------------------------------------- # Configure logging @@ -56,6 +55,7 @@ # Fake port class # ------------------------------------------------------------------------------- + class fake_port: def __init__(self, name): self.name = name @@ -133,6 +133,7 @@ def unset_alias(self, alias): # MIDI port helper functions + def get_port_friendly_name(uid): """Get port friendly name @@ -159,7 +160,7 @@ def set_port_friendly_name(port, friendly_name=None): try: alias1 = port.aliases[0] - if friendly_name is None: + if not friendly_name: # Reset name if alias1 in midi_port_names: midi_port_names.pop(alias1) @@ -349,6 +350,7 @@ def update_system_audio_aliases(): port.unset_alias(a) port.set_alias(alias) + def add_sidechain_ports(jackname): """Add ports that should be treated as sidechain inputs @@ -951,9 +953,11 @@ def audio_autoconnect(): # Release Mutex Lock release_lock() + def get_hw_audio_dst_ports(): return jclient.get_ports("system:playback", is_input=True, is_audio=True, is_physical=True) + jclient.get_ports("zynaout", is_input=True, is_audio=True) + def update_hw_audio_ports(): global alsa_audio_srcs, alsa_audio_dests @@ -1007,17 +1011,20 @@ def update_hw_audio_ports(): return dirty + def enable_hotplug(): zynthian_gui_config.hotplug_audio_enabled = True zynconf.save_config({"ZYNTHIAN_HOTPLUG_AUDIO": str(zynthian_gui_config.hotplug_audio_enabled)}) update_hw_audio_ports() audio_autoconnect() + def disable_hotplug(): zynthian_gui_config.hotplug_audio_enabled = False zynconf.save_config({"ZYNTHIAN_HOTPLUG_AUDIO": str(zynthian_gui_config.hotplug_audio_enabled)}) stop_all_alsa_in_out() + def enable_audio_input_device(device, enable=True): if enable: if start_alsa_in(device): @@ -1029,6 +1036,7 @@ def enable_audio_input_device(device, enable=True): zynthian_gui_config.disabled_audio_in.append(device) zynconf.save_config({"ZYNTHIAN_HOTPLUG_AUDIO_DISABLED_IN": ",".join(zynthian_gui_config.disabled_audio_in)}) + def enable_audio_output_device(device, enable=True): if enable: if start_alsa_out(device): @@ -1040,6 +1048,7 @@ def enable_audio_output_device(device, enable=True): zynthian_gui_config.disabled_audio_out.append(device) zynconf.save_config({"ZYNTHIAN_HOTPLUG_AUDIO_DISABLED_OUT": ",".join(zynthian_gui_config.disabled_audio_out)}) + def get_alsa_hotplug_audio_devices(playback=True): devices = [] for card in alsaaudio.pcms(alsaaudio.PCM_PLAYBACK if playback else alsaaudio.PCM_CAPTURE): @@ -1051,6 +1060,7 @@ def get_alsa_hotplug_audio_devices(playback=True): devices.append(device) return devices + def start_alsa_in(device): global alsa_audio_srcs if device in alsa_audio_srcs: @@ -1069,6 +1079,7 @@ def start_alsa_in(device): logging.warning(f"Failed to set {device} aliases") return True + def stop_alsa_in(device): global alsa_audio_srcs if device not in alsa_audio_srcs: @@ -1077,6 +1088,7 @@ def stop_alsa_in(device): alsa_audio_srcs.pop(device) return True + def start_alsa_out(device): global alsa_audio_dests if device in alsa_audio_dests: @@ -1095,6 +1107,7 @@ def start_alsa_out(device): logging.warning(f"Failed to set {device} aliases") return True + def stop_alsa_out(device): global alsa_audio_dests if device not in alsa_audio_dests: @@ -1103,22 +1116,23 @@ def stop_alsa_out(device): alsa_audio_dests.pop(device) return True + def stop_all_alsa_in_out(): for device in get_alsa_hotplug_audio_devices(False): stop_alsa_in(device) for device in get_alsa_hotplug_audio_devices(True): stop_alsa_out(device) + # Connect mixer to the ffmpeg recorder def audio_connect_ffmpeg(timeout=2.0): t = 0 while t < timeout: try: # TODO: Do we want post fader, post effects feed? - jclient.connect( - f"zynmixer:output_{MAIN_MIX_CHAN}a", "ffmpeg:input_1") - jclient.connect( - f"zynmixer:output_{MAIN_MIX_CHAN}b", "ffmpeg:input_2") + # => It's just for recording video tutorials, but if the recorded video is about post-fader effects ... + jclient.connect(f"zynmixer:output_{MAIN_MIX_CHAN}a", "ffmpeg:input_1") + jclient.connect(f"zynmixer:output_{MAIN_MIX_CHAN}b", "ffmpeg:input_2") return except: sleep(0.1) @@ -1360,6 +1374,7 @@ def init(): update_midi_in_dev_mode_all() update_system_audio_aliases() + def start(sm): """Initialise autoconnect and start MIDI port checker diff --git a/zynconf/zynthian_config.py b/zynconf/zynthian_config.py index 65624c26f..f0efd23cd 100755 --- a/zynconf/zynthian_config.py +++ b/zynconf/zynthian_config.py @@ -205,11 +205,10 @@ def load_config(set_env=True, fpath=None): res = pattern.match(line) if res: varnames.append(res.group(1)) - # logging.debug("CONFIG VARNAME: %s" % res.group(1)) + #logging.debug(f"CONFIG VARNAMES: {varnames}") # Execute config script and dump environment - env = check_output("source \"{}\";env".format( - fpath), shell=True, universal_newlines=True, executable="/bin/bash") + env = check_output("source \"{}\";env".format(fpath), shell=True, universal_newlines=True, executable="/bin/bash") # Parse environment dump config = {} @@ -281,11 +280,38 @@ def save_config(config, updsys=False, fpath=None): update_sys() +def load_plain_envars(fpath, set_env=True): + # Get config file content + with open(fpath) as f: + lines = f.readlines() + + # Parse plain envar assignment with or without export prefix + config = {} + pattern = re.compile("^([^#]*?)=(.*)") + for line in lines: + res = pattern.match(line) + if res: + parts = res.group(1).split(" ", maxsplit=1) + if len(parts) > 1: + if parts[0] == "export": + varname = parts[1] + else: + continue + else: + varname = res.group(1) + value = res.group(2).strip('\"').strip('\'') + config[varname] = value + # Set local environment + if set_env: + os.environ[varname] = value + #logging.debug(f"CONFIG: {config}") + return config + + def update_sys(): try: os.environ['ZYNTHIAN_FLAG_MASTER'] = "NONE" - check_output(os.environ.get('ZYNTHIAN_SYS_DIR') + - "/scripts/update_zynthian_sys.sh", shell=True) + check_output(os.environ.get('ZYNTHIAN_SYS_DIR') + "/scripts/update_zynthian_sys.sh", shell=True) except Exception as e: logging.error("Updating Sytem Config: %s" % e) @@ -337,14 +363,12 @@ def get_wifi_list(): # and create it if needed if "zynthian-ap" not in configured_wifi: logging.info("Creating Wi-Fi Access Point connection 'zynthian'...") - check_output( - f"{sys_dir}/sbin/create_wifi_access_point.sh", encoding='utf-8') + check_output(f"{sys_dir}/sbin/create_wifi_access_point.sh", encoding='utf-8') # Get list of available networks wifi_data = [] ap_enabled = False - rows = check_output(["nmcli", "--terse", "dev", "wifi", - "list"], encoding='utf-8').split("\n") + rows = check_output(["nmcli", "--terse", "dev", "wifi", "list"], encoding='utf-8').split("\n") for row in rows: parts = row.split(":") if len(parts) > 8: diff --git a/zyngine/zynthian_chain.py b/zyngine/zynthian_chain.py index 5e9f7fbdd..b6f552c16 100644 --- a/zyngine/zynthian_chain.py +++ b/zyngine/zynthian_chain.py @@ -95,7 +95,7 @@ def reset(self): self.title = "Main" self.audio_in = [] # Default use first two physical audio outputs - self.audio_out = ["system:playback_[1,2]$"] + self.audio_out = ["^system:playback_1$|^system:playback_2$"] self.audio_thru = True else: self.title = "" diff --git a/zyngine/zynthian_controller.py b/zyngine/zynthian_controller.py index b02d4c3c8..8fbc8fa7a 100644 --- a/zyngine/zynthian_controller.py +++ b/zyngine/zynthian_controller.py @@ -160,11 +160,7 @@ def set_options(self, options): if 'midi_chan' in options: self.midi_chan = options['midi_chan'] if 'midi_cc' in options: - cc = options['midi_cc'] - if isinstance(cc, str): - self.osc_path = cc - else: - self.midi_cc = cc + self.midi_cc = options['midi_cc'] if 'osc_port' in options: self.osc_port = options['osc_port'] if 'osc_path' in options: @@ -375,6 +371,10 @@ def set_value(self, val, send=True): if old_val == self.value: return + self.send_value(send) + self.is_dirty = True + + def send_value(self, send=True): mval = None if self.engine and send: # Send value using engine method... @@ -385,23 +385,19 @@ def set_value(self, val, send=True): try: if self.osc_path: # logging.debug("Sending OSC Controller '{}', {} => {}".format(self.symbol, self.osc_path, self.get_ctrl_osc_val())) - liblo.send(self.engine.osc_target, - self.osc_path, self.get_ctrl_osc_val()) + liblo.send(self.engine.osc_target, self.osc_path, self.get_ctrl_osc_val()) elif self.midi_cc: mval = self.get_ctrl_midi_val() # logging.debug("Sending MIDI Controller '{}', CH{}#CC{}={}".format(self.symbol, self.midi_chan, self.midi_cc, mval)) self.send_midi_cc(mval) except Exception as e: - logging.warning( - "Can't send controller '{}' => {}".format(self.symbol, e)) + logging.warning("Can't send controller '{}' => {}".format(self.symbol, e)) # Send feedback to MIDI controllers => What MIDI controllers? Those selected as MIDI-out? # TODO: Set midi_feeback to MIDI learn if self.midi_feedback: self.send_midi_feedback(mval) - self.is_dirty = True - def send_midi_cc(self, mval=None): if mval is None: mval = self.get_ctrl_midi_val() @@ -614,6 +610,12 @@ def midi_cc_mode_detect(self, val): #logging.debug(f"CC val={val} => current mode={self.midi_cc_mode}, detecting mode {self.midi_cc_mode_detecting}" # f" (count {self.midi_cc_mode_detecting_count}, zero {self.midi_cc_mode_detecting_zero})\n") + # Always use absolute mode with toggle controllers + if self.is_toggle: + self.midi_cc_mode = 0 + self.midi_cc_mode_detecting = 0 + return + # Mode autodetection timeout now = monotonic() if now - self.midi_cc_mode_detecting_ts > MIDI_CC_MODE_DETECT_TIMEOUT: diff --git a/zyngine/zynthian_engine.py b/zyngine/zynthian_engine.py index 8c41a3d5b..2abf0fa50 100644 --- a/zyngine/zynthian_engine.py +++ b/zyngine/zynthian_engine.py @@ -26,18 +26,17 @@ import re import json import glob +import copy import liblo import logging import pexpect import fnmatch from time import sleep -from string import Template -from os.path import isfile, isdir, ismount, join +from os.path import isfile, isdir, join import zynautoconnect from . import zynthian_controller from zyngui import zynthian_gui_config -from zyncoder.zyncore import lib_zyncore # -------------------------------------------------------------------------------- # Basic Engine Class: Spawn a process & manage IPC communication using pexpect @@ -572,12 +571,36 @@ def load_preset_favs(self): # Controllers Management # --------------------------------------------------------------------------- + def get_ctrl_options(self, ctrl, processor): + if isinstance(ctrl[1], dict): + build_from_options = True + options = copy.copy(ctrl[1]) + else: + build_from_options = False + options = {} + if isinstance(ctrl[1], int) and ctrl[1] > 0: + options["midi_cc"] = ctrl[1] + + options["processor"] = processor + options["midi_chan"] = processor.get_midi_chan() + if build_from_options: + return options + + # Add extra options depending on array length ... + if len(ctrl) > 4 and ctrl[0] in processor.controllers_dict: + # optional param 4 is graph path + options['graph_path'] = ctrl[4] + if len(ctrl) > 3: + # optional param 3 is called value_max but actually could be a configuration object + options['value_max'] = ctrl[3] + if len(ctrl) > 2: + options['value'] = ctrl[2] + return options + # Get zynthian controllers dictionary. # Updates existing processor dictionary. # + Default implementation uses a static controller definition array def get_controllers_dict(self, processor): - midich = processor.get_midi_chan() - if self._ctrls is not None: # Remove controls that are no longer used for symbol in list(processor.controllers_dict): @@ -591,63 +614,15 @@ def get_controllers_dict(self, processor): else: processor.controllers_dict[symbol].reset(self, symbol) + # Regenerate / update controller dictionary for ctrl in self._ctrls: - cc = None - options = {} - build_from_options = False - if isinstance(ctrl[1], dict): - options = ctrl[1] - build_from_options = True - # OSC control => - elif isinstance(ctrl[1], str): - # replace variables ... - tpl = Template(ctrl[1]) - cc = tpl.safe_substitute(ch=midich) - try: - cc = tpl.safe_substitute(i=processor.part_i) - except: - pass - # set osc_port option ... - if self.osc_target_port > 0: - options['osc_port'] = self.osc_target_port - # debug message - logging.debug('CONTROLLER %s OSC PATH => %s' % - (ctrl[0], cc)) - # MIDI Control => - else: - cc = ctrl[1] - - options["processor"] = processor - options["midi_chan"] = midich - if cc is not None: - options["midi_cc"] = cc - - # Build controller depending on array length ... + options = self.get_ctrl_options(ctrl, processor) + # Controller already exists so reconfigure with new settings if ctrl[0] in processor.controllers_dict: - # Controller already exists so reconfigure with new settings zctrl = processor.controllers_dict[ctrl[0]] - if build_from_options: - zctrl.set_options(options) - elif len(ctrl) > 3: - options['value'] = ctrl[2] - options['value_max'] = ctrl[3] - zctrl.set_options(options) - elif len(ctrl) > 2: - options['value'] = ctrl[2] - zctrl.set_options(options) - + zctrl.set_options(options) + # Create new controller else: - if not build_from_options: - if len(ctrl) > 4: - # optional param 4 is graph path - options['graph_path'] = ctrl[4] - if len(ctrl) > 3: - # optional param 3 is called value_max but actually could be a configuration object - options['value_max'] = ctrl[3] - if len(ctrl) > 2: - # param 2 is zctrl value - options['value'] = ctrl[2] - # param 0 is symbol string, param 1 is options or midi cc or osc path zctrl = zynthian_controller(self, ctrl[0], options) processor.controllers_dict[zctrl.symbol] = zctrl if zctrl.midi_cc is not None: diff --git a/zyngine/zynthian_engine_sooperlooper.py b/zyngine/zynthian_engine_sooperlooper.py index 0c4b07f3c..46687b7b3 100644 --- a/zyngine/zynthian_engine_sooperlooper.py +++ b/zyngine/zynthian_engine_sooperlooper.py @@ -378,18 +378,18 @@ def __init__(self, state_manager=None): loop_labels.append(str(i + 1)) self._ctrls = [ #symbol, {options}, midi_cc - ['record', {'name': 'record', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 102], - ['overdub', {'name': 'overdub', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 103], - ['multiply', {'name': 'multiply', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 104], - ['replace', {'name': 'replace', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 105], - ['substitute', {'name': 'substitute', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 106], - ['insert', {'name': 'insert', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 107], + ['record', {'name': 'record', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], + ['overdub', {'name': 'overdub', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], + ['multiply', {'name': 'multiply', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], + ['replace', {'name': 'replace', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], + ['substitute', {'name': 'substitute', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], + ['insert', {'name': 'insert', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], ['undo/redo', {'value': 1, 'labels': ['<', '<>', '>']}], ['prev/next', {'value': 63, 'value_max': 127, 'labels': ['<', '<>', '>']}], - ['trigger', {'name': 'trigger', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 108], - ['mute', {'name': 'mute', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 109], - ['oneshot', {'name': 'oneshot', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 110], - ['pause', {'name': 'pause', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 111], + ['trigger', {'name': 'trigger', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], + ['mute', {'name': 'mute', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], + ['oneshot', {'name': 'oneshot', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], + ['pause', {'name': 'pause', 'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}], ['reverse', {'name': 'direction', 'value': 0, 'labels': ['reverse', 'forward'], 'ticks':[1, 0], 'is_toggle': True}], ['rate', {'name': 'speed', 'value': 1.0, 'value_min': 0.25, 'value_max': 4.0, 'is_integer': False, 'nudge_factor': 0.01}], ['stretch_ratio', {'name': 'stretch', 'value': 1.0, 'value_min': 0.5, 'value_max': 4.0, 'is_integer': False, 'nudge_factor': 0.01}], @@ -414,7 +414,7 @@ def __init__(self, state_manager=None): ['loop_count', {'name': 'loop count', 'value': 1, 'value_min': 1, 'value_max': self.MAX_LOOPS}], ['selected_loop_num', {'name': 'selected loop', 'value': 1, 'value_min': 1, 'value_max': 6}], ['single_pedal', {'name': 'single pedal', 'value': 0, 'value_max': 1, 'labels': ['>', '<'], 'is_toggle': True}], - ['global_cc', {'name': 'direct cc', 'value': 1, 'labels':['off', 'on']}] + ['global_cc', {'name': 'midi cc to selected loop', 'value': 1, 'labels':['off', 'on']}] ] # Controller Screens @@ -460,11 +460,6 @@ def start(self): # Request current quantity of loops self.osc_server.send(self.osc_target, '/ping', ('s', self.osc_server_url), ('s', '/info')) - if self.config_remote_display(): - self.proc_gui = Popen("slgui", stdout=DEVNULL, stderr=DEVNULL, env=self.command_env, cwd=self.command_cwd) - else: - self.proc_gui = None - def stop(self): if self.proc: try: @@ -477,17 +472,6 @@ def stop(self): self.proc = None except Exception as err: logging.error(f"Can't stop engine {self.name} => {err}") - if self.proc_gui: - try: - logging.info("Stoping SLGUI") - self.proc_gui.terminate() - try: - self.proc_gui.wait(0.2) - except: - self.proc_gui.kill() - self.proc = None - except Exception as err: - logging.error(f"Can't stop engine {self.name} => {err}") self.osc_end() # --------------------------------------------------------------------------- @@ -600,7 +584,6 @@ def get_controllers_dict(self, processor): return processor.controllers_dict def send_controller_value(self, zctrl): - #logging.warning(f"{zctrl.symbol} {zctrl.value}") if zctrl.symbol == "global_cc": self.global_cc_binding = zctrl.value != 0 return @@ -659,7 +642,7 @@ def send_controller_value(self, zctrl): if self.pedal_taps: self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'undo_all')) elif symbol == 'selected_loop_num': - self.select_loop(zctrl.value - 1, False) + self.select_loop(zctrl.value - 1, True) elif symbol in self.SL_LOOP_PARAMS: # Selected loop self.osc_server.send(self.osc_target, f'/sl/{chan}/set', ('s', symbol), ('f', zctrl.value)) elif symbol in self.SL_LOOP_GLOBAL_PARAMS: # All loops @@ -710,7 +693,6 @@ def cb_osc_all(self, path, args, types, src): return try: processor = self.processors[0] - logging.warning(f"Rx OSC => {path} {args}") if path == '/state': # args: i:Loop index, s:control, f:value logging.debug("Loop State: %d %s=%0.1f", args[0], args[1], args[2]) @@ -769,7 +751,7 @@ def cb_osc_all(self, path, args, types, src): self.select_loop(self.loop_count - 1, True) self.osc_server.send(self.osc_target, '/get', ('s', 'sync_source'), ('s', self.osc_server_url), ('s', '/control')) - if self.selected_loop > self.loop_count: + if self.selected_loop is not None and self.selected_loop > self.loop_count: self.select_loop(self.loop_count - 1, True) self.monitors_dict['loop_count'] = self.loop_count @@ -802,8 +784,6 @@ def cb_osc_all(self, path, args, types, src): self.monitors_dict[args[1]] = args[2] else: self.monitors_dict[f"{args[1]}_{args[0]}"] = args[2] - #if args[1] in ['loop_len', 'rate_output', 'mute']: - # logging.warning("Monitor: Loop %d %s=%0.2f", args[0], args[1], args[2]) elif path == 'error': logging.error(f"SooperLooper daemon error: {args[0]}") except Exception as e: @@ -821,7 +801,7 @@ def update_state(self, loop): return try: current_state = self.state[loop] - logging.warning(f"loop: {loop} state: {current_state}") + #logging.warning(f"loop: {loop} state: {current_state}") # Turn off all controllers that are off in this state for symbol in self.SL_STATES[current_state]['ctrl_off']: if symbol in self.SL_LOOP_SEL_PARAM: diff --git a/zyngine/zynthian_engine_zynaddsubfx.py b/zyngine/zynthian_engine_zynaddsubfx.py index 56db836f9..a20d65216 100644 --- a/zyngine/zynthian_engine_zynaddsubfx.py +++ b/zyngine/zynthian_engine_zynaddsubfx.py @@ -26,6 +26,7 @@ import shutil import logging from time import sleep +from string import Template from os.path import isfile, join from subprocess import check_output @@ -49,10 +50,10 @@ class zynthian_engine_zynaddsubfx(zynthian_engine): # MIDI Controllers _ctrls = [ - ['volume', 7, 115], + #['volume', 7, 115], # ['panning', 10, 64], # ['expression', 11, 127], - # ['volume', '/part$i/Pvolume', 96], + ['volume', '/part$i/Pvolume', 96, 127, {'midi_cc': 7}], ['panning', '/part$i/Ppanning', 64], ['filter cutoff', 74, 64], ['filter resonance', 71, 64], @@ -182,15 +183,12 @@ def reset(self): def add_processor(self, processor): self.processors.append(processor) - try: - processor.part_i = self.get_free_parts()[0] - processor.jackname = "{}:part{}/".format( - self.jackname, processor.part_i) - processor.refresh_controllers() - logging.debug("ADD processor => Part {} ({})".format( - processor.part_i, self.jackname)) - except Exception as e: - logging.error(f"Unable to add processor to engine - {e}") + processor.part_i = self.get_free_parts()[0] + processor.jackname = "{}:part{}/".format(self.jackname, processor.part_i) + processor.refresh_controllers() + self.enable_part(processor) + processor.send_controller_values() + logging.debug("ADD processor => Part {} ({})".format(processor.part_i, self.jackname)) def remove_processor(self, processor): self.disable_part(processor.part_i) @@ -204,7 +202,9 @@ def remove_processor(self, processor): def set_midi_chan(self, processor): if self.osc_server and processor.part_i is not None: lib_zyncore.zmop_set_midi_chan_trans( - processor.chain.zmop_index, processor.get_midi_chan(), processor.part_i) + processor.chain.zmop_index, + processor.get_midi_chan(), + processor.part_i) # ---------------------------------------------------------------------------- # Preset Managament @@ -219,7 +219,7 @@ def _get_preset_list(bank): for f in sorted(os.listdir(preset_dir)): preset_fpath = join(preset_dir, f) ext = f[-3:].lower() - if (isfile(preset_fpath) and (ext == 'xiz' or ext == 'xmz' or ext == 'xsz' or ext == 'xlz')): + if isfile(preset_fpath) and (ext == 'xiz' or ext == 'xmz' or ext == 'xsz' or ext == 'xlz'): try: index = int(f[0:4])-1 title = str.replace(f[5:-4], '_', ' ') @@ -229,8 +229,7 @@ def _get_preset_list(bank): bank_lsb = int(index/128) bank_msb = bank[1] prg = index % 128 - preset_list.append( - [preset_fpath, [bank_msb, bank_lsb, prg], title, ext, f]) + preset_list.append([preset_fpath, [bank_msb, bank_lsb, prg], title, ext, f]) return preset_list def get_preset_list(self, bank): @@ -241,12 +240,9 @@ def set_preset(self, processor, preset, preload=False): return self.state_manager.start_busy("zynaddsubfx") if preset[3] == 'xiz': - self.enable_part(processor) - self.osc_server.send( - self.osc_target, "/load-part", processor.part_i, preset[0]) + self.osc_server.send(self.osc_target, "/load-part", processor.part_i, preset[0]) # logging.debug("OSC => /load-part %s, %s" % (processor.part_i,preset[0])) elif preset[3] == 'xmz': - self.enable_part(processor) self.osc_server.send(self.osc_target, "/load_xmz", preset[0]) logging.debug("OSC => /load_xmz %s" % preset[0]) elif preset[3] == 'xsz': @@ -255,16 +251,7 @@ def set_preset(self, processor, preset, preload=False): elif preset[3] == 'xlz': self.osc_server.send(self.osc_target, "/load_xlz", preset[0]) logging.debug("OSC => /load_xlz %s" % preset[0]) - self.osc_server.send(self.osc_target, "/volume") - i = 0 - while self.state_manager.is_busy("zynaddsubfx"): - sleep(0.1) - if i > 100: - self.state_manager.end_busy("zynaddsubfx") - break - else: - i = i + 1 - processor.send_ctrl_midi_cc() + self.wait_busy() return True def cmp_presets(self, preset1, preset2): @@ -280,18 +267,38 @@ def cmp_presets(self, preset1, preset2): # Controller Managament # ---------------------------------------------------------------------------- + def get_ctrl_options(self, ctrl, processor): + options = super().get_ctrl_options(ctrl, processor) + + # OSC control => + if isinstance(ctrl[1], str): + # replace variables ... + tpl = Template(ctrl[1]) + try: + osc_path = tpl.safe_substitute(i=processor.part_i) + options['osc_path'] = osc_path + if self.osc_target_port > 0: + options['osc_port'] = self.osc_target_port + logging.debug(f"CONTROLLER {ctrl[0]} with OSC PATH => {osc_path}") + except Exception as e: + logging.error(f"Malformed OSC path => {ctrl[1]}") + + # Extra options => Pre-MIDI learning, etc. + if len(ctrl) > 4 and isinstance(ctrl[4], dict): + options.update(ctrl[4]) + + return options + def send_controller_value(self, zctrl): try: if self.osc_server and zctrl.osc_path: - self.osc_server.send( - self.osc_target, zctrl.osc_path, zctrl.get_ctrl_osc_val()) + self.osc_server.send(self.osc_target, zctrl.osc_path, zctrl.get_ctrl_osc_val()) else: izmop = zctrl.processor.chain.zmop_index if izmop is not None and izmop >= 0: mchan = zctrl.processor.part_i mval = zctrl.get_ctrl_midi_val() - lib_zyncore.zmop_send_ccontrol_change( - izmop, mchan, zctrl.midi_cc, mval) + lib_zyncore.zmop_send_ccontrol_change(izmop, mchan, zctrl.midi_cc, mval) except Exception as err: logging.error(err) @@ -301,12 +308,11 @@ def send_controller_value(self, zctrl): def enable_part(self, processor): if self.osc_server and processor.part_i is not None: - self.osc_server.send( - self.osc_target, "/part%d/Penabled" % processor.part_i, True) - self.osc_server.send(self.osc_target, "/part%d/Prcvchn" % - processor.part_i, processor.part_i) - lib_zyncore.zmop_set_midi_chan_trans( - processor.chain.zmop_index, processor.get_midi_chan(), processor.part_i) + self.osc_server.send(self.osc_target, f"/part{processor.part_i}/Penabled", True) + self.osc_server.send(self.osc_target, f"/part{processor.part_i}/Prcvchn", processor.part_i) + lib_zyncore.zmop_set_midi_chan_trans(processor.chain.zmop_index, + processor.get_midi_chan(), + processor.part_i) def disable_part(self, i): if self.osc_server: @@ -335,6 +341,17 @@ def cb_osc_all(self, path, args, types, src): except Exception as e: logging.warning(e) + def wait_busy(self): + self.osc_server.send(self.osc_target, "/volume") + i = 0 + while self.state_manager.is_busy("zynaddsubfx"): + sleep(0.1) + if i > 100: + self.state_manager.end_busy("zynaddsubfx") + break + else: + i = i + 1 + # --------------------------------------------------------------------------- # API methods # --------------------------------------------------------------------------- diff --git a/zyngine/zynthian_processor.py b/zyngine/zynthian_processor.py index f1d2926c7..c5c55a9c9 100644 --- a/zyngine/zynthian_processor.py +++ b/zyngine/zynthian_processor.py @@ -612,10 +612,23 @@ def build_ctrl_screen(self, ctrl_keys): logging.error("Controller %s is not defined" % k) return zctrls + def send_controller_values(self): + """Send all controller values to engines + + It should be called once when creating some processors that don't give controller feedback + or when loading presets that modify these controller values without giving feedback. + => fluidsynth, zynaddsubfx, linuxsampler, ... + """ + + for k, zctrl in self.controllers_dict.items(): + zctrl.send_value() + def send_ctrl_midi_cc(self): """Send MIDI CC for all controllers TODO: When is this required? Fluidsynth, linuxsampler and others calls this during set_preset + => It's used for setting MIDI controllers to a known value, avoiding "jumps" when moving knobs + => It should be replaced by send_controllers() (see above) and called one-time when creating the processor """ for k, zctrl in self.controllers_dict.items(): diff --git a/zyngine/zynthian_state_manager.py b/zyngine/zynthian_state_manager.py index 891bdf35d..ed16c7f26 100644 --- a/zyngine/zynthian_state_manager.py +++ b/zyngine/zynthian_state_manager.py @@ -63,8 +63,7 @@ # ---------------------------------------------------------------------------- SNAPSHOT_SCHEMA_VERSION = 1 -capture_dir_sdc = os.environ.get( - 'ZYNTHIAN_MY_DATA_DIR', "/zynthian/zynthian-my-data") + "/capture" +capture_dir_sdc = os.environ.get('ZYNTHIAN_MY_DATA_DIR', "/zynthian/zynthian-my-data") + "/capture" ex_data_dir = os.environ.get('ZYNTHIAN_EX_DATA_DIR', "/media/root") @@ -100,11 +99,9 @@ def __init__(self): self.busy_details = None self.start_busy("zynthian_state_manager") - self.snapshot_dir = os.environ.get( - 'ZYNTHIAN_MY_DATA_DIR', "/zynthian/zynthian-my-data") + "/snapshots" + self.snapshot_dir = os.environ.get('ZYNTHIAN_MY_DATA_DIR', "/zynthian/zynthian-my-data") + "/snapshots" self.default_snapshot_fpath = join(self.snapshot_dir, "default.zss") - self.last_state_snapshot_fpath = join( - self.snapshot_dir, "last_state.zss") + self.last_state_snapshot_fpath = join(self.snapshot_dir, "last_state.zss") # Increments each time a snapshot is loaded - modules may use to update if required self.last_snapshot_count = 0 self.last_snapshot_fpath = "" @@ -152,10 +149,11 @@ def __init__(self): self.chain_manager = zynthian_chain_manager(self) self.reset_zs3() - self.alsa_mixer_processor = zynthian_processor( - "MX", {"NAME": "Mixer", "TITLE": "ALSA Mixer", "TYPE": "MIXER", "CAT": None, "ENGINE": zynthian_engine_alsa_mixer, "ENABLED": True}) - self.alsa_mixer_processor.engine = zynthian_engine_alsa_mixer( - self, self.alsa_mixer_processor) + self.alsa_mixer_processor = zynthian_processor("MX", { + "NAME": "Mixer", "TITLE": "ALSA Mixer", "TYPE": "MIXER", + "CAT": None, "ENGINE": zynthian_engine_alsa_mixer, "ENABLED": True + }) + self.alsa_mixer_processor.engine = zynthian_engine_alsa_mixer(self, self.alsa_mixer_processor) self.alsa_mixer_processor.refresh_controllers() self.audio_recorder = zynthian_audio_recorder(self) @@ -211,8 +209,7 @@ def start(self): logging.debug(f"Opened undervoltage sensor '{result[0]}'") except: try: - result = glob( - "/sys/devices/platform/soc/soc:firmware/raspberrypi-hwmon/hwmon/**/in0_lcrit_alarm')") + result = glob("/sys/devices/platform/soc/soc:firmware/raspberrypi-hwmon/hwmon/**/in0_lcrit_alarm')") self.hwmon_undervolt_file = open(result[0]) logging.debug(f"Opened undervoltage sensor '{result[0]}'") except: @@ -222,8 +219,7 @@ def start(self): # RBPi native sensors monitoring interface if self.hwmon_thermal_file is None or self.hwmon_undervolt_file is None: try: - self.get_throttled_file = open( - '/sys/devices/platform/soc/soc:firmware/get_throttled') + self.get_throttled_file = open('/sys/devices/platform/soc/soc:firmware/get_throttled') except: self.get_throttled_file = None @@ -249,8 +245,7 @@ def start(self): self.fast_thread.daemon = True # thread dies with the program self.fast_thread.start() - zynsigman.register(zynsigman.S_AUDIO_PLAYER, - self.SS_AUDIO_PLAYER_STATE, self.cb_status_audio_player) + zynsigman.register(zynsigman.S_AUDIO_PLAYER, self.SS_AUDIO_PLAYER_STATE, self.cb_status_audio_player) self.end_busy("start state") @@ -259,8 +254,7 @@ def stop(self): self.start_busy("stop state") - zynsigman.unregister(zynsigman.S_AUDIO_PLAYER, - self.SS_AUDIO_PLAYER_STATE, self.cb_status_audio_player) + zynsigman.unregister(zynsigman.S_AUDIO_PLAYER, self.SS_AUDIO_PLAYER_STATE, self.cb_status_audio_player) self.exit_flag = True if self.fast_thread and self.fast_thread.is_alive(): @@ -578,8 +572,7 @@ def slow_thread_task(self): if self.get_throttled_file: try: self.get_throttled_file.seek(0) - thr = int('0x%s' % - self.get_throttled_file.read(), 16) + thr = int('0x%s' % self.get_throttled_file.read(), 16) if thr & 0x1: self.status_undervoltage = True elif thr & (0x4 | 0x2): @@ -618,16 +611,14 @@ def slow_thread_task(self): status_midi_player = libsmf.getPlayState() if self.status_midi_player != status_midi_player: self.status_midi_player = status_midi_player - zynsigman.send( - zynsigman.S_STATE_MAN, self.SS_MIDI_PLAYER_STATE, state=status_midi_player) + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_MIDI_PLAYER_STATE, state=status_midi_player) # MIDI Recorder # TODO: Add callback from MIDI recorder to avoid polling (and regular access to c-lib) status_midi_recorder = libsmf.isRecording() if self.status_midi_recorder != status_midi_recorder: self.status_midi_recorder = status_midi_recorder - zynsigman.send( - zynsigman.S_STATE_MAN, self.SS_MIDI_RECORDER_STATE, state=status_midi_recorder) + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_MIDI_RECORDER_STATE, state=status_midi_recorder) # Sequencer Status => It must be improved using callbacks self.zynseq.update_state() @@ -810,11 +801,9 @@ def zynmidi_read(self): self.all_notes_off() else: if self.midi_learn_zctrl: - self.chain_manager.add_midi_learn( - chan, ccnum, self.midi_learn_zctrl, izmip) + self.chain_manager.add_midi_learn(chan, ccnum, self.midi_learn_zctrl, izmip) else: - self.zynmixer.midi_control_change( - chan, ccnum, ccval) + self.zynmixer.midi_control_change(chan, ccnum, ccval) # Master Note CUIA with ZynSwitch emulation elif evtype == 0x8 or evtype == 0x9: note = str(ev[1] & 0x7F) @@ -845,16 +834,12 @@ def zynmidi_read(self): # logging.debug("MIDI CONTROL CHANGE: CH{}, CC{} => {}".format(chan, ccnum, ccval)) if ccnum < 120: if not self.midi_learn_zctrl: - self.chain_manager.midi_control_change( - izmip, chan, ccnum, ccval) - self.zynmixer.midi_control_change( - chan, ccnum, ccval) - self.alsa_mixer_processor.midi_control_change( - chan, ccnum, ccval) - self.audio_player.midi_control_change( - chan, ccnum, ccval) - zynsigman.send_queued( - zynsigman.S_MIDI, zynsigman.SS_MIDI_CC, izmip=izmip, chan=chan, num=ccnum, val=ccval) + self.chain_manager.midi_control_change(izmip, chan, ccnum, ccval) + self.zynmixer.midi_control_change(chan, ccnum, ccval) + self.alsa_mixer_processor.midi_control_change(chan, ccnum, ccval) + self.audio_player.midi_control_change(chan, ccnum, ccval) + zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_CC, + izmip=izmip, chan=chan, num=ccnum, val=ccval) # Special CCs >= Channel Mode elif ccnum == 120: self.all_sounds_off_chan(chan) @@ -886,11 +871,10 @@ def zynmidi_read(self): # Sends to active chain's MIDI channel when device uses ACTI mode if zynautoconnect.get_midi_in_dev_mode(izmip): chan = self.chain_manager.get_active_chain().midi_chan - send_signal = self.chain_manager.set_midi_prog_preset( - chan, pgm) + send_signal = self.chain_manager.set_midi_prog_preset(chan, pgm) if send_signal: - zynsigman.send_queued( - zynsigman.S_MIDI, zynsigman.SS_MIDI_PC, izmip=izmip, chan=chan, num=pgm) + zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_PC, + izmip=izmip, chan=chan, num=pgm) # Note Off elif evtype == 0x8: @@ -1025,7 +1009,6 @@ def export_chain(self, fpath, chain_id): except: pass - for key in ["last_snapshot_fpath", "midi_profile_state", "engine_config", "audio_recorder_armed", "zynseq_riff_b64", "alsa_mixer", "zyngui"]: try: del state[key] @@ -1448,8 +1431,7 @@ def load_zs3(self, zs3_id, autoconnect=True): if "transpose_semitone" in chain_state: lib_zyncore.zmop_set_transpose_semitone(chain.zmop_index, chain_state["transpose_semitone"]) else: - lib_zyncore.zmop_set_transpose_semitone( - chain.zmop_index, 0) + lib_zyncore.zmop_set_transpose_semitone(chain.zmop_index, 0) if "midi_in" in chain_state: chain.midi_in = chain_state["midi_in"] if "midi_out" in chain_state: @@ -1461,9 +1443,12 @@ def load_zs3(self, zs3_id, autoconnect=True): chain.audio_out = [] if "audio_out" in chain_state: for out in chain_state["audio_out"]: - try: + if isinstance(out, list): chain.audio_out.append(f"{self.chain_manager.processors[out[0]].jackname}:{out[1]}") - except: + elif isinstance(out, str) and out.startswith("system:playback_["): + # Nasty temporary fix for change of output routing + chain.audio_out.append("^system:playback_1$|^system:playback_2$") + elif out not in chain.audio_out: chain.audio_out.append(out) if "audio_thru" in chain_state: @@ -1597,12 +1582,10 @@ def save_zs3(self, zs3_id=None, title=None): note_high = lib_zyncore.zmop_get_note_high(chain.zmop_index) if note_high < 127: chain_state["note_high"] = note_high - transpose_octave = lib_zyncore.zmop_get_transpose_octave( - chain.zmop_index) + transpose_octave = lib_zyncore.zmop_get_transpose_octave(chain.zmop_index) if transpose_octave: chain_state["transpose_octave"] = transpose_octave - transpose_semitone = lib_zyncore.zmop_get_transpose_semitone( - chain.zmop_index) + transpose_semitone = lib_zyncore.zmop_get_transpose_semitone(chain.zmop_index) if transpose_semitone: chain_state["transpose_semitone"] = transpose_semitone if chain.midi_in: @@ -1632,8 +1615,7 @@ def save_zs3(self, zs3_id=None, title=None): chain_state["midi_cc"] = {} chain_state["midi_cc"][cc] = [] for zctrl in zctrls: - chain_state["midi_cc"][cc].append( - [zctrl.processor.id, zctrl.symbol]) + chain_state["midi_cc"][cc].append([zctrl.processor.id, zctrl.symbol]) if chain_state: chain_states[chain_id] = chain_state if chain_states: @@ -2000,22 +1982,18 @@ def init_midi(self): """Initialise MIDI configuration""" try: # Set active MIDI channel - lib_zyncore.set_active_midi_chan( - zynthian_gui_config.active_midi_channel) + lib_zyncore.set_active_midi_chan(zynthian_gui_config.active_midi_channel) # Set Global Tuning self.fine_tuning_freq = zynthian_gui_config.midi_fine_tuning lib_zyncore.set_tuning_freq(ctypes.c_double(self.fine_tuning_freq)) # Set MIDI Master Channel - lib_zyncore.set_midi_master_chan( - zynthian_gui_config.master_midi_channel) + lib_zyncore.set_midi_master_chan(zynthian_gui_config.master_midi_channel) # Set MIDI System Messages flag - lib_zyncore.set_midi_system_events( - zynthian_gui_config.midi_sys_enabled) + lib_zyncore.set_midi_system_events(zynthian_gui_config.midi_sys_enabled) # Setup MIDI filter rules if self.midi_filter_script: self.midi_filter_script.clean() - self.midi_filter_script = zynthian_midi_filter.MidiFilterScript( - zynthian_gui_config.midi_filter_rules) + self.midi_filter_script = zynthian_midi_filter.MidiFilterScript(zynthian_gui_config.midi_filter_rules) except Exception as e: logging.error(f"ERROR initializing MIDI : {e}") @@ -2071,8 +2049,7 @@ def set_transport_clock_source(self, val=None, save_config=False): if val > 0: lib_zyncore.set_midi_system_events(1) else: - lib_zyncore.set_midi_system_events( - zynthian_gui_config.midi_sys_enabled) + lib_zyncore.set_midi_system_events(zynthian_gui_config.midi_sys_enabled) # Save config if save_config: @@ -2127,8 +2104,7 @@ def reset_midi_profile(self): def create_audio_player(self): if not self.audio_player: try: - self.audio_player = zynthian_processor( - "AP", self.chain_manager.engine_info["AP"]) + self.audio_player = zynthian_processor("AP", self.chain_manager.engine_info["AP"]) self.chain_manager.start_engine(self.audio_player, "AP") except Exception as e: logging.error( @@ -2186,8 +2162,7 @@ def start_midi_record(self): if not libsmf.isRecording(): libsmf.unload(self.smf_recorder) libsmf.startRecording() - zynsigman.send(zynsigman.S_STATE_MAN, - self.SS_MIDI_RECORDER_STATE, state=True) + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_MIDI_RECORDER_STATE, state=True) return True else: return False @@ -2204,8 +2179,7 @@ def stop_midi_record(self): self.last_midi_file = fpath result = True - zynsigman.send(zynsigman.S_STATE_MAN, - self.SS_MIDI_RECORDER_STATE, state=False) + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_MIDI_RECORDER_STATE, state=False) return result @@ -2247,8 +2221,7 @@ def start_midi_playback(self, fpath): self.zynseq.transport_start("zynsmf") if libsmf.getPlayState() != zynsmf.PLAY_STATE_STOPPED: self.status_midi_player = True - zynsigman.send(zynsigman.S_STATE_MAN, - self.SS_MIDI_PLAYER_STATE, state=True) + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_MIDI_PLAYER_STATE, state=True) self.status_midi_player = False self.last_midi_file = fpath # self.zynseq.libseq.transportLocate(0) @@ -2261,8 +2234,7 @@ def stop_midi_playback(self): if libsmf.getPlayState() != zynsmf.PLAY_STATE_STOPPED: libsmf.stopPlayback() self.status_midi_player = False - zynsigman.send(zynsigman.S_STATE_MAN, - self.SS_MIDI_PLAYER_STATE, state=False) + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_MIDI_PLAYER_STATE, state=False) return self.status_midi_player def toggle_midi_playback(self, fname=None): @@ -2766,10 +2738,10 @@ def update_thread(): path = f"/zynthian/{repo}" branch = get_repo_branch(path) # Get last tag release - check_output(["git", "-C", path, "remote", "update", "origin", "--prune"], encoding="utf-8", - stderr=STDOUT) - stags = check_output(["git", "-C", path, "tag", "-l", f"{stable_branch}-*"], encoding="utf-8", - stderr=STDOUT).strip().split("\n") + check_output(["git", "-C", path, "remote", "update", "origin", "--prune"], + encoding="utf-8", stderr=STDOUT) + stags = check_output(["git", "-C", path, "tag", "-l", f"{stable_branch}-*"], + encoding="utf-8", stderr=STDOUT).strip().split("\n") last_stag = stags[-1].strip() #logging.debug(f"STABLE TAG RELEASES => {stags}") if branch != last_stag: @@ -2781,10 +2753,10 @@ def update_thread(): for repo in repos: path = f"/zynthian/{repo}" branch = get_repo_branch(path) - local_hash = check_output(["git", "-C", path, "rev-parse", "HEAD"], encoding="utf-8", - stderr=STDOUT).strip() - remote_hash = check_output(["git", "-C", path, "ls-remote", "origin", branch], encoding="utf-8", - stderr=STDOUT).strip().split("\t")[0] + local_hash = check_output(["git", "-C", path, "rev-parse", "HEAD"], + encoding="utf-8", stderr=STDOUT).strip() + remote_hash = check_output(["git", "-C", path, "ls-remote", "origin", branch], + encoding="utf-8", stderr=STDOUT).strip().split("\t")[0] #logging.debug(f"*********** BRANCH {branch} => local hash {local_hash}, remote hash {remote_hash} ****************") if local_hash != remote_hash: self.update_available = True diff --git a/zyngui/__init__.py b/zyngui/__init__.py index 00fd347cd..261a414af 100644 --- a/zyngui/__init__.py +++ b/zyngui/__init__.py @@ -35,7 +35,8 @@ "zynthian_gui_brightness_config", "zynthian_gui_cv_config", "zynthian_gui_wifi", - "zynthian_gui_bluetooth" + "zynthian_gui_bluetooth", + "zynthian_gui_touchkeypad_v5" ] import zyngui.zynthian_gui_config as zynthian_gui_config diff --git a/zyngui/zynthian_gui.py b/zyngui/zynthian_gui.py index 6fd396a88..0dee653cb 100644 --- a/zyngui/zynthian_gui.py +++ b/zyngui/zynthian_gui.py @@ -112,10 +112,8 @@ class zynthian_gui: SCREEN_HMODE_RESET = 3 def __init__(self): - self.capture_dir_sdc = os.environ.get( - 'ZYNTHIAN_MY_DATA_DIR', "/zynthian/zynthian-my-data") + "/capture" - self.ex_data_dir = os.environ.get( - 'ZYNTHIAN_EX_DATA_DIR', "/media/root") + self.capture_dir_sdc = os.environ.get('ZYNTHIAN_MY_DATA_DIR', "/zynthian/zynthian-my-data") + "/capture" + self.ex_data_dir = os.environ.get('ZYNTHIAN_EX_DATA_DIR', "/media/root") self.test_mode = False self.alt_mode = False @@ -167,8 +165,7 @@ def __init__(self): # Init multitouch driver if os.environ.get('DISPLAY_ROTATION', 'None') == 'Inverted' or zynthian_gui_config.check_wiring_layout(["Z2", "V5"]): - self.multitouch = MultiTouch( - invert_x_axis=True, invert_y_axis=True) + self.multitouch = MultiTouch(invert_x_axis=True, invert_y_axis=True) else: self.multitouch = MultiTouch() @@ -190,20 +187,17 @@ def __init__(self): def start_capture_log(self, title="ui_sesion"): now = datetime.now() self.capture_log_ts0 = now - self.capture_log_fname = "{}-{}".format( - title, now.strftime("%Y%m%d%H%M%S")) + self.capture_log_fname = "{}-{}".format(title, now.strftime("%Y%m%d%H%M%S")) self.start_capture_ffmpeg() if self.wsleds: self.wsleds.reset_last_state() - self.write_capture_log("LAYOUT: {}".format( - zynthian_gui_config.wiring_layout)) + self.write_capture_log("LAYOUT: {}".format(zynthian_gui_config.wiring_layout)) self.write_capture_log("TITLE: {}".format(self.capture_log_fname)) zynautoconnect.audio_connect_ffmpeg(timeout=2.0) def start_capture_ffmpeg(self): fbdev = os.environ.get("FRAMEBUFFER", "/dev/fb0") - fpath = "{}/{}.mp4".format(self.capture_dir_sdc, - self.capture_log_fname) + fpath = "{}/{}.mp4".format(self.capture_dir_sdc, self.capture_log_fname) self.capture_ffmpeg_proc = ffmpeg.output( ffmpeg.input(":0", r=20, f="x11grab"), # ffmpeg.input(fbdev, r=20, f="fbdev"), @@ -228,8 +222,7 @@ def write_capture_log(self, message): if self.capture_log_fname: try: rts = str(datetime.now() - self.capture_log_ts0) - fh = open("{}/{}.log".format(self.capture_dir_sdc, - self.capture_log_fname), 'a') + fh = open("{}/{}.log".format(self.capture_dir_sdc, self.capture_log_fname), 'a') fh.write("{} {}\n".format(rts, message)) fh.close() except Exception as e: @@ -240,7 +233,12 @@ def write_capture_log(self, message): # --------------------------------------------------------------------------- def init_wsleds(self): - if zynthian_gui_config.check_wiring_layout("Z2"): + if zynthian_gui_config.touch_keypad: + if zynthian_gui_config.touch_keypad_option == "V5": + from zyngui.zynthian_wsleds_v5touch import zynthian_wsleds_v5touch + self.wsleds = zynthian_wsleds_v5touch(self) + self.wsleds.start() + elif zynthian_gui_config.check_wiring_layout("Z2"): from zyngui.zynthian_wsleds_z2 import zynthian_wsleds_z2 self.wsleds = zynthian_wsleds_z2(self) self.wsleds.start() @@ -261,10 +259,8 @@ def wiring_midi_setup(current_chan=None): if event is not None: swi = 4 + i if event['type'] >= 0xF8: - lib_zyncore.setup_zynswitch_midi( - swi, event['type'], 0, 0, 0) - logging.info("MIDI ZYNSWITCH {}: SYSRT {}".format( - swi, event['type'])) + lib_zyncore.setup_zynswitch_midi(swi, event['type'], 0, 0, 0) + logging.info("MIDI ZYNSWITCH {}: SYSRT {}".format(swi, event['type'])) else: if event['chan'] is not None: midi_chan = event['chan'] @@ -272,14 +268,11 @@ def wiring_midi_setup(current_chan=None): midi_chan = current_chan if midi_chan is not None: - lib_zyncore.setup_zynswitch_midi( - swi, event['type'], midi_chan, event['num'], event['val']) - logging.info("MIDI ZYNSWITCH {}: {} CH#{}, {}, {}".format( - swi, event['type'], midi_chan, event['num'], event['val'])) + lib_zyncore.setup_zynswitch_midi(swi, event['type'], midi_chan, event['num'], event['val']) + logging.info("MIDI ZYNSWITCH {}: {} CH#{}, {}, {}".format(swi, event['type'], midi_chan, event['num'], event['val'])) else: lib_zyncore.setup_zynswitch_midi(swi, 0, 0, 0, 0) - logging.info( - "MIDI ZYNSWITCH {}: DISABLED!".format(swi)) + logging.info("MIDI ZYNSWITCH {}: DISABLED!".format(swi)) # Configure Zynaptik Analog Inputs (CV-IN) for i, event in enumerate(zynthian_gui_config.zynaptik_ad_midi_events): @@ -290,10 +283,8 @@ def wiring_midi_setup(current_chan=None): midi_chan = current_chan if midi_chan is not None: - lib_zyncore.zynaptik_setup_cvin( - i, event['type'], midi_chan, event['num']) - logging.info("ZYNAPTIK CV-IN {}: {} CH#{}, {}".format(i, - event['type'], midi_chan, event['num'])) + lib_zyncore.zynaptik_setup_cvin(i, event['type'], midi_chan, event['num']) + logging.info("ZYNAPTIK CV-IN {}: {} CH#{}, {}".format(i, event['type'], midi_chan, event['num'])) else: lib_zyncore.zynaptik_disable_cvin(i) logging.info("ZYNAPTIK CV-IN {}: DISABLED!".format(i)) @@ -307,10 +298,8 @@ def wiring_midi_setup(current_chan=None): midi_chan = current_chan if midi_chan is not None: - lib_zyncore.zynaptik_setup_cvout( - i, event['type'], midi_chan, event['num']) - logging.info("ZYNAPTIK CV-OUT {}: {} CH#{}, {}".format(i, - event['type'], midi_chan, event['num'])) + lib_zyncore.zynaptik_setup_cvout(i, event['type'], midi_chan, event['num']) + logging.info("ZYNAPTIK CV-OUT {}: {} CH#{}, {}".format(i, event['type'], midi_chan, event['num'])) else: lib_zyncore.zynaptik_disable_cvout(i) logging.info("ZYNAPTIK CV-OUT {}: DISABLED!".format(i)) @@ -324,10 +313,8 @@ def wiring_midi_setup(current_chan=None): midi_chan = current_chan if midi_chan is not None: - lib_zyncore.setup_zyntof( - i, event['type'], midi_chan, event['num']) - logging.info("ZYNTOF {}: {} CH#{}, {}".format( - i, event['type'], midi_chan, event['num'])) + lib_zyncore.setup_zyntof(i, event['type'], midi_chan, event['num']) + logging.info("ZYNTOF {}: {} CH#{}, {}".format(i, event['type'], midi_chan, event['num'])) else: lib_zyncore.disable_zyntof(i) logging.info("ZYNTOF {}: DISABLED!".format(i)) @@ -349,18 +336,14 @@ def reload_wiring_layout(self): def osc_init(self): try: - self.osc_server = liblo.Server( - self.osc_server_port, self.osc_proto) + self.osc_server = liblo.Server(self.osc_server_port, self.osc_proto) self.osc_server_port = self.osc_server.get_port() - self.osc_server_url = liblo.Address( - 'localhost', self.osc_server_port, self.osc_proto).get_url() - logging.info( - "ZYNTHIAN-UI OSC server running in port {}".format(self.osc_server_port)) + self.osc_server_url = liblo.Address('localhost', self.osc_server_port, self.osc_proto).get_url() + logging.info("ZYNTHIAN-UI OSC server running in port {}".format(self.osc_server_port)) self.osc_server.add_method(None, None, self.osc_cb_all) # except liblo.AddressError as err: except Exception as err: - logging.error( - "ZYNTHIAN-UI OSC Server can't be started: {}".format(err)) + logging.error("ZYNTHIAN-UI OSC Server can't be started: {}".format(err)) def osc_end(self): if self.osc_server: @@ -368,8 +351,7 @@ def osc_end(self): self.osc_server.free() logging.info("ZYNTHIAN-UI OSC server stopped") except Exception as err: - logging.error( - "ZYNTHIAN-UI OSC server can't be stopped: {}".format(err)) + logging.error("ZYNTHIAN-UI OSC server can't be stopped: {}".format(err)) self.osc_server = None def osc_receive(self): @@ -388,8 +370,7 @@ def osc_cb_all(self, path, args, types, src): # Execute action cuia = parts[2].upper() if self.state_manager.is_busy(): - logging.debug( - "BUSY! Ignoring OSC CUIA '{}' => {}".format(cuia, args)) + logging.debug("BUSY! Ignoring OSC CUIA '{}' => {}".format(cuia, args)) return self.cuia_queue.put_nowait((cuia, args)) # Run autoconnect if needed @@ -402,38 +383,28 @@ def osc_cb_all(self, path, args, types, src): if src.hostname not in self.osc_clients: try: if self.state_manager.zynmixer.add_osc_client(src.hostname) < 0: - logging.warning( - "Failed to add OSC client registration {}".format(src.hostname)) + logging.warning("Failed to add OSC client registration {}".format(src.hostname)) return except: - logging.warning( - "Error trying to add OSC client registration {}".format(src.hostname)) + logging.warning("Error trying to add OSC client registration {}".format(src.hostname)) return self.osc_clients[src.hostname] = monotonic() - self.state_manager.zynmixer.enable_dpm( - 0, self.state_manager.zynmixer.MAX_NUM_CHANNELS - 2, True) + self.state_manager.zynmixer.enable_dpm(0, self.state_manager.zynmixer.MAX_NUM_CHANNELS - 2, True) else: if part2[:6] == "VOLUME": - self.state_manager.zynmixer.set_level( - int(part2[6:]), float(args[0])) + self.state_manager.zynmixer.set_level(int(part2[6:]), float(args[0])) if part2[:5] == "FADER": - self.state_manager.zynmixer.set_level( - int(part2[5:]), float(args[0])) + self.state_manager.zynmixer.set_level(int(part2[5:]), float(args[0])) if part2[:5] == "LEVEL": - self.state_manager.zynmixer.set_level( - int(part2[5:]), float(args[0])) + self.state_manager.zynmixer.set_level(int(part2[5:]), float(args[0])) elif part2[:7] == "BALANCE": - self.state_manager.zynmixer.set_balance( - int(part2[7:]), float(args[0])) + self.state_manager.zynmixer.set_balance(int(part2[7:]), float(args[0])) elif part2[:4] == "MUTE": - self.state_manager.zynmixer.set_mute( - int(part2[4:]), int(args[0])) + self.state_manager.zynmixer.set_mute(int(part2[4:]), int(args[0])) elif part2[:4] == "SOLO": - self.state_manager.zynmixer.set_solo( - int(part2[4:]), int(args[0])) + self.state_manager.zynmixer.set_solo(int(part2[4:]), int(args[0])) elif part2[:4] == "MONO": - self.state_manager.zynmixer.set_mono( - int(part2[4:]), int(args[0])) + self.state_manager.zynmixer.set_mono(int(part2[4:]), int(args[0])) else: logging.warning(f"Not supported OSC call '{path}'") @@ -681,16 +652,13 @@ def close_screen(self, screen=None): last_screen = "audio_mixer" if last_screen not in self.screens: - logging.error( - f"Can't back to screen '{last_screen}'. It doesn't exist!") + logging.error(f"Can't back to screen '{last_screen}'. It doesn't exist!") last_screen = "audio_mixer" - logging.debug( - f"CLOSE SCREEN '{self.current_screen}' => Back to '{last_screen}'") + logging.debug(f"CLOSE SCREEN '{self.current_screen}' => Back to '{last_screen}'") self.show_screen(last_screen) def purge_screen_history(self, screen): - self.screen_history = list( - filter(lambda i: i != screen, self.screen_history)) + self.screen_history = list(filter(lambda i: i != screen, self.screen_history)) def prune_screen_history(self, screen, soft=True): logging.debug(f"SCREEN HISTORY => {self.screen_history}") @@ -702,8 +670,7 @@ def prune_screen_history(self, screen, soft=True): self.screen_history.append(screen) except: pass - logging.debug( - f"PRUNE '{screen}' FROM SCREEN HISTORY => {self.screen_history}") + logging.debug(f"PRUNE '{screen}' FROM SCREEN HISTORY => {self.screen_history}") def back_screen(self): try: @@ -773,8 +740,7 @@ def hide_info(self): def hide_info_timer(self, tms=3000): if self.current_screen == 'info': self.cancel_screen_timer() - self.screen_timer_id = zynthian_gui_config.top.after( - tms, self.hide_info) + self.screen_timer_id = zynthian_gui_config.top.after(tms, self.hide_info) def show_splash(self, text): self.screen_lock.acquire() @@ -983,15 +949,13 @@ def chain_control(self, chain_id=None, processor=None, hmode=SCREEN_HMODE_RESET, custom_screen_name = module_name[len("zynthian_gui_"):] if custom_screen_name not in self.screens: try: - spec = importlib.util.spec_from_file_location( - module_name, module_path) + spec = importlib.util.spec_from_file_location(module_name, module_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) class_ = getattr(module, module_name) self.screens[custom_screen_name] = class_() except Exception as e: - logging.error("Can't load custom control screen {} => {}".format( - custom_screen_name, e)) + logging.error("Can't load custom control screen {} => {}".format(custom_screen_name, e)) if custom_screen_name in self.screens: control_screen_name = custom_screen_name @@ -1287,16 +1251,14 @@ def cuia_set_tempo(self, params=None): def cuia_toggle_seq(self, params=None): try: - self.state_manager.zynseq.libseq.togglePlayState( - self.state_manager.zynseq.bank, int(params[0])) + self.state_manager.zynseq.libseq.togglePlayState(self.state_manager.zynseq.bank, int(params[0])) except (AttributeError, TypeError): pass def cuia_tempo_up(self, params=None): if params: try: - self.state_manager.zynseq.set_tempo( - self.state_manager.zynseq.get_tempo() + params[0]) + self.state_manager.zynseq.set_tempo(self.state_manager.zynseq.get_tempo() + params[0]) except (AttributeError, TypeError): pass else: @@ -1306,13 +1268,11 @@ def cuia_tempo_up(self, params=None): def cuia_tempo_down(self, params=None): if params: try: - self.state_manager.zynseq.set_tempo( - self.state_manager.zynseq.get_tempo() - params[0]) + self.state_manager.zynseq.set_tempo(self.state_manager.zynseq.get_tempo() - params[0]) except (AttributeError, TypeError): pass else: - self.state_manager.zynseq.set_tempo( - self.state_manager.zynseq.get_tempo() - 1) + self.state_manager.zynseq.set_tempo(self.state_manager.zynseq.get_tempo() - 1) def cuia_tap_tempo(self, params=None): self.screens["tempo"].tap() @@ -1324,8 +1284,7 @@ def cuia_zynpot(self, params=None): d = int(params[1]) self.get_current_screen_obj().zynpot_cb(i, d) except IndexError: - logging.error( - "zynpot requires 2 parameters: index, delta, not {params}") + logging.error("zynpot requires 2 parameters: index, delta, not {params}") return except Exception as e: logging.error(e) @@ -1336,8 +1295,7 @@ def cuia_zynswitch(self, params=None): d = params[1] self.cuia_queue.put_nowait(("zynswitch", (i, d))) except IndexError: - logging.error( - "zynswitch requires 2 parameters: index, delta, not {params}") + logging.error("zynswitch requires 2 parameters: index, delta, not {params}") return except Exception as e: logging.error(e) @@ -1440,6 +1398,13 @@ def cuia_screen_preset(self, params=None): def cuia_screen_calibrate(self, params=None): self.calibrate_touchscreen() + def cuia_screen_clean(self, params=None): + self.state_manager.start_busy("clean_screen", "Clean screen") + for i in range(10, 0, -1): + self.state_manager.set_busy_details(f"Closing in {i}s") + sleep(1) + self.state_manager.end_busy("clean_screen") + def cuia_chain_control(self, params=None): try: # Select chain by index @@ -1470,15 +1435,13 @@ def cuia_chain_options(self, params=None): if params[0] == 0: chain_id = 0 else: - chain_id = self.chain_manager.get_chain_id_by_index( - params[0] - 1) + chain_id = self.chain_manager.get_chain_id_by_index(params[0] - 1) except: chain_id = self.chain_manager.active_chain_id if chain_id is not None: self.screens['chain_options'].setup(chain_id) - self.show_screen( - 'chain_options', hmode=zynthian_gui.SCREEN_HMODE_ADD) + self.show_screen('chain_options', hmode=zynthian_gui.SCREEN_HMODE_ADD) cuia_layer_options = cuia_chain_options @@ -1503,8 +1466,7 @@ def cuia_bank_preset(self, params=None): elif not self.is_shown_audio_player(): self.screens["control"].fill_list() try: - self.chain_manager.get_active_chain().set_current_processor( - self.screens['control'].screen_processor) + self.chain_manager.get_active_chain().set_current_processor(self.screens['control'].screen_processor) self.current_processor = None except: logging.warning("Can't set control screen processor! ") @@ -1524,14 +1486,12 @@ def cuia_bank_preset(self, params=None): else: if len(curproc.preset_list) > 0 and curproc.preset_list[0][0] != '': self.screens['preset'].index = curproc.get_preset_index() - self.show_screen( - 'preset', hmode=zynthian_gui.SCREEN_HMODE_ADD) + self.show_screen('preset', hmode=zynthian_gui.SCREEN_HMODE_ADD) if len(curproc.preset_list) == 0 or curproc.preset_list[0][0] == '': # Handle change of bank name, e.g. via webconf self.replace_screen('bank') elif len(bank_list) > 0 and bank_list[0][0] != '': - self.show_screen( - 'bank', hmode=zynthian_gui.SCREEN_HMODE_ADD) + self.show_screen('bank', hmode=zynthian_gui.SCREEN_HMODE_ADD) cuia_preset = cuia_bank_preset @@ -1593,8 +1553,7 @@ def cuia_midi_learn_control(self, params=None): def cuia_midi_unlearn_control(self, params=None): if self.current_screen in ("control", "alsa_mixer"): if params: - self.midi_learn_zctrl = self.screens[self.current_screen].get_zcontroller( - params[0]) + self.midi_learn_zctrl = self.screens[self.current_screen].get_zcontroller(params[0]) # if not parameter, unlearn selected learning control if self.midi_learn_zctrl: self.screens[self.current_screen].midi_unlearn_action() @@ -1684,8 +1643,7 @@ def cuia_midi_unlearn_chain(self, params=None): if params: self.chain_manager.clean_midi_learn(params[0]) else: - self.chain_manager.clean_midi_learn( - self.chain_manager.active_chain_id) + self.chain_manager.clean_midi_learn(self.chain_manager.active_chain_id) # MIDI CUIAs def cuia_program_change(self, params=None): @@ -1709,11 +1667,9 @@ def cuia_zyn_cc(self, params=None): cc = int(params[1]) if params[-1] == 'R': if len(params) > 3: - lib_zyncore.write_zynmidi_ccontrol_change( - chan, cc, int(params[3])) + lib_zyncore.write_zynmidi_ccontrol_change(chan, cc, int(params[3])) else: - lib_zyncore.write_zynmidi_ccontrol_change( - chan, cc, int(params[2])) + lib_zyncore.write_zynmidi_ccontrol_change(chan, cc, int(params[2])) # Common methods to control views derived from zynthian_gui_base def cuia_show_cursor(self, params=None): @@ -1755,16 +1711,14 @@ def cuia_hide_buttonbar(self, params=None): def cuia_show_sidebar(self, params=None): try: self.screens[self.current_screen].show_sidebar(True) - zynsigman.send_queued( - zynsigman.S_GUI, zynsigman.SS_GUI_SHOW_SIDEBAR, shown=True) + zynsigman.send_queued(zynsigman.S_GUI, zynsigman.SS_GUI_SHOW_SIDEBAR, shown=True) except (AttributeError, TypeError): pass def cuia_hide_sidebar(self, params=None): try: self.screens[self.current_screen].show_sidebar(False) - zynsigman.send_queued( - zynsigman.S_GUI, zynsigman.SS_GUI_SHOW_SIDEBAR, shown=False) + zynsigman.send_queued(zynsigman.S_GUI, zynsigman.SS_GUI_SHOW_SIDEBAR, shown=False) except (AttributeError, TypeError): pass @@ -1772,8 +1726,7 @@ def cuia_toggle_sidebar(self, params=None): try: show = not self.screens[self.current_screen].sidebar_shown self.screens[self.current_screen].show_sidebar(show) - zynsigman.send_queued( - zynsigman.S_GUI, zynsigman.SS_GUI_SHOW_SIDEBAR, shown=show) + zynsigman.send_queued(zynsigman.S_GUI, zynsigman.SS_GUI_SHOW_SIDEBAR, shown=show) except (AttributeError, TypeError): pass @@ -1885,8 +1838,7 @@ def check_current_screen_switch(self, action_config): # Init Standard Zynswitches def zynswitches_init(self): - logging.info( - f"INIT {zynthian_gui_config.num_zynswitches} ZYNSWITCHES ...") + logging.info(f"INIT {zynthian_gui_config.num_zynswitches} ZYNSWITCHES ...") self.dtsw = [datetime.now()] * zynthian_gui_config.num_zynswitches # Initialize custom switches, analog I/O, TOF sensors, etc. @@ -1904,10 +1856,8 @@ def zynswitches_midi_setup(self, current_chain_chan=None): midi_chan = current_chain_chan if midi_chan is not None: - lib_zyncore.setup_zynswitch_midi( - swi, event['type'], midi_chan, event['num'], event['val']) - logging.info( - f"MIDI ZYNSWITCH {swi}: {event['type']} CH#{midi_chan}, {event['num']}, {event['val']}") + lib_zyncore.setup_zynswitch_midi(swi, event['type'], midi_chan, event['num'], event['val']) + logging.info(f"MIDI ZYNSWITCH {swi}: {event['type']} CH#{midi_chan}, {event['num']}, {event['val']}") else: lib_zyncore.setup_zynswitch_midi(swi, 0, 0, 0, 0) logging.info(f"MIDI ZYNSWITCH {swi}: DISABLED!") @@ -1922,10 +1872,8 @@ def zynswitches_midi_setup(self, current_chain_chan=None): midi_chan = current_chain_chan if midi_chan is not None: - lib_zyncore.setup_zynaptik_cvin( - i, event['type'], midi_chan, event['num']) - logging.info( - f"ZYNAPTIK CV-IN {i}: {event['type']} CH#{midi_chan}, {event['num']}") + lib_zyncore.setup_zynaptik_cvin(i, event['type'], midi_chan, event['num']) + logging.info(f"ZYNAPTIK CV-IN {i}: {event['type']} CH#{midi_chan}, {event['num']}") else: lib_zyncore.disable_zynaptik_cvin(i) logging.info(f"ZYNAPTIK CV-IN {i}: DISABLED!") @@ -1940,10 +1888,8 @@ def zynswitches_midi_setup(self, current_chain_chan=None): midi_chan = current_chain_chan if midi_chan is not None: - lib_zyncore.setup_zynaptik_cvout( - i, event['type'], midi_chan, event['num']) - logging.info( - f"ZYNAPTIK CV-OUT {i}: {event['type']} CH#{midi_chan}, {event['num']}") + lib_zyncore.setup_zynaptik_cvout(i, event['type'], midi_chan, event['num']) + logging.info(f"ZYNAPTIK CV-OUT {i}: {event['type']} CH#{midi_chan}, {event['num']}") else: lib_zyncore.disable_zynaptik_cvout(i) logging.info(f"ZYNAPTIK CV-OUT {i}: DISABLED!") @@ -1957,10 +1903,8 @@ def zynswitches_midi_setup(self, current_chain_chan=None): midi_chan = current_chain_chan if midi_chan is not None: - lib_zyncore.setup_zyntof( - i, event['type'], midi_chan, event['num']) - logging.info( - f"ZYNTOF {i}: {event['type']} CH#{midi_chan}, {event['num']}") + lib_zyncore.setup_zyntof(i, event['type'], midi_chan, event['num']) + logging.info(f"ZYNTOF {i}: {event['type']} CH#{midi_chan}, {event['num']}") else: lib_zyncore.disable_zyntof(i) logging.info(f"ZYNTOF {i}: DISABLED!") @@ -1994,8 +1938,7 @@ def zynswitches(self): # dtus is 0 if switched pressed, dur of last press or -1 if already processed dtus = lib_zyncore.get_zynswitch(i, zs_long_us) if dtus >= 0: - self.cuia_queue.put_nowait( - ("zynswitch", (i, self.zynswitch_timing(dtus)))) + self.cuia_queue.put_nowait(("zynswitch", (i, self.zynswitch_timing(dtus)))) i += 1 def zynswitch_timing(self, dtus): @@ -2163,16 +2106,12 @@ def zynswitch_read(self): # ------------------------------------------------------------------ def register_signals(self): - zynsigman.register( - zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_ON, self.cb_midi_note_on) - zynsigman.register( - zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_OFF, self.cb_midi_note_off) + zynsigman.register(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_ON, self.cb_midi_note_on) + zynsigman.register(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_OFF, self.cb_midi_note_off) def unregister_signals(self): - zynsigman.unregister( - zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_ON, self.cb_midi_note_on) - zynsigman.unregister( - zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_OFF, self.cb_midi_note_off) + zynsigman.unregister(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_ON, self.cb_midi_note_on) + zynsigman.unregister(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_OFF, self.cb_midi_note_off) def cb_midi_note_on(self, izmip, chan, note, vel): """Handle MIDI_NOTE_ON signal @@ -2234,8 +2173,7 @@ def zynpot_thread_task(self): self.screens[self.current_screen].zynpot_cb(i, dval) self.state_manager.set_event_flag() if self.capture_log_fname: - self.write_capture_log( - "ZYNPOT:{},{}".format(i, dval)) + self.write_capture_log("ZYNPOT:{},{}".format(i, dval)) except Exception as err: pass # Some screens don't use controllers logging.exception(err) @@ -2330,8 +2268,7 @@ def busy_thread_task(self): else: busy_success = self.state_manager.get_busy_success() if busy_success: - self.screens['loading'].set_success( - busy_success) + self.screens['loading'].set_success(busy_success) elif busy_message: self.screens['loading'].set_title(busy_message) if busy_details: @@ -2349,12 +2286,10 @@ def busy_thread_task(self): if self.current_screen: self.screens[self.current_screen].refresh_loading() except Exception as err: - logging.error( - f"refresh_loading() on screen '{self.current_screen}' => {err}") + logging.error(f"refresh_loading() on screen '{self.current_screen}' => {err}") if busy_timeout == busy_warn_time: - logging.warning( - f"Clients have been busy for longer than {int(busy_warn_time / 10)}s: {self.state_manager.busy}") + logging.warning(f"Clients have been busy for longer than {int(busy_warn_time / 10)}s: {self.state_manager.busy}") sleep(0.1) @@ -2422,7 +2357,9 @@ def cuia_thread_task(self): for i, ts in enumerate(zynswitch_cuia_ts): if ts is not None and ts < long_ts: zynswitch_cuia_ts[i] = None - self.zynswitch_long(i) + zpi = zynthian_gui_config.zynpot2switch.index(i) + if self.zynpot_pr_state[zpi] <= 1: + self.zynswitch_long(i) event = self.cuia_queue.get(True, repeat_interval) params = None if isinstance(event, str): @@ -2449,16 +2386,15 @@ def cuia_thread_task(self): del zynswitch_repeat[i] continue else: - dtus = int( - 1000000 * (monotonic() - zynswitch_cuia_ts[i])) + dtus = int(1000000 * (monotonic() - zynswitch_cuia_ts[i])) zynswitch_cuia_ts[i] = None t = self.zynswitch_timing(dtus) if t == 'P': pr = 0 if zynthian_gui_config.num_zynpots > 0: try: - zpi = zynthian_gui_config.zynpot2switch.index( - i) + zynswitch_cuia_ts[i] = monotonic() + zpi = zynthian_gui_config.zynpot2switch.index(i) self.zynpot_pr_state[zpi] = 1 pr = 1 except: @@ -2471,8 +2407,7 @@ def cuia_thread_task(self): else: if zynthian_gui_config.num_zynpots > 0: try: - zpi = zynthian_gui_config.zynpot2switch.index( - i) + zpi = zynthian_gui_config.zynpot2switch.index(i) if self.zynpot_pr_state[zpi] > 1: t = 'PR' self.zynpot_pr_state[zpi] = 0 @@ -2491,8 +2426,7 @@ def cuia_thread_task(self): zynswitch_cuia_ts[i] = None else: zynswitch_cuia_ts[i] = None - logging.warning( - "Unknown Action Type: {}".format(t)) + logging.warning("Unknown Action Type: {}".format(t)) if i in zynswitch_repeat: del zynswitch_repeat[i] @@ -2526,10 +2460,8 @@ def cuia_thread_task(self): self.cuia_zynpot(zynpot_repeat[i][1]) except Exception as e: - logging.error( - f"CUIA '{cuia}' failed with params: {params}\n{traceback.format_exc()}") - self.state_manager.set_busy_error( - f"ERROR CUIA {cuia}: {params}", e) + logging.error(f"CUIA '{cuia}' failed with params: {params}\n{traceback.format_exc()}") + self.state_manager.set_busy_error(f"ERROR CUIA {cuia}: {params}", e) sleep(3) self.state_manager.clear_busy() @@ -2613,12 +2545,10 @@ def osc_timeout(self): pass if not self.osc_clients and self.current_screen != "audio_mixer": - self.state_manager.zynmixer.enable_dpm( - 0, self.state_manager.zynmixer.MAX_NUM_CHANNELS - 2, False) + self.state_manager.zynmixer.enable_dpm(0, self.state_manager.zynmixer.MAX_NUM_CHANNELS - 2, False) # Poll - zynthian_gui_config.top.after( - self.osc_heartbeat_timeout * 1000, self.osc_timeout) + zynthian_gui_config.top.after(self.osc_heartbeat_timeout * 1000, self.osc_timeout) # ------------------------------------------------------------------ # Zynthian Config Info diff --git a/zyngui/zynthian_gui_admin.py b/zyngui/zynthian_gui_admin.py index 0f83d48bf..48d13e4d8 100644 --- a/zyngui/zynthian_gui_admin.py +++ b/zyngui/zynthian_gui_admin.py @@ -5,7 +5,7 @@ # # Zynthian GUI Admin Class # -# Copyright (C) 2015-2023 Fernando Moyano +# Copyright (C) 2015-2024 Fernando Moyano # # ****************************************************************************** # @@ -90,8 +90,7 @@ def build_view(self): self.update_available = self.state_manager.update_available if not self.refresh_wifi_thread: self.refresh_wifi = True - self.refresh_wifi_thread = Thread( - target=self.refresh_wifi_task, name="wifi_refresh") + self.refresh_wifi_thread = Thread(target=self.refresh_wifi_task, name="wifi_refresh") self.refresh_wifi_thread.start() res = super().build_view() self.state_manager.check_for_updates() @@ -110,109 +109,95 @@ def fill_list(self): self.list_data = [] self.list_data.append((None, 0, "> MIDI")) - self.list_data.append( - (self.zyngui.midi_in_config, 0, "MIDI Input Devices")) - self.list_data.append( - (self.zyngui.midi_out_config, 0, "MIDI Output Devices")) + self.list_data.append((self.zyngui.midi_in_config, 0, "MIDI Input Devices")) + self.list_data.append((self.zyngui.midi_out_config, 0, "MIDI Output Devices")) # self.list_data.append((self.midi_profile, 0, "MIDI Profile")) if lib_zyncore.get_active_midi_chan(): - self.list_data.append( - (self.toggle_active_midi_channel, 0, "\u2612 Active MIDI channel")) + self.list_data.append((self.toggle_active_midi_channel, 0, "\u2612 Active MIDI channel")) else: - self.list_data.append( - (self.toggle_active_midi_channel, 0, "\u2610 Active MIDI channel")) + self.list_data.append((self.toggle_active_midi_channel, 0, "\u2610 Active MIDI channel")) if zynthian_gui_config.midi_prog_change_zs3: - self.list_data.append( - (self.toggle_prog_change_zs3, 0, "\u2612 Program Change for ZS3")) + self.list_data.append((self.toggle_prog_change_zs3, 0, "\u2612 Program Change for ZS3")) else: - self.list_data.append( - (self.toggle_prog_change_zs3, 0, "\u2610 Program Change for ZS3")) + self.list_data.append((self.toggle_prog_change_zs3, 0, "\u2610 Program Change for ZS3")) if zynthian_gui_config.midi_bank_change: - self.list_data.append( - (self.toggle_bank_change, 0, "\u2612 MIDI Bank Change")) + self.list_data.append((self.toggle_bank_change, 0, "\u2612 MIDI Bank Change")) else: - self.list_data.append( - (self.toggle_bank_change, 0, "\u2610 MIDI Bank Change")) + self.list_data.append((self.toggle_bank_change, 0, "\u2610 MIDI Bank Change")) if zynthian_gui_config.preset_preload_noteon: - self.list_data.append( - (self.toggle_preset_preload_noteon, 0, "\u2612 Note-On Preset Preload")) + self.list_data.append((self.toggle_preset_preload_noteon, 0, "\u2612 Note-On Preset Preload")) else: - self.list_data.append( - (self.toggle_preset_preload_noteon, 0, "\u2610 Note-On Preset Preload")) + self.list_data.append((self.toggle_preset_preload_noteon, 0, "\u2610 Note-On Preset Preload")) if zynthian_gui_config.midi_usb_by_port: - self.list_data.append( - (self.toggle_usbmidi_by_port, 0, "\u2612 MIDI-USB mapped by port")) + self.list_data.append((self.toggle_usbmidi_by_port, 0, "\u2612 MIDI-USB mapped by port")) else: - self.list_data.append( - (self.toggle_usbmidi_by_port, 0, "\u2610 MIDI-USB mapped by port")) + self.list_data.append((self.toggle_usbmidi_by_port, 0, "\u2610 MIDI-USB mapped by port")) if zynthian_gui_config.transport_clock_source == 0: if zynthian_gui_config.midi_sys_enabled: - self.list_data.append( - (self.toggle_midi_sys, 0, "\u2612 MIDI System Messages")) + self.list_data.append((self.toggle_midi_sys, 0, "\u2612 MIDI System Messages")) else: - self.list_data.append( - (self.toggle_midi_sys, 0, "\u2610 MIDI System Messages")) + self.list_data.append((self.toggle_midi_sys, 0, "\u2610 MIDI System Messages")) gtrans = lib_zyncore.get_global_transpose() if gtrans > 0: display_val = f"+{gtrans}" else: display_val = f"{gtrans}" - self.list_data.append( - (self.edit_global_transpose, 0, f"[{display_val}] Global Transpose")) + self.list_data.append((self.edit_global_transpose, 0, f"[{display_val}] Global Transpose")) self.list_data.append((None, 0, "> AUDIO")) if self.state_manager.allow_rbpi_headphones(): if zynthian_gui_config.rbpi_headphones: - self.list_data.append( - (self.stop_rbpi_headphones, 0, "\u2612 RBPi Headphones")) + self.list_data.append((self.stop_rbpi_headphones, 0, "\u2612 RBPi Headphones")) else: - self.list_data.append( - (self.start_rbpi_headphones, 0, "\u2610 RBPi Headphones")) + self.list_data.append((self.start_rbpi_headphones, 0, "\u2610 RBPi Headphones")) self.list_data.append((self.hotplug_audio_menu, 0, "Hotplug USB Audio")) if zynthian_gui_config.snapshot_mixer_settings: - self.list_data.append( - (self.toggle_snapshot_mixer_settings, 0, "\u2612 Audio Levels on Snapshots")) + self.list_data.append((self.toggle_snapshot_mixer_settings, 0, "\u2612 Audio Levels on Snapshots")) else: - self.list_data.append( - (self.toggle_snapshot_mixer_settings, 0, "\u2610 Audio Levels on Snapshots")) + self.list_data.append((self.toggle_snapshot_mixer_settings, 0, "\u2610 Audio Levels on Snapshots")) if zynthian_gui_config.enable_dpm: - self.list_data.append( - (self.toggle_dpm, 0, "\u2612 Mixer Peak Meters")) + self.list_data.append((self.toggle_dpm, 0, "\u2612 Mixer Peak Meters")) else: - self.list_data.append( - (self.toggle_dpm, 0, "\u2610 Mixer Peak Meters")) + self.list_data.append((self.toggle_dpm, 0, "\u2610 Mixer Peak Meters")) self.list_data.append((None, 0, "> NETWORK")) self.list_data.append((self.network_info, 0, "Network Info")) - self.list_data.append( - (self.wifi_config, 0, f"Wi-Fi Config ({self.wifi_status})")) + self.list_data.append((self.wifi_config, 0, f"Wi-Fi Config ({self.wifi_status})")) self.wifi_index = len(self.list_data) - 1 if zynconf.is_service_active("vncserver0"): - self.list_data.append( - (self.state_manager.stop_vncserver, 0, "\u2612 VNC Server")) + self.list_data.append((self.state_manager.stop_vncserver, 0, "\u2612 VNC Server")) else: - self.list_data.append( - (self.state_manager.start_vncserver, 0, "\u2610 VNC Server")) + self.list_data.append((self.state_manager.start_vncserver, 0, "\u2610 VNC Server")) self.list_data.append((None, 0, "> SETTINGS")) - self.list_data.append((self.bluetooth, 0, "Bluetooth")) + if not zynthian_gui_config.wiring_layout.startswith("V5"): + match zynthian_gui_config.touch_navigation: + case "touch_widgets": + touch_navigation_option = "touch-widgets" + case "v5_keypad_left": + touch_navigation_option = "V5 keypad at Left" + case "v5_keypad_right": + touch_navigation_option = "V5 keypad at right" + case _: + touch_navigation_option = "None" + self.list_data.append((self.touch_navigation_menu, 0, f"Touch Navigation: {touch_navigation_option}")) if "brightness_config" in self.zyngui.screens and self.zyngui.screens["brightness_config"].get_num_zctrls() > 0: - self.list_data.append( - (self.zyngui.brightness_config, 0, "Brightness")) + self.list_data.append((self.zyngui.brightness_config, 0, "Brightness")) if "cv_config" in self.zyngui.screens: self.list_data.append((self.show_cv_config, 0, "CV Settings")) - self.list_data.append( - (self.zyngui.calibrate_touchscreen, 0, "Calibrate Touchscreen")) + self.list_data.append((self.zyngui.calibrate_touchscreen, 0, "Calibrate Touchscreen")) + #self.list_data.append((self.zyngui.cuia_screen_clean, 0, "Clean Screen")) # What the hell is this? + self.list_data.append((self.bluetooth, 0, "Bluetooth")) self.list_data.append((None, 0, "> TEST")) self.list_data.append((self.test_audio, 0, "Test Audio")) @@ -222,11 +207,9 @@ def fill_list(self): self.list_data.append((None, 0, "> SYSTEM")) if self.zyngui.capture_log_fname: - self.list_data.append( - (self.workflow_capture_stop, 0, "\u2612 Capture Workflow")) + self.list_data.append((self.workflow_capture_stop, 0, "\u2612 Capture Workflow")) else: - self.list_data.append( - (self.workflow_capture_start, 0, "\u2610 Capture Workflow")) + self.list_data.append((self.workflow_capture_start, 0, "\u2610 Capture Workflow")) if self.state_manager.update_available: self.list_data.append((self.update_software, 0, "Update Software")) # self.list_data.append((self.update_system, 0, "Update Operating System")) @@ -259,8 +242,7 @@ def execute_commands(self): self.zyngui.add_info("EXECUTING:\n", "EMPHASIS") self.zyngui.add_info("{}\n".format(cmd)) try: - self.proc = Popen(cmd, shell=True, stdout=PIPE, - stderr=STDOUT, universal_newlines=True) + self.proc = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT, universal_newlines=True) self.zyngui.add_info("RESULT:\n", "EMPHASIS") for line in self.proc.stdout: if re.search("ERROR", line, re.IGNORECASE): @@ -279,8 +261,7 @@ def execute_commands(self): if error_counter > 0: logging.info("COMPLETED WITH {} ERRORS!".format(error_counter)) - self.zyngui.add_info( - "COMPLETED WITH {} ERRORS!".format(error_counter), "WARNING") + self.zyngui.add_info("COMPLETED WITH {} ERRORS!".format(error_counter), "WARNING") else: logging.info("COMPLETED OK!") self.zyngui.add_info("COMPLETED OK!", "SUCCESS") @@ -331,8 +312,7 @@ def killable_start_command(self, cmds): if not self.commands: logging.info("Starting Command Sequence") self.commands = cmds - self.thread = Thread( - target=self.killable_execute_commands, args=()) + self.thread = Thread(target=self.killable_execute_commands, args=()) self.thread.name = "killable command sequence" self.thread.daemon = True # thread dies with the program self.thread.start() @@ -361,7 +341,6 @@ def start_rbpi_headphones(self, save_config=True): }) # Call autoconnect after a little time zynautoconnect.request_audio_connect() - except Exception as e: logging.error(e) @@ -391,7 +370,6 @@ def default_rbpi_headphones(self): else: self.stop_rbpi_headphones(False) - def get_hotplug_menu_options(self): options = {} if zynthian_gui_config.hotplug_audio_enabled: @@ -418,7 +396,7 @@ def hotplug_audio_menu(self): def hotplug_audio_cb(self, option, value): zynautoconnect.pause() - match(value): + match value: case "enable_hotplug": self.zyngui.state_manager.start_busy("hotplug", "Enabling hotplug audio") zynautoconnect.enable_hotplug() @@ -472,13 +450,31 @@ def toggle_midi_sys(self): "ZYNTHIAN_MIDI_SYS_ENABLED": str(int(zynthian_gui_config.midi_sys_enabled)) }) - lib_zyncore.set_midi_system_events( - zynthian_gui_config.midi_sys_enabled) + lib_zyncore.set_midi_system_events(zynthian_gui_config.midi_sys_enabled) self.update_list() def bluetooth(self): self.zyngui.show_screen("bluetooth") + def touch_navigation_menu(self): + self.zyngui.screens['option'].config("Touch Navigation", + {"None": "", + "Touch-widgets": "touch_widgets", + "V5 keypad at left": "v5_keypad_left", + "V5 keypad at right": "v5_keypad_right"}, + self.touch_navigation_cb, + True) + self.zyngui.show_screen('option') + + def touch_navigation_cb(self, option, value): + if value != zynthian_gui_config.touch_navigation: + self.zyngui.show_confirm("Restart UI to apply touch-navigation settings?", + self.touch_navigation_cb_confirmed, value) + + def touch_navigation_cb_confirmed(self, value=""): + zynconf.save_config({"ZYNTHIAN_UI_TOUCH_NAVIGATION2": value}) + self.restart_gui() + # ------------------------------------------------------------------------- # Global Transpose editing # ------------------------------------------------------------------------- @@ -605,13 +601,12 @@ def test_audio(self): self.zyngui.show_info("TEST AUDIO") # self.killable_start_command(["mpg123 {}/audio/test.mp3".format(self.data_dir)]) self.killable_start_command( - ["mplayer -nogui -noconsolecontrols -nolirc -nojoystick -really-quiet -ao jack {}/audio/test.mp3".format(self.data_dir)]) + [f"mplayer -nogui -noconsolecontrols -nolirc -nojoystick -really-quiet -ao jack {self.data_dir}/audio/test.mp3"]) zynautoconnect.request_audio_connect() def test_midi(self): logging.info("TESTING MIDI") - self.zyngui.alt_mode = self.state_manager.toggle_midi_playback( - f"{self.data_dir}/mid/test.mid") + self.zyngui.alt_mode = self.state_manager.toggle_midi_playback(f"{self.data_dir}/mid/test.mid") def control_test(self, t='S'): logging.info("TEST CONTROL HARDWARE") @@ -665,8 +660,7 @@ def exit_to_console(self): self.zyngui.exit(101) def reboot(self): - self.zyngui.show_confirm( - "Do you really want to reboot?", self.reboot_confirmed) + self.zyngui.show_confirm("Do you really want to reboot?", self.reboot_confirmed) def reboot_confirmed(self, params=None): logging.info("REBOOT") @@ -675,8 +669,7 @@ def reboot_confirmed(self, params=None): self.zyngui.exit(100) def power_off(self): - self.zyngui.show_confirm( - "Do you really want to power off?", self.power_off_confirmed) + self.zyngui.show_confirm("Do you really want to power off?", self.power_off_confirmed) def power_off_confirmed(self, params=None): logging.info("POWER OFF") diff --git a/zyngui/zynthian_gui_audio_in.py b/zyngui/zynthian_gui_audio_in.py index 1cb4b7627..de76a19ef 100644 --- a/zyngui/zynthian_gui_audio_in.py +++ b/zyngui/zynthian_gui_audio_in.py @@ -27,18 +27,18 @@ # Zynthian specific modules import zynautoconnect -from zyngui.zynthian_gui_selector import zynthian_gui_selector +from zyngui.zynthian_gui_selector_info import zynthian_gui_selector_info # ------------------------------------------------------------------------------ # Zynthian Audio-In Selection GUI Class # ------------------------------------------------------------------------------ -class zynthian_gui_audio_in(zynthian_gui_selector): +class zynthian_gui_audio_in(zynthian_gui_selector_info): def __init__(self): self.chain = None - super().__init__('Audio In', True) + super().__init__('Audio In') def set_chain(self, chain): self.chain = chain @@ -68,10 +68,12 @@ def fill_list(self): suffix = "" if i + 1 in self.chain.audio_in: self.list_data.append( - (i + 1, scp.name, f"\u2612 Audio input {i + 1}{suffix}")) + (i + 1, scp.name, f"\u2612 Audio input {i + 1}{suffix}", + [f"Audio input {i + 1} is connected to this chain.", "audio_input.png"])) else: self.list_data.append( - (i + 1, scp.name, f"\u2610 Audio input {i + 1}{suffix}")) + (i + 1, scp.name, f"\u2610 Audio input {i + 1}{suffix}", + [f"Audio input {i + 1} is disconnected from this chain.", "audio_input.png"])) super().fill_list() diff --git a/zyngui/zynthian_gui_audio_out.py b/zyngui/zynthian_gui_audio_out.py index 33ec24d8f..79710111c 100644 --- a/zyngui/zynthian_gui_audio_out.py +++ b/zyngui/zynthian_gui_audio_out.py @@ -28,9 +28,7 @@ # Zynthian specific modules import zynautoconnect from zyngine.zynthian_signal_manager import zynsigman -from zyngui import zynthian_gui_config -from zyngui.zynthian_gui_selector import zynthian_gui_selector -from zyngine.zynthian_engine_modui import zynthian_engine_modui +from zyngui.zynthian_gui_selector_info import zynthian_gui_selector_info from zyngine.zynthian_audio_recorder import zynthian_audio_recorder # ------------------------------------------------------------------------------ @@ -38,11 +36,11 @@ # ------------------------------------------------------------------------------ -class zynthian_gui_audio_out(zynthian_gui_selector): +class zynthian_gui_audio_out(zynthian_gui_selector_info): def __init__(self): self.chain = None - super().__init__('Audio Out', True) + super().__init__('Audio Out') def build_view(self): self.check_ports = 0 @@ -81,7 +79,7 @@ def fill_list(self): self.list_data = [] if self.chain.chain_id: # Normal chain so add mixer / chain targets - port_names = [("Main mixbus", 0)] + port_names = [("Main mixbus", 0, ["Send audio from this chain to the main mixbus", "audio_output.png"])] self.list_data.append((None, None, "> Chain inputs")) for chain_id, chain in self.zyngui.chain_manager.chains.items(): if chain_id != 0 and chain != self.chain and chain.audio_thru or chain.is_synth() and chain.synth_slots[0][0].type == "Special": @@ -89,19 +87,19 @@ def fill_list(self): prefix = "∞ " else: prefix = "" - port_names.append((f"{prefix}{chain.get_name()}", chain_id)) + port_names.append((f"{prefix}{chain.get_name()}", chain_id, [f"Send audio from this chain to the input of chain {chain.get_name()}.", "audio_output.png"])) # Add side-chain targets for processor in chain.get_processors(): try: for port_name in zynautoconnect.get_sidechain_portnames(processor.jackname): - port_names.append((f"↣ side {port_name}", port_name)) + port_names.append((f"↣ side {port_name}", port_name), [f"Send audio from this chain to the sidechain input of processor {port_name}.", "audio_output.png"]) except: pass - for title, processor in port_names: + for title, processor, info in port_names: if processor in self.chain.audio_out: - self.list_data.append((processor, processor, "\u2612 " + title)) + self.list_data.append((processor, processor, "\u2612 " + title, info)) else: - self.list_data.append((processor, processor, "\u2610 " + title)) + self.list_data.append((processor, processor, "\u2610 " + title, info)) if self.chain.is_audio(): port_names = [] @@ -113,19 +111,19 @@ def fill_list(self): suffix = f" ({self.playback_ports[i].aliases[0]})" else: suffix = "" - port_names.append((f"Output {i + 1}{suffix}", f"^{self.playback_ports[i].name}$")) + port_names.append((f"Output {i + 1}{suffix}", f"^{self.playback_ports[i].name}$", [f"Send audio from this chain directly to physical audio output {i + 1} as mono.", "audio_output.png"])) if i < port_count: if self.playback_ports[i + 1].aliases: suffix = f" ({self.playback_ports[i + 1].aliases[0]})" else: suffix = "" - port_names.append((f"Output {i + 2}{suffix}", f"^{self.playback_ports[i + 1].name}$")) - port_names.append((f"Outputs {i + 1}+{i + 2} (stereo)", f"^{self.playback_ports[i].name}$|^{self.playback_ports[i + 1].name}$")) - for title, processor in port_names: + port_names.append((f"Output {i + 2}{suffix}", f"^{self.playback_ports[i + 1].name}$", [f"Send audio from this chain directly to physical audio output {i + 2} as mono.", "audio_output.png"])) + port_names.append((f"Outputs {i + 1}+{i + 2} (stereo)", f"^{self.playback_ports[i].name}$|^{self.playback_ports[i + 1].name}$", [f"Send audio from this chain directly to physical audio outputs {i + 1} & {i + 2} as stereo.", "audio_output.png"])) + for title, processor, info in port_names: if processor in self.chain.audio_out: - self.list_data.append((processor, processor, "\u2612 " + title)) + self.list_data.append((processor, processor, "\u2612 " + title, info)) else: - self.list_data.append((processor, processor, "\u2610 " + title)) + self.list_data.append((processor, processor, "\u2610 " + title, info)) self.list_data.append((None, None, "> Audio Recorder")) armed = self.zyngui.state_manager.audio_recorder.is_armed(self.chain.mixer_chan) @@ -134,9 +132,9 @@ def fill_list(self): else: locked = "record" if armed: - self.list_data.append((locked, 'record_disable', '\u2612 Record chain')) + self.list_data.append((locked, 'record_disable', '\u2612 Record chain', [f"The chain will be recorded as a stereo track within a multitrack audio recording.", "audio_output.png"])) else: - self.list_data.append((locked, 'record_enable', '\u2610 Record chain')) + self.list_data.append((locked, 'record_enable', '\u2610 Record chain', [f"The chain will be not be recorded as a stereo track within a multitrack audio recording.", "audio_output.png"])) super().fill_list() diff --git a/zyngui/zynthian_gui_base.py b/zyngui/zynthian_gui_base.py index 2fdeae0a3..78b184436 100644 --- a/zyngui/zynthian_gui_base.py +++ b/zyngui/zynthian_gui_base.py @@ -46,8 +46,8 @@ class zynthian_gui_base(tkinter.Frame): def __init__(self, has_backbutton=True): tkinter.Frame.__init__(self, zynthian_gui_config.top, - width=zynthian_gui_config.display_width, - height=zynthian_gui_config.display_height) + width=zynthian_gui_config.screen_width, + height=zynthian_gui_config.screen_height) self.grid_propagate(False) self.rowconfigure(1, weight=1) self.columnconfigure(0, weight=1) @@ -60,14 +60,13 @@ def __init__(self, has_backbutton=True): self.buttonbar_button = [] # Geometry vars - self.buttonbar_height = zynthian_gui_config.display_height // 7 - self.width = zynthian_gui_config.display_width + self.buttonbar_height = zynthian_gui_config.screen_height // 7 + self.width = zynthian_gui_config.screen_width # TODO: Views should use current height if they need dynamic changes else grow rows to fill main_frame if zynthian_gui_config.enable_touch_navigation and self.buttonbar_config: - self.height = zynthian_gui_config.display_height - \ - self.topbar_height - self.buttonbar_height + self.height = zynthian_gui_config.screen_height - self.topbar_height - self.buttonbar_height else: - self.height = zynthian_gui_config.display_height - self.topbar_height + self.height = zynthian_gui_config.screen_height - self.topbar_height # Status Area Parameters self.status_l = int(self.width * 0.25) @@ -85,10 +84,10 @@ def __init__(self, has_backbutton=True): self.backbutton_height = 0 # Title Area parameters - self.title_canvas_width = zynthian_gui_config.display_width - \ - self.backbutton_width - self.status_l - self.status_lpad - 2 - self.select_path_font = tkFont.Font( - family=zynthian_gui_config.font_topbar[0], size=zynthian_gui_config.font_topbar[1]) + self.title_canvas_width = self.width - self.backbutton_width - self.status_l - self.status_lpad - 2 + self.select_path_font = tkFont.Font(family=zynthian_gui_config.font_topbar[0], + size=zynthian_gui_config.font_topbar[1]) + self.select_path_width = 0 self.select_path_offset = 0 self.select_path_dir = 2 @@ -100,7 +99,7 @@ def __init__(self, has_backbutton=True): # Topbar's frame self.tb_frame = tkinter.Frame(self, - width=zynthian_gui_config.display_width, + width=self.width, height=self.topbar_height, bg=zynthian_gui_config.color_bg) self.tb_frame.grid_propagate(False) @@ -119,8 +118,7 @@ def __init__(self, has_backbutton=True): self.backbutton_canvas.grid(row=0, column=col, sticky="wn", padx=(0, self.status_lpad)) self.backbutton_canvas.grid_propagate(False) self.backbutton_canvas.bind('', self.cb_backbutton) - self.backbutton_canvas.bind( - '', self.cb_backbutton_release) + self.backbutton_canvas.bind('', self.cb_backbutton_release) self.backbutton_timer = None col += 1 # Add back-arrow symbol @@ -131,8 +129,7 @@ def __init__(self, has_backbutton=True): fg=zynthian_gui_config.color_tx) self.label_backbutton.place(relx=0.3, rely=0.5, anchor='w') self.label_backbutton.bind('', self.cb_backbutton) - self.label_backbutton.bind( - '', self.cb_backbutton_release) + self.label_backbutton.bind('', self.cb_backbutton_release) # Title self.title = "" @@ -215,8 +212,7 @@ def __init__(self, has_backbutton=True): def show_back_button(self, show=True): if show: - self.backbutton_canvas.grid( - row=0, column=0, sticky="wn", padx=(0, self.status_lpad)) + self.backbutton_canvas.grid(row=0, column=0, sticky="wn", padx=(0, self.status_lpad)) self.backbutton_canvas.grid_propagate(False) else: self.backbutton_canvas.grid_remove() @@ -277,19 +273,14 @@ def init_buttonbar(self, config=None): return self.buttonbar_frame = tkinter.Frame(self, - width=zynthian_gui_config.display_width, + width=self.width, height=self.buttonbar_height, bg=zynthian_gui_config.color_bg) self.buttonbar_frame.grid(row=2, padx=(0, 0), pady=(0, 0)) self.buttonbar_frame.grid_propagate(False) - self.buttonbar_frame.grid_rowconfigure( - 0, minsize=self.buttonbar_height, pad=0) + self.buttonbar_frame.grid_rowconfigure(0, minsize=self.buttonbar_height, pad=0) for i in range(max(4, len(config))): - self.buttonbar_frame.grid_columnconfigure( - i, - weight=1, - uniform='buttonbar', - pad=0) + self.buttonbar_frame.grid_columnconfigure(i, weight=1, uniform='buttonbar', pad=0) try: self.add_button(i, config[i][0], config[i][1]) except Exception as e: @@ -378,8 +369,7 @@ def set_button_status(self, column, status=False): # Default topbar touch callback def cb_topbar_press(self, params=None): - self.topbar_timer = Timer( - zynthian_gui_config.zynswitch_long_seconds, self.cb_topbar_long) + self.topbar_timer = Timer(zynthian_gui_config.zynswitch_long_seconds, self.cb_topbar_long) self.topbar_timer.start() self.topbar_press_time = time.monotonic() @@ -414,8 +404,7 @@ def topbar_long_touch_action(self): # Default status touch callback def cb_status_press(self, params=None): - self.status_timer = Timer( - zynthian_gui_config.zynswitch_long_seconds, self.cb_status_long) + self.status_timer = Timer(zynthian_gui_config.zynswitch_long_seconds, self.cb_status_long) self.status_timer.start() self.status_press_time = time.monotonic() @@ -458,8 +447,7 @@ def status_long_touch_action(self): # Default menu button touch callback def cb_backbutton(self, params=None): - self.backbutton_timer = Timer( - zynthian_gui_config.zynswitch_long_seconds, self.cb_backbutton_long) + self.backbutton_timer = Timer(zynthian_gui_config.zynswitch_long_seconds, self.cb_backbutton_long) self.backbutton_timer.start() self.backbutton_press_time = time.monotonic() @@ -504,11 +492,10 @@ def build_view(self): def show(self): if not self.shown: if self.zyngui.test_mode: - logging.warning("TEST_MODE: {}".format( - self.__class__.__module__)) + logging.warning("TEST_MODE: {}".format(self.__class__.__module__)) self.shown = True self.refresh_status() - self.grid(row=0, column=0, sticky='nsew') + self.grid(row=0, column=zynthian_gui_config.main_screen_column, sticky='nsew') self.propagate(False) self.main_frame.focus() @@ -877,10 +864,10 @@ def set_select_path(self): # Override if required def update_layout(self): if zynthian_gui_config.enable_touch_navigation and self.buttonbar_config: - self.height = zynthian_gui_config.display_height - \ + self.height = zynthian_gui_config.screen_height - \ self.topbar_height - self.buttonbar_height else: - self.height = zynthian_gui_config.display_height - self.topbar_height + self.height = zynthian_gui_config.screen_height - self.topbar_height # Function to enable the top-bar parameter editor # engine: Object to recieve send_controller_value callback diff --git a/zyngui/zynthian_gui_chain_options.py b/zyngui/zynthian_gui_chain_options.py index a2783ceeb..f37bdccd8 100644 --- a/zyngui/zynthian_gui_chain_options.py +++ b/zyngui/zynthian_gui_chain_options.py @@ -28,17 +28,17 @@ # Zynthian specific modules from zyngui import zynthian_gui_config -from zyngui.zynthian_gui_selector import zynthian_gui_selector +from zyngui.zynthian_gui_selector_info import zynthian_gui_selector_info # ------------------------------------------------------------------------------ # Zynthian Chain Options GUI Class # ------------------------------------------------------------------------------ -class zynthian_gui_chain_options(zynthian_gui_selector): +class zynthian_gui_chain_options(zynthian_gui_selector_info): def __init__(self): - super().__init__('Option', True) + super().__init__('Option') self.index = 0 self.chain = None self.chain_id = None @@ -58,70 +58,81 @@ def fill_list(self): audio_proc_count = self.chain.get_processor_count("Audio Effect") if self.chain.is_midi(): - self.list_data.append( - (self.chain_note_range, None, "Note Range & Transpose")) - self.list_data.append((self.chain_midi_capture, None, "MIDI In")) + self.list_data.append((self.chain_note_range, None, "Note Range & Transpose", + ["Configure note range and transpose by octaves and semitones.", "note_range.png"])) + self.list_data.append((self.chain_midi_capture, None, "MIDI In", + ["Manage MIDI input sources. Enable/disable MIDI sources, toggle active/multi-timbral mode, load controller drivers, etc.", "midi_input.png"])) if self.chain.midi_thru: - self.list_data.append((self.chain_midi_routing, None, "MIDI Out")) + self.list_data.append((self.chain_midi_routing, None, "MIDI Out", + ["Manage MIDI output routing to external devices and other chains.", "midi_output.png"])) if self.chain.is_midi(): try: if synth_proc_count == 0 or self.chain.synth_slots[0][0].engine.options["midi_chan"]: - self.list_data.append((self.chain_midi_chan, None, "MIDI Channel")) + self.list_data.append((self.chain_midi_chan, None, "MIDI Channel", + ["Select MIDI channel to receive from.", "midi_logo.png"])) except Exception as e: logging.error(e) if synth_proc_count: - self.list_data.append((self.chain_midi_cc, None, "MIDI CC")) + self.list_data.append((self.chain_midi_cc, None, "MIDI CC", + ["Select MIDI CC numbers passed-thru to chain processors. It could interfere with MIDI-learning. Use with caution!", "midi_logo.png"])) if self.chain.get_processor_count() and not zynthian_gui_config.check_wiring_layout(["Z2", "V5"]): # TODO Disable midi learn for some chains??? - self.list_data.append((self.midi_learn, None, "MIDI Learn")) + self.list_data.append((self.midi_learn, None, "MIDI Learn", + ["Enter MIDI-learning mode for processor parameters.", ""])) if self.chain.audio_thru and self.chain_id != 0: - self.list_data.append((self.chain_audio_capture, None, "Audio In")) + self.list_data.append((self.chain_audio_capture, None, "Audio In", + ["Manage audio capture sources.", "audio_input.png"])) if self.chain.is_audio(): - self.list_data.append((self.chain_audio_routing, None, "Audio Out")) + self.list_data.append((self.chain_audio_routing, None, "Audio Out", + ["Manage audio output routing.", "audio_output.png"])) if self.chain.is_audio(): - self.list_data.append((self.audio_options, None, "Audio Options")) + self.list_data.append((self.audio_options, None, "Mixer Options", + ["Extra audio mixer options.", "audio_options.png"])) # TODO: Catch signal for Audio Recording status change if self.chain_id == 0 and not zynthian_gui_config.check_wiring_layout(["Z2", "V5"]): if self.zyngui.state_manager.audio_recorder.status: - self.list_data.append((self.toggle_recording, None, "■ Stop Audio Recording")) + self.list_data.append((self.toggle_recording, None, "■ Stop Audio Recording", ["Stop audio recording", ""])) else: - self.list_data.append((self.toggle_recording, None, "⬤ Start Audio Recording")) + self.list_data.append((self.toggle_recording, None, "⬤ Start Audio Recording", ["Start audio recording", ""])) self.list_data.append((None, None, "> Processors")) if self.chain.is_midi(): # Add MIDI-FX options - self.list_data.append((self.midifx_add, None, "Add MIDI-FX")) + self.list_data.append((self.midifx_add, None, "Add MIDI-FX", + ["Add a new MIDI processor to process chain's MIDI input.", "midi_processor.png"])) self.list_data += self.generate_chaintree_menu() if self.chain.is_audio(): # Add Audio-FX options - self.list_data.append((self.audiofx_add, None, "Add Pre-fader Audio-FX")) - self.list_data.append((self.postfader_add, None, "Add Post-fader Audio-FX")) + self.list_data.append((self.audiofx_add, None, "Add Pre-fader Audio-FX", + ["Add a new audio processor to process chain's audio before the mixer's fader.", "audio_processor.png"])) + self.list_data.append((self.postfader_add, None, "Add Post-fader Audio-FX", + ["Add a new audio processor to process chain's audio after the mixer's fader.", "audio_processor.png"])) if self.chain_id != 0: if synth_proc_count * midi_proc_count + audio_proc_count == 0: - self.list_data.append((self.remove_chain, None, "Remove Chain")) + self.list_data.append((self.remove_chain, None, "Remove Chain", ["Remove this chain and all its processors.", "delete.png"])) else: - self.list_data.append((self.remove_cb, None, "Remove...")) - self.list_data.append((self.export_chain, None, "Export chain as snapshot...")) + self.list_data.append((self.remove_cb, None, "Remove...", ["Remove chain or processors.", "delete.png"])) + self.list_data.append((self.export_chain, None, "Export chain as snapshot...", ["Save the selected chain as a snapshot which may then be imported into another snapshot.", None])) elif audio_proc_count > 0: - self.list_data.append((self.remove_all_audiofx, None, "Remove all Audio-FX")) + self.list_data.append((self.remove_all_audiofx, None, "Remove all Audio-FX", ["Remove all audio-FX processors in this chain.", "delete.png"])) self.list_data.append((None, None, "> GUI")) - self.list_data.append((self.rename_chain, None, "Rename chain")) + self.list_data.append((self.rename_chain, None, "Rename chain", ["Rename the chain. Clear name to reset to default name.", None])) if self.chain_id: if len(self.zyngui.chain_manager.ordered_chain_ids) > 2: - self.list_data.append((self.move_chain, None, "Move chain ⇦ ⇨")) + self.list_data.append((self.move_chain, None, "Move chain ⇦ ⇨", ["Reposition the chain in the mixer view.", None])) super().fill_list() @@ -136,17 +147,21 @@ def generate_chaintree_menu(self): for index, processor in enumerate(procs): name = processor.get_name() if index == num_procs - 1: - res.append((self.processor_options, processor, - " " * indent + "╰─ " + name)) + text = " " * indent + "╰─ " + name else: - res.append((self.processor_options, processor, - " " * indent + "├─ " + name)) + text = " " * indent + "├─ " + name + + res.append((self.processor_options, processor, text, + [f"Options for MIDI processor '{name}'", "midi_processor.png"])) + indent += 1 # Add synth processor for slot in self.chain.synth_slots: - for proc in slot: - res.append((self.processor_options, proc, " " * - indent + "╰━ " + proc.get_name())) + for processor in slot: + name = processor.get_name() + text = " " * indent + "╰━ " + name + res.append((self.processor_options, processor, text, + [f"Options for synth processor '{name}'", "synth_processor.png"])) indent += 1 # Build pre-fader audio effects chain for slot in range(self.chain.fader_pos): @@ -157,11 +172,11 @@ def generate_chaintree_menu(self): for index, processor in enumerate(procs): name = processor.get_name() if index == num_procs - 1: - res.append((self.processor_options, processor, - " " * indent + "┗━ " + name)) + text = " " * indent + "┗━ " + name else: - res.append((self.processor_options, processor, - " " * indent + "┣━ " + name)) + text = " " * indent + "┣━ " + name + res.append((self.processor_options, processor, text, + [f"Options for pre-fader audio processor '{name}'", "audio_processor.png"])) indent += 1 # Add FADER mark if self.chain.audio_thru or self.chain.synth_slots: @@ -174,11 +189,11 @@ def generate_chaintree_menu(self): for index, processor in enumerate(procs): name = processor.get_name() if index == num_procs - 1: - res.append((self.processor_options, processor, - " " * indent + "┗━ " + name)) + text = " " * indent + "┗━ " + name else: - res.append((self.processor_options, processor, - " " * indent + "┣━ " + name)) + text = " " * indent + "┣━ " + name + res.append((self.processor_options, processor, text, + [f"Options for post-fader audio processor '{name}'", "audio_processor.png"])) indent += 1 return res @@ -314,31 +329,31 @@ def chain_audio_routing(self): def audio_options(self): options = {} if self.zyngui.state_manager.zynmixer.get_mono(self.chain.mixer_chan): - options['\u2612 Mono'] = 'mono' + options['\u2612 Mono'] = ['mono', ["Chain is mono.\n\nLeft and right inputs are summed and fed as mono to left and right outputs", None]] else: - options['\u2610 Mono'] = 'mono' + options['\u2610 Mono'] = ['mono', ["Chain is stereo.\n\nLeft input feeds left output and right input feeds right output.", None]] if self.zyngui.state_manager.zynmixer.get_phase(self.chain.mixer_chan): - options['\u2612 Phase reverse'] = 'phase' + options['\u2612 Phase reverse'] = ['phase', ["Chain is phase reversed.\n\nRight output is inverted, making it 180° out of phase with its input.", None]] else: - options['\u2610 Phase reverse'] = 'phase' + options['\u2610 Phase reverse'] = ['phase', ["Chain is not phase reversed.\n\nLeft and right inputs feed left and right outputs without phase modification.", None]] if self.zyngui.state_manager.zynmixer.get_ms(self.chain.mixer_chan): - options['\u2612 M+S'] = 'ms' + options['\u2612 M+S'] = ['ms', ["Mid/Side mode is enabled.\n\nLeft output carries the 'Mid' signal. Right output carries the 'Side' signal.", None]] else: - options['\u2610 M+S'] = 'ms' + options['\u2610 M+S'] = ['ms', ["Mid/Side mode is disabled.\n\nLeft and right inputs feed left and right outputs.", None]] self.zyngui.screens['option'].config( - "Audio options", options, self.audio_menu_cb) + "Mixer options", options, self.audio_menu_cb, False, False, None) self.zyngui.show_screen('option') def audio_menu_cb(self, options, params): if params == 'mono': self.zyngui.state_manager.zynmixer.toggle_mono( self.chain.mixer_chan) - elif params == 'ms': - self.zyngui.state_manager.zynmixer.toggle_ms(self.chain.mixer_chan) elif params == 'phase': self.zyngui.state_manager.zynmixer.toggle_phase( self.chain.mixer_chan) + elif params == 'ms': + self.zyngui.state_manager.zynmixer.toggle_ms(self.chain.mixer_chan) self.audio_options() def chain_audio_capture(self): @@ -377,7 +392,7 @@ def export_chain(self): for dir in dirs: if dir.startswith(".") or not os.path.isdir(f"{self.zyngui.state_manager.snapshot_dir}/{dir}"): continue - options[dir] = dir + options[dir] = [dir, ["Choose folder to store snapshot.", "folder.png"]] self.zyngui.screens['option'].config( "Select location for export", options, self.name_export) self.zyngui.show_screen('option') diff --git a/zyngui/zynthian_gui_config.py b/zyngui/zynthian_gui_config.py index 0f501e036..f23a6ee86 100644 --- a/zyngui/zynthian_gui_config.py +++ b/zyngui/zynthian_gui_config.py @@ -39,8 +39,7 @@ log_level = int(os.environ.get('ZYNTHIAN_LOG_LEVEL', logging.WARNING)) # log_level = logging.DEBUG -logging.basicConfig(format='%(levelname)s:%(module)s.%(funcName)s: %(message)s', - stream=sys.stderr, level=log_level) +logging.basicConfig(format='%(levelname)s:%(module)s.%(funcName)s: %(message)s', stream=sys.stderr, level=log_level) logging.getLogger().setLevel(level=log_level) # Reduce log level for other modules @@ -52,10 +51,10 @@ # Wiring layout # ------------------------------------------------------------------------------ -wiring_layout = os.environ.get('ZYNTHIAN_WIRING_LAYOUT', "DUMMIES") -if wiring_layout == "DUMMIES": - logging.info( - "No Wiring Layout configured. Only touch interface is available.") +wiring_layout = os.environ.get('ZYNTHIAN_WIRING_LAYOUT', "TOUCH_ONLY") +if wiring_layout in ("TOUCH_ONLY", "DUMMIES"): + wiring_layout = "TOUCH_ONLY" + logging.info("No Wiring Layout configured. Only touch interface is available.") else: logging.info("Wiring Layout %s" % wiring_layout) @@ -131,10 +130,8 @@ def config_zynswitch_timing(): global zynswitch_bold_seconds global zynswitch_long_seconds try: - zynswitch_bold_us = 1000 * \ - int(os.environ.get('ZYNTHIAN_UI_SWITCH_BOLD_MS', 300)) - zynswitch_long_us = 1000 * \ - int(os.environ.get('ZYNTHIAN_UI_SWITCH_LONG_MS', 2000)) + zynswitch_bold_us = 1000 * int(os.environ.get('ZYNTHIAN_UI_SWITCH_BOLD_MS', 300)) + zynswitch_long_us = 1000 * int(os.environ.get('ZYNTHIAN_UI_SWITCH_LONG_MS', 2000)) zynswitch_bold_seconds = zynswitch_bold_us / 1000000 zynswitch_long_seconds = zynswitch_long_us / 1000000 @@ -245,6 +242,7 @@ def config_custom_switches(): custom_switch_ui_actions.append(cuias) custom_switch_midi_events.append(midi_event) + #logging.debug(f"CUSTOM_SWITCH_UI_ACTIONS => \n {custom_switch_ui_actions}") def config_zynpot2switch(): @@ -327,15 +325,13 @@ def config_zynaptik(): if "4xAD" in zynaptik_config: for i in range(4): root_varname = "ZYNTHIAN_WIRING_ZYNAPTIK_AD{:02d}".format(i+1) - zynaptik_ad_midi_events.append( - get_zynsensor_config(root_varname)) + zynaptik_ad_midi_events.append(get_zynsensor_config(root_varname)) # Zynaptik DA Action Configuration if "4xDA" in zynaptik_config: for i in range(4): root_varname = "ZYNTHIAN_WIRING_ZYNAPTIK_DA{:02d}".format(i+1) - zynaptik_da_midi_events.append( - get_zynsensor_config(root_varname)) + zynaptik_da_midi_events.append(get_zynsensor_config(root_varname)) def config_zyntof(): @@ -370,39 +366,28 @@ def set_midi_config(): global master_midi_bank_change_down_ccnum, master_midi_bank_base # MIDI options - midi_fine_tuning = float(os.environ.get( - 'ZYNTHIAN_MIDI_FINE_TUNING', "440.0")) - active_midi_channel = int(os.environ.get( - 'ZYNTHIAN_MIDI_ACTIVE_CHANNEL', "0")) - midi_prog_change_zs3 = int(os.environ.get( - 'ZYNTHIAN_MIDI_PROG_CHANGE_ZS3', "1")) + midi_fine_tuning = float(os.environ.get('ZYNTHIAN_MIDI_FINE_TUNING', "440.0")) + active_midi_channel = int(os.environ.get('ZYNTHIAN_MIDI_ACTIVE_CHANNEL', "0")) + midi_prog_change_zs3 = int(os.environ.get('ZYNTHIAN_MIDI_PROG_CHANGE_ZS3', "1")) midi_bank_change = int(os.environ.get('ZYNTHIAN_MIDI_BANK_CHANGE', "0")) - preset_preload_noteon = int(os.environ.get( - 'ZYNTHIAN_MIDI_PRESET_PRELOAD_NOTEON', "1")) + preset_preload_noteon = int(os.environ.get('ZYNTHIAN_MIDI_PRESET_PRELOAD_NOTEON', "1")) midi_sys_enabled = int(os.environ.get('ZYNTHIAN_MIDI_SYS_ENABLED', "1")) midi_usb_by_port = int(os.environ.get("ZYNTHIAN_MIDI_USB_BY_PORT", "0")) - midi_network_enabled = int(os.environ.get( - 'ZYNTHIAN_MIDI_NETWORK_ENABLED', "0")) - midi_netump_enabled = int(os.environ.get( - 'ZYNTHIAN_MIDI_NETUMP_ENABLED', "0")) - midi_rtpmidi_enabled = int(os.environ.get( - 'ZYNTHIAN_MIDI_RTPMIDI_ENABLED', "0")) - midi_touchosc_enabled = int(os.environ.get( - 'ZYNTHIAN_MIDI_TOUCHOSC_ENABLED', "0")) + midi_network_enabled = int(os.environ.get('ZYNTHIAN_MIDI_NETWORK_ENABLED', "0")) + midi_netump_enabled = int(os.environ.get('ZYNTHIAN_MIDI_NETUMP_ENABLED', "0")) + midi_rtpmidi_enabled = int(os.environ.get('ZYNTHIAN_MIDI_RTPMIDI_ENABLED', "0")) + midi_touchosc_enabled = int(os.environ.get('ZYNTHIAN_MIDI_TOUCHOSC_ENABLED', "0")) bluetooth_enabled = int(os.environ.get('ZYNTHIAN_MIDI_BLE_ENABLED', "0")) ble_controller = os.environ.get('ZYNTHIAN_MIDI_BLE_CONTROLLER', "") - midi_aubionotes_enabled = int(os.environ.get( - 'ZYNTHIAN_MIDI_AUBIONOTES_ENABLED', "0")) - transport_clock_source = int(os.environ.get( - 'ZYNTHIAN_MIDI_TRANSPORT_CLOCK_SOURCE', "0")) + midi_aubionotes_enabled = int(os.environ.get('ZYNTHIAN_MIDI_AUBIONOTES_ENABLED', "0")) + transport_clock_source = int(os.environ.get('ZYNTHIAN_MIDI_TRANSPORT_CLOCK_SOURCE', "0")) # Filter Rules midi_filter_rules = os.environ.get('ZYNTHIAN_MIDI_FILTER_RULES', "") midi_filter_rules = midi_filter_rules.replace("\\n", "\n") # Master Channel Features - master_midi_channel = int(os. environ.get( - "ZYNTHIAN_MIDI_MASTER_CHANNEL", 0)) + master_midi_channel = int(os. environ.get("ZYNTHIAN_MIDI_MASTER_CHANNEL", 0)) master_midi_channel -= 1 if master_midi_channel > 15: master_midi_channel = 15 @@ -411,52 +396,42 @@ def set_midi_config(): else: mmc_hex = None - master_midi_change_type = os.environ.get( - "ZYNTHIAN_MIDI_MASTER_CHANGE_TYPE", "Roland") + master_midi_change_type = os.environ.get("ZYNTHIAN_MIDI_MASTER_CHANGE_TYPE", "Roland") # Use LSB Bank by default - master_midi_bank_change_ccnum = int(os.environ.get( - "ZYNTHIAN_MIDI_MASTER_BANK_CHANGE_CCNUM", 0x20)) + master_midi_bank_change_ccnum = int(os.environ.get("ZYNTHIAN_MIDI_MASTER_BANK_CHANGE_CCNUM", 0x20)) # Use MSB Bank by default # master_midi_bank_change_ccnum = int(os.environ.get("ZYNTHIAN_MIDI_MASTER_BANK_CHANGE_CCNUM", 0x00)) mmpcu = os.environ.get('ZYNTHIAN_MIDI_MASTER_PROGRAM_CHANGE_UP', "") if mmc_hex and len(mmpcu) == 4: - master_midi_program_change_up = int( - "{:<06}".format(mmpcu.replace("#", mmc_hex)), 16) + master_midi_program_change_up = int("{:<06}".format(mmpcu.replace("#", mmc_hex)), 16) else: master_midi_program_change_up = None mmpcd = os.environ.get('ZYNTHIAN_MIDI_MASTER_PROGRAM_CHANGE_DOWN', "") if mmc_hex and len(mmpcd) == 4: - master_midi_program_change_down = int( - "{:<06}".format(mmpcd.replace("#", mmc_hex)), 16) + master_midi_program_change_down = int("{:<06}".format(mmpcd.replace("#", mmc_hex)), 16) else: master_midi_program_change_down = None mmbcu = os.environ.get('ZYNTHIAN_MIDI_MASTER_BANK_CHANGE_UP', "") if mmc_hex and len(mmbcu) == 6: - master_midi_bank_change_up = int( - "{:<06}".format(mmbcu.replace("#", mmc_hex)), 16) + master_midi_bank_change_up = int("{:<06}".format(mmbcu.replace("#", mmc_hex)), 16) else: master_midi_bank_change_up = None mmbcd = os.environ.get('ZYNTHIAN_MIDI_MASTER_BANK_CHANGE_DOWN', "") if mmc_hex and len(mmbcd) == 6: - master_midi_bank_change_down = int( - "{:<06}".format(mmbcd.replace("#", mmc_hex)), 16) + master_midi_bank_change_down = int("{:<06}".format(mmbcd.replace("#", mmc_hex)), 16) else: master_midi_bank_change_down = None - logging.debug("MMC Bank Change CCNum: {}".format( - master_midi_bank_change_ccnum)) + logging.debug("MMC Bank Change CCNum: {}".format(master_midi_bank_change_ccnum)) logging.debug("MMC Bank Change UP: {}".format(master_midi_bank_change_up)) - logging.debug("MMC Bank Change DOWN: {}".format( - master_midi_bank_change_down)) - logging.debug("MMC Program Change UP: {}".format( - master_midi_program_change_up)) - logging.debug("MMC Program Change DOWN: {}".format( - master_midi_program_change_down)) + logging.debug("MMC Bank Change DOWN: {}".format(master_midi_bank_change_down)) + logging.debug("MMC Program Change UP: {}".format(master_midi_program_change_up)) + logging.debug("MMC Program Change DOWN: {}".format(master_midi_program_change_down)) # Master Note CUIA mmncuia_envar = os.environ.get('ZYNTHIAN_MIDI_MASTER_NOTE_CUIA', None) @@ -476,8 +451,7 @@ def set_midi_config(): else: raise Exception("Bad format!") except Exception as err: - logging.warning( - "Bad MIDI Master Note CUIA config {} => {}".format(cuianote, err)) + logging.warning("Bad MIDI Master Note CUIA config {} => {}".format(cuianote, err)) # ------------------------------------------------------------------------------ # External storage (removable disks) @@ -538,32 +512,67 @@ def get_external_storage_dirs(exdpath): # Touch Options # ------------------------------------------------------------------------------ -enable_touch_widgets = int(os.environ.get('ZYNTHIAN_UI_TOUCH_WIDGETS', 0)) -enable_touch_navigation = int( - os.environ.get('ZYNTHIAN_UI_TOUCH_NAVIGATION', 0)) -force_enable_cursor = int(os.environ.get('ZYNTHIAN_UI_ENABLE_CURSOR', 0)) - -if check_wiring_layout(["Z2", "V5"]): - # TODO: BW: Do we need to inhibit touch mimic of V5 encoders? - enable_touch_controller_switches = 0 -else: - enable_touch_controller_switches = 1 +touch_navigation = os.environ.get('ZYNTHIAN_UI_TOUCH_NAVIGATION2', '_UNDEF_') + +# Backward compatibility +if touch_navigation == "_UNDEF_": + touch_navigation = os.environ.get('ZYNTHIAN_UI_TOUCH_NAVIGATION', '') + if touch_navigation == "1": + touch_navigation = "touch_widgets" + elif touch_navigation == "0": + touch_keypad = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD', '') + if touch_keypad == "V5": + touch_navigation = "v5_keypad_left" + +match touch_navigation: + case "touch_widgets": + enable_touch_navigation = True + touch_keypad_option = "" + touch_keypad_side_left = True + enable_touch_controller_switches = 1 + main_screen_column = 0 + case "v5_keypad_left": + enable_touch_navigation = False + touch_keypad_option = "V5" + touch_keypad_side_left = True + enable_touch_controller_switches = 1 + main_screen_column = 1 + case "v5_keypad_right": + enable_touch_navigation = False + touch_keypad_option = "V5" + touch_keypad_side_left = False + enable_touch_controller_switches = 1 + main_screen_column = 0 + case _: + enable_touch_navigation = False + touch_keypad_option = "" + touch_keypad_side_left = True + enable_touch_controller_switches = 0 + main_screen_column = 0 + +try: + force_enable_cursor = int(os.environ.get('ZYNTHIAN_UI_ENABLE_CURSOR', 0)) +except: + force_enable_cursor = 0 + +# Configure switch actions for touch only configuration so it works with touch-keypad +if touch_keypad_option == "V5" and wiring_layout =="TOUCH_ONLY": + if os.environ.get("ZYNTHIAN_WIRING_LAYOUT_CUSTOM_PROFILE", "") != "v5": + config_dir = os.environ.get("ZYNTHIAN_CONFIG_DIR", "/zynthian/config") + zynconf.load_plain_envars(f"{config_dir}/wiring-profiles/v5", True) + os.environ["ZYNTHIAN_WIRING_SWITCHES"] = ",".join(36 * ["-1"]) # ------------------------------------------------------------------------------ # UI Options # ------------------------------------------------------------------------------ restore_last_state = int(os.environ.get('ZYNTHIAN_UI_RESTORE_LAST_STATE', 0)) -snapshot_mixer_settings = int(os.environ.get( - 'ZYNTHIAN_UI_SNAPSHOT_MIXER_SETTINGS', 0)) +snapshot_mixer_settings = int(os.environ.get('ZYNTHIAN_UI_SNAPSHOT_MIXER_SETTINGS', 0)) show_cpu_status = int(os.environ.get('ZYNTHIAN_UI_SHOW_CPU_STATUS', 0)) -visible_mixer_strips = int(os.environ.get( - 'ZYNTHIAN_UI_VISIBLE_MIXER_STRIPS', 0)) +visible_mixer_strips = int(os.environ.get('ZYNTHIAN_UI_VISIBLE_MIXER_STRIPS', 0)) ctrl_graph = int(os.environ.get('ZYNTHIAN_UI_CTRL_GRAPH', 1)) -control_test_enabled = int(os.environ.get( - 'ZYNTHIAN_UI_CONTROL_TEST_ENABLED', 0)) -power_save_secs = 60 * \ - int(os.environ.get('ZYNTHIAN_UI_POWER_SAVE_MINUTES', 60)) +control_test_enabled = int(os.environ.get('ZYNTHIAN_UI_CONTROL_TEST_ENABLED', 0)) +power_save_secs = 60 * int(os.environ.get('ZYNTHIAN_UI_POWER_SAVE_MINUTES', 60)) # ------------------------------------------------------------------------------ # Audio Options @@ -592,8 +601,7 @@ def get_external_storage_dirs(exdpath): # Experimental features # ------------------------------------------------------------------------------ -experimental_features = os.environ.get( - 'ZYNTHIAN_EXPERIMENTAL_FEATURES', "").split(',') +experimental_features = os.environ.get('ZYNTHIAN_EXPERIMENTAL_FEATURES', "").split(',') # ------------------------------------------------------------------------------ # Sequence states @@ -637,11 +645,9 @@ def get_external_storage_dirs(exdpath): def color_variant(hex_color, brightness_offset=1): """ takes a color like #87c95f and produces a lighter or darker variant """ if len(hex_color) != 7: - raise Exception( - "Passed %s into color_variant(), needs to be in #87c95f format." % hex_color) + raise Exception("Passed %s into color_variant(), needs to be in #87c95f format." % hex_color) rgb_hex = [hex_color[x:x + 2] for x in [1, 3, 5]] - new_rgb_int = [int(hex_value, 16) + - brightness_offset for hex_value in rgb_hex] + new_rgb_int = [int(hex_value, 16) + brightness_offset for hex_value in rgb_hex] # make sure new values are between 0 and 255 new_rgb_int = [min([255, max([0, i])]) for i in new_rgb_int] # hex() produces "0x88", we want just "88" @@ -689,13 +695,37 @@ def color_variant(hex_color, brightness_offset=1): if not font_size: font_size = int(display_width / 40) + touch_keypad = None + # Touch Keypad enabled => + if touch_keypad_option == 'V5': + # Screen dimensions < Display dimensions + touch_keypad_side_width = display_height // 3 + touch_keypad_bottom_height = display_height // 6 + screen_width = display_width - touch_keypad_side_width + screen_height = display_height - touch_keypad_bottom_height + # Create touch keypad frame and show it! + try: + from zyngui.zynthian_gui_touchkeypad_v5 import zynthian_gui_touchkeypad_v5 + touch_keypad = zynthian_gui_touchkeypad_v5(top, side_width=touch_keypad_side_width, left_side=touch_keypad_side_left) + touch_keypad.show() + except Exception as e: + logging.error(f"Can't start touch keypad {touch_keypad_option} => {e}") + + # Touch Keypad disabled or failed to start => + if not touch_keypad: + # Screen dimensions = Display dimensions + touch_keypad_side_width = 0 + touch_keypad_bottom_height = 0 + screen_width = display_width + screen_height = display_height + # Geometric params - button_width = display_width // 4 - if display_width >= 800: - topbar_height = display_height // 12 + button_width = screen_width // 4 + if screen_width >= 800: + topbar_height = screen_height // 12 topbar_fs = int(1.5*font_size) else: - topbar_height = display_height // 10 + topbar_height = screen_height // 10 topbar_fs = int(1.1*font_size) # Adjust Root Window Geometry @@ -704,7 +734,7 @@ def color_variant(hex_color, brightness_offset=1): top.minsize(display_width, display_height) # Disable cursor for real Zynthian Boxes - if force_enable_cursor or wiring_layout == "EMULATOR" or wiring_layout == "DUMMIES": + if force_enable_cursor or wiring_layout == "EMULATOR" or wiring_layout == "TOUCH_ONLY": top.config(cursor="arrow") else: top.config(cursor="none") @@ -722,7 +752,7 @@ def color_variant(hex_color, brightness_offset=1): loading_imgs = [] pil_frame = Image.open("./img/zynthian_gui_loading.gif") fw, fh = pil_frame.size - fw2 = display_width // 4 - 8 + fw2 = screen_width // 4 - 8 fh2 = int(fh * fw2 / fw) nframes = 0 while pil_frame: @@ -738,8 +768,7 @@ def color_variant(hex_color, brightness_offset=1): # loading_imgs.append(tkinter.PhotoImage(file="./img/zynthian_gui_loading.gif", format="gif -index "+str(i))) except Exception as e: - logging.error( - "ERROR initializing Tkinter graphic framework => {}".format(e)) + logging.error("ERROR initializing Tkinter graphic framework => {}".format(e)) # ------------------------------------------------------------------------------ # Initialize ZynCore low-level library diff --git a/zyngui/zynthian_gui_confirm.py b/zyngui/zynthian_gui_confirm.py index eabb7c131..ac5155ac8 100644 --- a/zyngui/zynthian_gui_confirm.py +++ b/zyngui/zynthian_gui_confirm.py @@ -44,11 +44,13 @@ def __init__(self): self.callback = None self.callback_params = None self.zyngui = zynthian_gui_config.zyngui + self.width = zynthian_gui_config.screen_width + self.height = zynthian_gui_config.screen_height # Main Frame self.main_frame = tkinter.Frame(zynthian_gui_config.top, - width=zynthian_gui_config.display_width, - height=zynthian_gui_config.display_height, + width=self.width, + height=self.height, bg=zynthian_gui_config.color_bg) self.text = tkinter.StringVar() @@ -56,7 +58,7 @@ def __init__(self): font=(zynthian_gui_config.font_family, zynthian_gui_config.font_size, "normal"), textvariable=self.text, - wraplength=zynthian_gui_config.display_width-zynthian_gui_config.font_size*2, + wraplength=self.width-zynthian_gui_config.font_size*2, justify=tkinter.LEFT, padx=zynthian_gui_config.font_size, pady=zynthian_gui_config.font_size, @@ -66,7 +68,8 @@ def __init__(self): self.yes_text_label = tkinter.Label(self.main_frame, font=( - zynthian_gui_config.font_family, zynthian_gui_config.font_size*2, "normal"), + zynthian_gui_config.font_family, + zynthian_gui_config.font_size*2, "normal"), text="Yes", width=3, justify=tkinter.RIGHT, @@ -75,12 +78,12 @@ def __init__(self): bg=zynthian_gui_config.color_ctrl_bg_off, fg=zynthian_gui_config.color_tx) self.yes_text_label.bind("", self.cb_yes_push) - self.yes_text_label.place(x=zynthian_gui_config.display_width, - y=zynthian_gui_config.display_height, anchor=tkinter.SE) + self.yes_text_label.place(x=self.width, y=self.height, anchor=tkinter.SE) self.no_text_label = tkinter.Label(self.main_frame, font=( - zynthian_gui_config.font_family, zynthian_gui_config.font_size*2, "normal"), + zynthian_gui_config.font_family, + zynthian_gui_config.font_size*2, "normal"), text="No", width=3, justify=tkinter.LEFT, @@ -89,8 +92,7 @@ def __init__(self): bg=zynthian_gui_config.color_ctrl_bg_off, fg=zynthian_gui_config.color_tx) self.no_text_label.bind("", self.cb_no_push) - self.no_text_label.place( - x=0, y=zynthian_gui_config.display_height, anchor=tkinter.SW) + self.no_text_label.place(x=0, y=self.height, anchor=tkinter.SW) def hide(self): if self.shown: @@ -108,7 +110,7 @@ def show(self, text, callback=None, cb_params=None): self.callback_params = cb_params if not self.shown: self.shown = True - self.main_frame.grid() + self.main_frame.grid(row=0, column=zynthian_gui_config.main_screen_column) def zynpot_cb(self, i, dval): pass diff --git a/zyngui/zynthian_gui_control.py b/zyngui/zynthian_gui_control.py index 323321778..9ac3e7a37 100644 --- a/zyngui/zynthian_gui_control.py +++ b/zyngui/zynthian_gui_control.py @@ -460,6 +460,10 @@ def rotate_chain(self): # t: Press type ["S"=Short, "B"=Bold, "L"=Long] # returns True if action fully handled or False if parent action should be triggered def switch(self, swi, t='S'): + if t == 'B' and self.midi_learning: + self.midi_learn_options(swi) + return True + if swi == 0: if t == 'S': self.rotate_chain() @@ -481,7 +485,7 @@ def switch(self, swi, t='S'): if self.mode == 'control': return False elif t == 'B': - if self.midi_learning and self.zyngui.state_manager.midi_learn_cc: + if self.midi_learning and self.zyngui.state_manager.midi_learn_zctrl: self.midi_unlearn_action() return True @@ -665,28 +669,30 @@ def midi_learn_options(self, i, unlearn_only=False): zctrl = self.zgui_controllers[i].zctrl if zctrl is None: return + mcparams = self.zyngui.chain_manager.get_midi_learn_from_zctrl(zctrl) if not unlearn_only: title = "Control options" - options["X-Y touchpad"] = None - # Only show X-Y if both zctrl are valid - if self.zyngui.state_manager.zctrl_x and self.zyngui.state_manager.zctrl_y: - options["Control"] = True - if self.zyngui.state_manager.zctrl_x: - xinfo = f" => {self.zyngui.state_manager.zctrl_x.name}" - else: - xinfo = "" - if zctrl == self.zyngui.state_manager.zctrl_x: - options[f"\u2612 X-axis{xinfo}"] = False - else: - options[f"\u2610 X-axis{xinfo}"] = zctrl - if self.zyngui.state_manager.zctrl_y: - yinfo = f" => {self.zyngui.state_manager.zctrl_y.name}" - else: - yinfo = "" - if zctrl == self.zyngui.state_manager.zctrl_y: - options[f"\u2612 Y-axis{yinfo}"] = False - else: - options[f"\u2610 Y-axis{yinfo}"] = zctrl + if not zctrl.is_toggle: + options["X-Y touchpad"] = None + # Only show X-Y if both zctrl are valid + if self.zyngui.state_manager.zctrl_x and self.zyngui.state_manager.zctrl_y: + options["Control"] = True + if self.zyngui.state_manager.zctrl_x: + xinfo = f" => {self.zyngui.state_manager.zctrl_x.name}" + else: + xinfo = "" + if zctrl == self.zyngui.state_manager.zctrl_x: + options[f"\u2612 X-axis{xinfo}"] = False + else: + options[f"\u2610 X-axis{xinfo}"] = zctrl + if self.zyngui.state_manager.zctrl_y: + yinfo = f" => {self.zyngui.state_manager.zctrl_y.name}" + else: + yinfo = "" + if zctrl == self.zyngui.state_manager.zctrl_y: + options[f"\u2612 Y-axis{yinfo}"] = False + else: + options[f"\u2610 Y-axis{yinfo}"] = zctrl options["MIDI learn"] = None if zctrl.is_toggle: @@ -694,7 +700,7 @@ def midi_learn_options(self, i, unlearn_only=False): options["\u2612 Momentary => Latch"] = i else: options["\u2610 Momentary => Latch"] = i - else: + elif mcparams: if zctrl.midi_cc_mode == 0: options["\u2610 Relative Mode"] = i else: @@ -704,10 +710,9 @@ def midi_learn_options(self, i, unlearn_only=False): else: title = "Control unlearn" - params = self.zyngui.chain_manager.get_midi_learn_from_zctrl(zctrl) - if params: - if params[1]: - dev_name = zynautoconnect.get_midi_in_devid(params[0] >> 24) + if mcparams: + if mcparams[1]: + dev_name = zynautoconnect.get_midi_in_devid(mcparams[0] >> 24) options[f"Unlearn '{zctrl.name}' from {dev_name}"] = zctrl else: options[f"Unlearn '{zctrl.name}'"] = zctrl @@ -768,8 +773,8 @@ def cb_listbox_release(self, event): now = monotonic() dts = now - self.listbox_push_ts - rdts = now - self.last_release - self.last_release = now + rdts = now - self.last_release_ts + self.last_release_ts = now if self.swiping: self.swipe_nudge(dts) else: diff --git a/zyngui/zynthian_gui_control_xy.py b/zyngui/zynthian_gui_control_xy.py old mode 100644 new mode 100755 index 796fcef74..411f50469 --- a/zyngui/zynthian_gui_control_xy.py +++ b/zyngui/zynthian_gui_control_xy.py @@ -5,7 +5,7 @@ # # Zynthian GUI XY-Controller Class # -# Copyright (C) 2015-2022 Fernando Moyano +# Copyright (C) 2015-2024 Fernando Moyano # # ****************************************************************************** # @@ -50,13 +50,13 @@ def __init__(self): # Init X vars self.padx = 24 - self.width = zynthian_gui_config.display_width - 2 * self.padx + self.width = zynthian_gui_config.screen_width - 2 * self.padx self.x = self.width / 2 self.xvalue = 64 # Init Y vars self.pady = 18 - self.height = zynthian_gui_config.display_height - 2 * self.pady + self.height = zynthian_gui_config.screen_height - 2 * self.pady self.y = self.height / 2 self.yvalue = 64 @@ -64,8 +64,8 @@ def __init__(self): # Main Frame self.main_frame = tkinter.Frame(zynthian_gui_config.top, - width=zynthian_gui_config.display_width, - height=zynthian_gui_config.display_height, + width=zynthian_gui_config.screen_width, + height=zynthian_gui_config.screen_height, bg=zynthian_gui_config.color_panel_bg) # Create Canvas @@ -81,23 +81,22 @@ def __init__(self): # Setup Canvas Callbacks self.canvas.bind("", self.cb_canvas) - if zynthian_gui_config.enable_touch_navigation: - self.last_tap = 0 - self.tap_count = 0 - self.canvas.bind("", self.cb_press) + self.last_tap = 0 + self.tap_count = 0 + self.canvas.bind("", self.cb_press) # Create Cursor self.hline = self.canvas.create_line( 0, self.y, - zynthian_gui_config.display_width, + zynthian_gui_config.screen_width, self.y, fill=zynthian_gui_config.color_on) self.vline = self.canvas.create_line( self.x, 0, self.x, - zynthian_gui_config.display_width, + zynthian_gui_config.screen_width, fill=zynthian_gui_config.color_on) def build_view(self): @@ -113,10 +112,9 @@ def build_view(self): def show(self): if not self.shown: if self.zyngui.test_mode: - logging.warning("TEST_MODE: {}".format( - self.__class__.__module__)) + logging.warning("TEST_MODE: {}".format(self.__class__.__module__)) self.shown = True - self.main_frame.grid() + self.main_frame.grid(row=0, column=zynthian_gui_config.main_screen_column) self.get_controller_values() self.refresh() diff --git a/zyngui/zynthian_gui_details.py b/zyngui/zynthian_gui_details.py index 570bec4ed..7db49e088 100644 --- a/zyngui/zynthian_gui_details.py +++ b/zyngui/zynthian_gui_details.py @@ -43,9 +43,9 @@ def __init__(self): # Textarea self.textarea = tkinter.Text(self.main_frame, width=int( - zynthian_gui_config.display_width/(zynthian_gui_config.font_size + 5)), + zynthian_gui_config.screen_width/(zynthian_gui_config.font_size + 5)), height=int( - zynthian_gui_config.display_height/(zynthian_gui_config.font_size + 8)), + zynthian_gui_config.screen_height/(zynthian_gui_config.font_size + 8)), font=(zynthian_gui_config.font_family, zynthian_gui_config.font_size, "normal"), wrap='word', diff --git a/zyngui/zynthian_gui_help.py b/zyngui/zynthian_gui_help.py index eb666e308..41284533a 100644 --- a/zyngui/zynthian_gui_help.py +++ b/zyngui/zynthian_gui_help.py @@ -54,7 +54,13 @@ def __init__(self): self.touch_last_release_ts = 0 # Main Frame - self.main_frame = HtmlFrame(zynthian_gui_config.top, messages_enabled=False) + + self.main_frame = HtmlFrame(zynthian_gui_config.top, + width=zynthian_gui_config.screen_width, + height=zynthian_gui_config.screen_height, + vertical_scrollbar=False, + messages_enabled=False) + self.main_frame.grid_propagate(False) # Patch HtmlFrame widget self.main_frame.event_generate = self.main_frame.html.event_generate # Bind events @@ -88,7 +94,8 @@ def show(self): logging.warning("TEST_MODE: {}".format(self.__class__.__module__)) if not self.shown: self.shown = True - self.main_frame.grid() + self.main_frame.grid_propagate(False) + self.main_frame.grid(row=0, column=zynthian_gui_config.main_screen_column) def zynpot_cb(self, i, dval): if i == 3: diff --git a/zyngui/zynthian_gui_info.py b/zyngui/zynthian_gui_info.py index 83352cb8d..c71495c52 100644 --- a/zyngui/zynthian_gui_info.py +++ b/zyngui/zynthian_gui_info.py @@ -41,14 +41,14 @@ def __init__(self): # Main Frame self.main_frame = tkinter.Frame(zynthian_gui_config.top, - width=zynthian_gui_config.display_width, - height=zynthian_gui_config.display_height, + width=zynthian_gui_config.screen_width, + height=zynthian_gui_config.screen_height, bg=zynthian_gui_config.color_bg) # Textarea self.textarea = tkinter.Text(self.main_frame, height=int( - zynthian_gui_config.display_height/(zynthian_gui_config.font_size + 8)), + zynthian_gui_config.screen_height/(zynthian_gui_config.font_size + 8)), font=(zynthian_gui_config.font_family, zynthian_gui_config.font_size, "normal"), # font=("sans-serif", zynthian_gui_config.font_size, "normal"), @@ -90,7 +90,7 @@ def show(self, text): self.set(text) if not self.shown: self.shown = True - self.main_frame.grid() + self.main_frame.grid(row=0, column=zynthian_gui_config.main_screen_column) def zynpot_cb(self, i, dval): return True diff --git a/zyngui/zynthian_gui_keyboard.py b/zyngui/zynthian_gui_keyboard.py index 0c75bd524..4bbc3c8ee 100644 --- a/zyngui/zynthian_gui_keyboard.py +++ b/zyngui/zynthian_gui_keyboard.py @@ -58,33 +58,31 @@ def __init__(self): self.ctrl_order = zynthian_gui_config.layout['ctrl_order'] # Geometry vars - self.width = zynthian_gui_config.display_width - self.height = zynthian_gui_config.display_height - \ - zynthian_gui_config.topbar_height + self.width = zynthian_gui_config.screen_width + self.height = zynthian_gui_config.screen_height - zynthian_gui_config.topbar_height + # Fonts - self.font_button = (zynthian_gui_config.font_family, - int(1.2*zynthian_gui_config.font_size)) + self.font_button = (zynthian_gui_config.font_family, int(1.2*zynthian_gui_config.font_size)) # Create main frame self.main_frame = tkinter.Frame(zynthian_gui_config.top, - width=zynthian_gui_config.display_width, - height=zynthian_gui_config.display_height, + width=zynthian_gui_config.screen_width, + height=zynthian_gui_config.screen_height, bg=zynthian_gui_config.color_bg) self.main_frame.grid_propagate(False) # Display string being edited - self.text_canvas = tkinter.Canvas( - self.main_frame, width=self.width, height=zynthian_gui_config.topbar_height) + self.text_canvas = tkinter.Canvas(self.main_frame, width=self.width, height=zynthian_gui_config.topbar_height) self.text_label = self.text_canvas.create_text(self.width / 2, zynthian_gui_config.topbar_height / 2, font=zynthian_gui_config.font_topbar, - # font=tkFont.Font(family=zynthian_gui_config.font_topbar[0], size= int(zynthian_gui_config.topbar_height * 0.8)) + # font=tkFont.Font(family=zynthian_gui_config.font_topbar[0], + # size= int(zynthian_gui_config.topbar_height * 0.8)) ) self.text_canvas.grid(column=0, row=0, sticky="nsew") # Display keyboard grid - self.key_canvas = tkinter.Canvas( - self.main_frame, width=self.width, height=self.height, bg="grey") + self.key_canvas = tkinter.Canvas(self.main_frame, width=self.width, height=self.height, bg="grey") self.key_canvas.grid_propagate(False) self.key_canvas.grid(column=0, row=1, sticky="nesw") self.set_mode(OSK_QWERTY) @@ -138,9 +136,9 @@ def refresh_keys(self): elif self.alt: if self.shift: self.keys = ['\\', '|', '@', '/', '*', '=', '\"', '\'', '?', '¡', - 'Ä', 'Ë', 'Ï', 'Ö', 'Ü', 'Â', 'Ê', 'Î', 'Ô', 'Û', - 'Ñ', 'Ç', 'Ẅ', 'Ŵ', 'Ĉ', 'Ÿ', 'Ŷ', 'Ŝ', 'Ĝ', 'Ḧ', - 'Ĥ', 'Ĵ', 'Ẑ', 'Ẍ', '{', '}', '~', '^', ':', '_'] + 'Ä', 'Ë', 'Ï', 'Ö', 'Ü', 'Â', 'Ê', 'Î', 'Ô', 'Û', + 'Ñ', 'Ç', 'Ẅ', 'Ŵ', 'Ĉ', 'Ÿ', 'Ŷ', 'Ŝ', 'Ĝ', 'Ḧ', + 'Ĥ', 'Ĵ', 'Ẑ', 'Ẍ', '{', '}', '~', '^', ':', '_'] else: self.keys = ['á', 'é', 'í', 'ó', 'ú', 'à', 'è', 'ì', 'ò', 'ù', 'ä', 'ë', 'ï', 'ö', 'ü', 'â', 'ê', 'î', 'ô', 'û', @@ -159,8 +157,9 @@ def refresh_keys(self): 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '#'] self.key_canvas.itemconfig("keycaps", text="") for index in range(len(self.keys)): - self.key_canvas.itemconfig(self.buttons[index][1], text=self.keys[index], tags=( - "key:%d" % (index), "keycaps")) + self.key_canvas.itemconfig(self.buttons[index][1], + text=self.keys[index], + tags=("key:%d" % (index), "keycaps")) # Function to add a button to the keyboard # label: Button label @@ -171,14 +170,18 @@ def refresh_keys(self): def add_button(self, label, col, row, colspan=1): index = len(self.buttons) tag = "key:%d" % (index) - r = self.key_canvas.create_rectangle(1 + self.key_width * col, 1 + self.key_height * row, self.key_width * ( - col + colspan) - 1, self.key_height * (row + 1) - 1, tags=(tag), fill="black") - l = self.key_canvas.create_text(1 + self.key_width * (col + colspan / 2), 1 + self.key_height * (row + 0.5), + r = self.key_canvas.create_rectangle(1 + self.key_width * col, + 1 + self.key_height * row, + self.key_width * (col + colspan) - 1, + self.key_height * (row + 1) - 1, + tags=(tag), + fill="black") + l = self.key_canvas.create_text(1 + self.key_width * (col + colspan / 2), + 1 + self.key_height * (row + 0.5), text=label, fill="white", font=self.font_button, - tags=(tag) - ) + tags=(tag)) self.key_canvas.tag_bind(tag, "", self.on_key_press) self.key_canvas.tag_bind(tag, "", self.on_key_release) self.buttons.append([r, l]) @@ -191,8 +194,7 @@ def bold_press(self): # Function to handle key press # event: Mouse event def on_key_press(self, event=None): - tags = self.key_canvas.gettags( - self.key_canvas.find_withtag(tkinter.CURRENT)) + tags = self.key_canvas.gettags(self.key_canvas.find_withtag(tkinter.CURRENT)) if not tags: return dummy, index = tags[0].split(':') @@ -239,11 +241,9 @@ def execute_key_press(self, key, bold=False): elif key == self.btn_alt: self.alt = not self.alt if self.alt: - self.key_canvas.itemconfig( - self.buttons[self.btn_alt][0], fill="red") + self.key_canvas.itemconfig(self.buttons[self.btn_alt][0], fill="red") else: - self.key_canvas.itemconfig( - self.buttons[self.btn_alt][0], fill="black") + self.key_canvas.itemconfig(self.buttons[self.btn_alt][0], fill="black") self.refresh_keys() if key == self.btn_shift: @@ -258,14 +258,11 @@ def execute_key_press(self, key, bold=False): if shift != self.shift: if self.shift == 1: - self.key_canvas.itemconfig( - self.buttons[self.btn_shift][0], fill="grey") + self.key_canvas.itemconfig(self.buttons[self.btn_shift][0], fill="grey") elif self.shift == 2: - self.key_canvas.itemconfig( - self.buttons[self.btn_shift][0], fill="red") + self.key_canvas.itemconfig(self.buttons[self.btn_shift][0], fill="red") else: - self.key_canvas.itemconfig( - self.buttons[self.btn_shift][0], fill="black") + self.key_canvas.itemconfig(self.buttons[self.btn_shift][0], fill="black") self.refresh_keys() self.text_canvas.itemconfig(self.text_label, text=self.text) @@ -275,8 +272,7 @@ def execute_key_press(self, key, bold=False): def highlight(self, key): box = self.key_canvas.bbox(self.buttons[key][0]) if box: - self.key_canvas.coords( - self.highlight_box, box[0]+1, box[1]+1, box[2], box[3]) + self.key_canvas.coords(self.highlight_box, box[0]+1, box[1]+1, box[2], box[3]) # Function to hide dialog def hide(self): @@ -306,7 +302,7 @@ def show(self, function, text="", max_len=None): self.highlight(self.selected_button) self.setup_zynpots() self.refresh_keys() - self.main_frame.grid() + self.main_frame.grid(row=0, column=zynthian_gui_config.main_screen_column) self.shown = True # Function to register encoders diff --git a/zyngui/zynthian_gui_loading.py b/zyngui/zynthian_gui_loading.py index 8d38e96bd..4b710580c 100644 --- a/zyngui/zynthian_gui_loading.py +++ b/zyngui/zynthian_gui_loading.py @@ -39,8 +39,8 @@ class zynthian_gui_loading: def __init__(self): self.shown = False self.zyngui = zynthian_gui_config.zyngui - self.width = zynthian_gui_config.display_width - self.height = zynthian_gui_config.display_height + self.width = zynthian_gui_config.screen_width + self.height = zynthian_gui_config.screen_height # Canvas for loading image animation self.canvas = tkinter.Canvas( zynthian_gui_config.top, @@ -62,14 +62,15 @@ def __init__(self): int(0.85 * self.height), anchor=tkinter.CENTER, justify=tkinter.CENTER, - font=(zynthian_gui_config.font_family, int( - 0.8*zynthian_gui_config.font_size)), + font=(zynthian_gui_config.font_family, int(0.8*zynthian_gui_config.font_size)), fill=zynthian_gui_config.color_tx_off, text="") # Setup Loading Logo Animation self.loading_index = 0 self.loading_item = self.canvas.create_image( - self.width//2, self.height//2, image=zynthian_gui_config.loading_imgs[0], anchor=tkinter.CENTER) + self.width//2, self.height//2, + image=zynthian_gui_config.loading_imgs[0], + anchor=tkinter.CENTER) def build_view(self): return True @@ -82,7 +83,7 @@ def hide(self): def show(self): if not self.shown: self.shown = True - self.canvas.grid() + self.canvas.grid(row=0, column=zynthian_gui_config.main_screen_column) def set_error(self, txt): self.set_title(txt, zynthian_gui_config.color_error) @@ -115,16 +116,15 @@ def refresh_loading(self): self.loading_index += 1 if self.loading_index >= len(zynthian_gui_config.loading_imgs): self.loading_index = 0 - self.canvas.itemconfig( - self.loading_item, image=zynthian_gui_config.loading_imgs[self.loading_index]) + self.canvas.itemconfig(self.loading_item, + image=zynthian_gui_config.loading_imgs[self.loading_index]) else: self.reset_loading() def reset_loading(self, force=False): if self.loading_index > 0 or force: self.loading_index = 0 - self.canvas.itemconfig( - self.loading_item, image=zynthian_gui_config.loading_imgs[0]) + self.canvas.itemconfig(self.loading_item, image=zynthian_gui_config.loading_imgs[0]) def zynpot_cb(self, i, dval): pass diff --git a/zyngui/zynthian_gui_midi_config.py b/zyngui/zynthian_gui_midi_config.py index bb628382c..ad0497cae 100644 --- a/zyngui/zynthian_gui_midi_config.py +++ b/zyngui/zynthian_gui_midi_config.py @@ -34,8 +34,9 @@ # Zynthian specific modules import zynautoconnect from zyncoder.zyncore import lib_zyncore -from zyngui.zynthian_gui_selector import zynthian_gui_selector +from zyngui.zynthian_gui_selector_info import zynthian_gui_selector_info from zyngui import zynthian_gui_config +import zynconf # ------------------------------------------------------------------------------ # Mini class to allow use of audio_in gui @@ -63,15 +64,18 @@ def toggle_audio_in(self, input): ZMIP_MODE_CONTROLLER = "⌨" # \u2328 ZMIP_MODE_ACTIVE = "⇥" # \u21e5 ZMIP_MODE_MULTI = "⇶" # \u21f6 +SERVICE_ICONS = { + "aubionotes": "midi_audio.png" +} -class zynthian_gui_midi_config(zynthian_gui_selector): +class zynthian_gui_midi_config(zynthian_gui_selector_info): def __init__(self): self.chain = None # Chain object self.input = True # True to process MIDI inputs, False for MIDI outputs self.thread = None - super().__init__('MIDI Devices', True) + super().__init__('Menu') def build_view(self): # Enable background scan for MIDI devices @@ -124,36 +128,41 @@ def append_port(idev): if self.input: port = zynautoconnect.devices_in[idev] mode = get_mode_str(idev) + input_mode_info = f"\n\n{ZMIP_MODE_ACTIVE} Active mode\n{ZMIP_MODE_MULTI} Multitimbral mode\n{ZMIP_MODE_CONTROLLER} Driver loaded" if self.chain is None: - self.list_data.append((port.aliases[0], idev, f"{mode}{port.aliases[1]}")) + self.list_data.append((port.aliases[0], idev, f"{mode}{port.aliases[1]}", + [f"Bold select to show options for '{port.aliases[1]}'.{input_mode_info}", "midi_input.png"])) elif not self.zyngui.state_manager.ctrldev_manager.is_input_device_available_to_chains(idev): - self.list_data.append((port.aliases[0], idev, f" {mode}{port.aliases[1]}")) + self.list_data.append((port.aliases[0], idev, f" {mode}{port.aliases[1]}", + [f"Bold select to show options '{port.aliases[1]}'.{input_mode_info}", "midi_input.png"])) else: if lib_zyncore.zmop_get_route_from(self.chain.zmop_index, idev): - self.list_data.append((port.aliases[0], idev, f"\u2612 {mode}{port.aliases[1]}")) + self.list_data.append((port.aliases[0], idev, f"\u2612 {mode}{port.aliases[1]}", + [f"'{port.aliases[1]}' connected to chain's MIDI input.\nBold select to show more options.{input_mode_info}", "midi_input.png"])) else: - self.list_data.append((port.aliases[0], idev, f"\u2610 {mode}{port.aliases[1]}")) + self.list_data.append((port.aliases[0], idev, f"\u2610 {mode}{port.aliases[1]}", + [f"'{port.aliases[1]}' disconnected from chain's MIDI input.\nBold select to show more options.{input_mode_info}", "midi_input.png"])) else: port = zynautoconnect.devices_out[idev] if self.chain is None: - self.list_data.append((port.aliases[0], idev, f"{port.aliases[1]}")) + self.list_data.append((port.aliases[0], idev, f"{port.aliases[1]}", + [f"Bold select to show options for '{port.aliases[1]}'.", "midi_output.png"])) elif port.aliases[0] in self.chain.midi_out: - self.list_data.append((port.aliases[0], idev, f"\u2612 {port.aliases[1]}")) + self.list_data.append((port.aliases[0], idev, f"\u2612 {port.aliases[1]}", + [f"Chain's MIDI output connected to '{port.aliases[1]}'.\nBold select to show more options.", "midi_output.png"])) else: - self.list_data.append((port.aliases[0], idev, f"\u2610 {port.aliases[1]}")) + self.list_data.append((port.aliases[0], idev, f"\u2610 {port.aliases[1]}", + [f"Chain's MIDI output disconnected from '{port.aliases[1]}'.\nBold select to show more options.", "midi_output.png"])) - def append_service_device(dev_name, obj): - """Add service (that is also a port) to list""" - if isinstance(obj, int): - if self.input: - port = zynautoconnect.devices_in[obj] - else: - port = zynautoconnect.devices_out[obj] - if port: - mode = get_mode_str(obj) - self.list_data.append((f"stop_{dev_name}", obj, f"\u2612 {mode}{port.aliases[1]}")) + def append_service(service, name, help_info=""): + if service in SERVICE_ICONS: + icon = SERVICE_ICONS[service] + else: + icon = "midi_logo.png" + if zynconf.is_service_active(service): + self.list_data.append((f"stop_{service}", None, f"\u2612 {name}", [f"Disable {help_info}", icon])) else: - self.list_data.append((f"start_{dev_name}", None, f"\u2610 {obj}")) + self.list_data.append((f"start_{service}", None, f"\u2610 {name}", [f"Enable {help_info}", icon])) def atoi(text): return int(text) if text.isdigit() else text @@ -196,10 +205,8 @@ def natural_keys(t): for i in aubio_devices: append_port(i) else: - if aubio_devices: - append_service_device("aubionotes", aubio_devices[0]) - else: - append_service_device("aubionotes", "Aubionotes (Audio \u2794 MIDI)") + append_service("aubionotes", "Aubionotes (Audio \u2794 MIDI)", + "Aubionotes. Converts audio input to MIDI note on/off commands.") # Remove "Internal Devices" title if section is empty if len(self.list_data) == nint: @@ -212,13 +219,10 @@ def natural_keys(t): if self.chain is None or ble_devices: self.list_data.append((None, None, "Bluetooth Devices")) - if zynthian_gui_config.bluetooth_enabled: - if self.chain is None: - self.list_data.append(("stop_bluetooth", None, "\u2612 BLE MIDI")) - for x in sorted(ble_devices, key=natural_keys): - append_port(x[1]) - elif self.chain is None: - self.list_data.append(("start_bluetooth", None, "\u2610 BLE MIDI")) + if self.chain is None: + append_service("bluetooth", "BLE MIDI", "Bluetooth MIDI.") + for x in sorted(ble_devices, key=natural_keys): + append_port(x[1]) if not self.chain or net_devices: self.list_data.append((None, None, "Network Devices")) @@ -227,36 +231,20 @@ def natural_keys(t): append_port(i) else: if os.path.isfile("/usr/local/bin/jacknetumpd"): - if "jacknetumpd:netump_in" in net_devices: - append_service_device("jacknetumpd", net_devices["jacknetumpd:netump_in"]) - elif "jacknetumpd:netump_out" in net_devices: - append_service_device("jacknetumpd", net_devices["jacknetumpd:netump_out"]) - else: - append_service_device("jacknetumpd", "NetUMP: MIDI 2.0") + append_service("jacknetumpd", "NetUMP: MIDI 2.0", + "NetUMP. Provides MIDI over an IP connection using NetUMP protocol (MIDI 2.0).") if os.path.isfile("/usr/local/bin/jackrtpmidid"): - if "jackrtpmidid:rtpmidi_in" in net_devices: - append_service_device("jackrtpmidid", net_devices["jackrtpmidid:rtpmidi_in"]) - elif "jackrtpmidid:rtpmidi_out" in net_devices: - append_service_device("jackrtpmidid", net_devices["jackrtpmidid:rtpmidi_out"]) - else: - append_service_device("jackrtpmidid", "RTP-MIDI") + append_service("jackrtpmidid", "RTP-MIDI", + "RTP-MIDI. Provides MIDI over an IP connection using RTP-MIDI protocol (AppleMIDI).") if os.path.isfile("/usr/local/bin/qmidinet"): - if "QmidiNet:in_1" in net_devices: - append_service_device("QmidiNet", net_devices["QmidiNet:in_1"]) - elif "QmidiNet:out_1" in net_devices: - append_service_device("QmidiNet", net_devices["QmidiNet:out_1"]) - else: - append_service_device("QmidiNet", "QmidiNet") + append_service("qmidinet", "QmidiNet", + "QmidiNet. Provides MIDI over an IP connection using UDP/IP multicast (ipMIDI).") if os.path.isfile("/zynthian/venv/bin/touchosc2midi"): - if "RtMidiIn Client:TouchOSC Bridge" in net_devices: - append_service_device("touchosc", net_devices["RtMidiIn Client:TouchOSC Bridge"]) - elif "RtMidiOut Client:TouchOSC Bridge" in net_devices: - append_service_device("touchosc", net_devices["RtMidiOut Client:TouchOSC Bridge"]) - else: - append_service_device("touchosc", "TouchOSC Bridge") + append_service("touchosc2midi", "TouchOSC Bridge", + "Interface with Hexler TouchOSC modular control surface.") if not self.input and self.chain: self.list_data.append((None, None, "> Chain inputs")) @@ -268,11 +256,13 @@ def natural_keys(t): else: prefix = "" if chain_id in self.chain.midi_out: - self.list_data.append( - (chain_id, None, f"\u2612 {prefix}{chain.get_name()}")) + self.list_data.append((chain_id, None, f"\u2612 {prefix}{chain.get_name()}", + [f"Chain's MIDI output connected to chain '{prefix}{chain.get_name()}'.", + "midi_output.png"])) else: - self.list_data.append( - (chain_id, None, f"\u2610 {prefix}{chain.get_name()}")) + self.list_data.append((chain_id, None, f"\u2610 {prefix}{chain.get_name()}", + [f"Chain's MIDI output disconnected from chain '{prefix}{chain.get_name()}'.", + "midi_output.png"])) super().fill_list() @@ -288,13 +278,13 @@ def select_action(self, i, t='S'): self.zyngui.state_manager.stop_rtpmidi(wait=wait) elif action == "start_jackrtpmidid": self.zyngui.state_manager.start_rtpmidi(wait=wait) - elif action == "stop_QmidiNet": + elif action == "stop_qmidinet": self.zyngui.state_manager.stop_qmidinet(wait=wait) - elif action == "start_QmidiNet": + elif action == "start_qmidinet": self.zyngui.state_manager.start_qmidinet(wait=wait) - elif action == "stop_touchosc": + elif action == "stop_touchosc2midi": self.zyngui.state_manager.stop_touchosc2midi(wait=wait) - elif action == "start_touchosc": + elif action == "start_touchosc2midi": self.zyngui.state_manager.start_touchosc2midi(wait=wait) elif action == "stop_aubionotes": self.zyngui.state_manager.stop_aubionotes(wait=wait) @@ -326,38 +316,52 @@ def select_action(self, i, t='S'): # Change mode elif t == 'B': - idev = self.list_data[i][1] + self.show_options() + + def show_options(self): + try: + idev = self.list_data[self.index][1] if idev is None: return - try: - options = {} - if self.input: - options["MIDI Input Mode"] = None - if zynautoconnect.get_midi_in_dev_mode(idev): - options[f'\u2610 {ZMIP_MODE_ACTIVE} Multitimbral mode '] = "MULTI" + options = {} + if self.input: + options["MIDI Input Mode"] = None + mode_info = "Toggle input mode.\n\n" + if zynautoconnect.get_midi_in_dev_mode(idev): + title = f"{ZMIP_MODE_ACTIVE} Active mode" + if lib_zyncore.get_active_midi_chan(): + mode_info += f"{title}. Translate MIDI channel. Send to chains matching active chain's MIDI channel." else: - options[f'\u2612 {ZMIP_MODE_MULTI} Multitimbral mode '] = "ACTI" - - options["Configuration"] = None - dev_id = zynautoconnect.get_midi_in_devid(idev) - if dev_id in self.zyngui.state_manager.ctrldev_manager.available_drivers: - # TODO: Offer list of profiles - if idev in self.zyngui.state_manager.ctrldev_manager.drivers: - options[f"\u2612 {ZMIP_MODE_CONTROLLER} Controller driver"] = "UNLOAD_DRIVER" - else: - options[f"\u2610 {ZMIP_MODE_CONTROLLER} Controller driver"] = "LOAD_DRIVER" - port = zynautoconnect.devices_in[idev] + mode_info += f"{title}. Translate MIDI channel. Send to active chain only." + options[title] = ["MULTI", [mode_info, None]] else: - port = zynautoconnect.devices_out[idev] - if self.list_data[i][0].startswith("AUBIO:") or self.list_data[i][0].endswith("aubionotes"): - options["Select aubio inputs"] = "AUBIO_INPUTS" - options[f"Rename port '{port.aliases[0]}'"] = port - # options[f"Reset name to '{zynautoconnect.build_midi_port_name(port)[1]}'"] = port - self.zyngui.screens['option'].config( - "MIDI Input Device", options, self.menu_cb) - self.zyngui.show_screen('option') - except: - pass # Port may have disappeared whilst building menu + title = f"{ZMIP_MODE_MULTI} Multitimbral mode" + mode_info += f"{title}. Don't translate MIDI channel. Send to chains matching device's MIDI channel." + options[title] = ["ACTI", [mode_info, None]] + options["Configuration"] = None + dev_id = zynautoconnect.get_midi_in_devid(idev) + if dev_id in self.zyngui.state_manager.ctrldev_manager.available_drivers: + # TODO: Offer list of profiles + if idev in self.zyngui.state_manager.ctrldev_manager.drivers: + options[f"\u2612 {ZMIP_MODE_CONTROLLER} Device driver"] = ["UNLOAD_DRIVER", + ["Driver enabled. A specific driver manage the device, integrating UI functions and customized workflow.", None]] + else: + options[f"\u2610 {ZMIP_MODE_CONTROLLER} Device driver"] = ["LOAD_DRIVER", + ["Driver disabled. The device is used as MIDI input for chains and MIDI-learning.", None]] + port = zynautoconnect.devices_in[idev] + else: + port = zynautoconnect.devices_out[idev] + if self.list_data[self.index][0].startswith("AUBIO:") or self.list_data[self.index][0].endswith("aubionotes"): + options["Select aubio inputs"] = ["AUBIO_INPUTS", + ["Select which audio inputs are connected to aubionotes Audio \u2794 MIDI.", + "midi_audio.png"]] + options[f"Rename port '{port.aliases[0]}'"] = [port, ["Rename the MIDI port.\nClear name to reset to default name.", None]] + # options[f"Reset name to '{zynautoconnect.build_midi_port_name(port)[1]}'"] = port + self.zyngui.screens['option'].config( + "MIDI Input Device", options, self.menu_cb, False, False, None) + self.zyngui.show_screen('option') + except: + pass # Port may have disappeared whilst building menu def menu_cb(self, option, params): try: @@ -375,10 +379,12 @@ def menu_cb(self, option, params): ain = aubio_inputs(self.zyngui.state_manager) self.zyngui.screens['audio_in'].set_chain(ain) self.zyngui.show_screen('audio_in') + return elif self.input: idev = self.list_data[self.index][1] lib_zyncore.zmip_set_flag_active_chain(idev, params == "ACTI") zynautoconnect.update_midi_in_dev_mode(idev) + self.show_options() self.update_list() except: pass # Ports may have changed since menu opened @@ -414,6 +420,7 @@ def rename_device(self, name): port = zynautoconnect.devices_out[self.list_data[self.index][1]] zynautoconnect.set_port_friendly_name(port, name) self.update_list() + self.zyngui.close_screen("option") def set_select_path(self): if self.chain: diff --git a/zyngui/zynthian_gui_midi_key_range.py b/zyngui/zynthian_gui_midi_key_range.py index faf369f5f..123706d19 100644 --- a/zyngui/zynthian_gui_midi_key_range.py +++ b/zyngui/zynthian_gui_midi_key_range.py @@ -67,7 +67,7 @@ def __init__(self): bg=zynthian_gui_config.color_panel_bg, bd=0, highlightthickness=0) - self.piano_canvas_width = zynthian_gui_config.display_width + self.piano_canvas_width = self.width self.piano_canvas_height = self.height // 4 self.main_frame.rowconfigure(2, weight=1) @@ -176,7 +176,7 @@ def update_piano(self): j += 1 midi_note += 1 - if self.black_keys_pattern[i % 7]: + if self.black_keys_pattern[i % 7] and j < len(self.piano_keys): if self.note_low > midi_note or self.note_high < midi_note: bgcolor = "#707070" else: diff --git a/zyngui/zynthian_gui_option.py b/zyngui/zynthian_gui_option.py index df1a02d50..bc9d7031a 100644 --- a/zyngui/zynthian_gui_option.py +++ b/zyngui/zynthian_gui_option.py @@ -5,7 +5,7 @@ # # Zynthian GUI Option Selector Class # -# Copyright (C) 2015-2020 Fernando Moyano +# Copyright (C) 2015-2024 Fernando Moyano # # ****************************************************************************** # @@ -29,14 +29,14 @@ from os.path import basename, splitext # Zynthian specific modules -from zyngui.zynthian_gui_selector import zynthian_gui_selector +from zyngui.zynthian_gui_selector_info import zynthian_gui_selector_info # ------------------------------------------------------------------------------ # Zynthian Option Selection GUI Class # ------------------------------------------------------------------------------ -class zynthian_gui_option(zynthian_gui_selector): +class zynthian_gui_option(zynthian_gui_selector_info): def __init__(self): self.title = "" @@ -45,9 +45,9 @@ def __init__(self): self.cb_select = None self.click_type = False self.close_on_select = True - super().__init__("Option", True) + super().__init__("Menu") - def config(self, title, options, cb_select, close_on_select=True, click_type=False): + def config(self, title, options, cb_select, close_on_select=True, click_type=False, index=0): self.title = title if callable(options): self.options_cb = options @@ -58,7 +58,8 @@ def config(self, title, options, cb_select, close_on_select=True, click_type=Fal self.cb_select = cb_select self.close_on_select = close_on_select self.click_type = click_type - self.index = 0 + if index is not None: + self.index = index def config_file_list(self, title, dpaths, fpat, cb_select, close_on_select=True, click_type=False): self.title = title @@ -93,7 +94,10 @@ def fill_list(self): if self.options_cb: self.options = self.options_cb() for k, v in self.options.items(): - self.list_data.append((v, i, k)) + if isinstance(v, list): + self.list_data.append((v[0], i, k, v[1])) + else: + self.list_data.append((v, i, k)) i += 1 super().fill_list() diff --git a/zyngui/zynthian_gui_selector_info.py b/zyngui/zynthian_gui_selector_info.py index 424dc17d1..2eb9f816c 100644 --- a/zyngui/zynthian_gui_selector_info.py +++ b/zyngui/zynthian_gui_selector_info.py @@ -114,7 +114,7 @@ def get_info(self): try: return self.list_data[self.index][3] except: - return None + return ["", ""] def update_info(self): info = self.get_info() diff --git a/zyngui/zynthian_gui_splash.py b/zyngui/zynthian_gui_splash.py index 66be8f4c6..cd123b17f 100644 --- a/zyngui/zynthian_gui_splash.py +++ b/zyngui/zynthian_gui_splash.py @@ -56,6 +56,8 @@ def hide(self): if self.shown: self.shown = False self.canvas.grid_forget() + if zynthian_gui_config.touch_keypad: + zynthian_gui_config.touch_keypad.show() def show(self, text): if self.zyngui.test_mode: @@ -80,6 +82,8 @@ def show(self, text): except: pass if not self.shown: + if zynthian_gui_config.touch_keypad: + zynthian_gui_config.touch_keypad.hide() self.shown = True self.canvas.grid() diff --git a/zyngui/zynthian_gui_touchkeypad_v5.py b/zyngui/zynthian_gui_touchkeypad_v5.py new file mode 100644 index 000000000..29926242a --- /dev/null +++ b/zyngui/zynthian_gui_touchkeypad_v5.py @@ -0,0 +1,369 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ****************************************************************************** +# ZYNTHIAN PROJECT: Zynthian GUI +# +# Zynthian Touchscreen Keypad V5 Class +# +# Copyright (C) 2024 Pavel Vondřička +# +# ****************************************************************************** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# For a full copy of the GNU General Public License see the LICENSE.txt file. +# +# ****************************************************************************** + +import os +import tkinter +from io import BytesIO +from PIL import Image, ImageTk + +try: + import cairosvg +except: + cairosvg = None + +# Zynthian specific modules +from zyngui import zynthian_gui_config + +# ------------------------------------------------------------------------------ +# Touchscreen V5 keypad configuration +# ------------------------------------------------------------------------------ + +# Button definitions and mapping + +BUTTONS = { + # labels, ZYNSWITCH number, wsLED number + 'OPT_ADMIN': ({'default': 'OPT/ADMIN'}, 4, 0), + 'MIX_LEVEL': ({'default': 'MIX/LEVEL'}, 5, 1), + 'CTRL_PRESET': ({'default': 'CTRL/PRESET'}, 6, 2), + 'ZS3_SHOT': ({'default': 'ZS3/SHOT'}, 7, 3), + 'METRONOME': ({'default': '_icons/metronome.svg'}, 9, 6), + 'PAD_STEP': ({'default': 'PAD/STEP'}, 10, 5), + 'ALT': ({'default': 'ALT'}, 8, 4), + + 'REC': ({'default': '\uf111'}, 12, 8), + 'STOP': ({'default': '\uf04d'}, 13, 9), + 'PLAY': ({'default': '\uf04b', 'active': '\uf04c'}, 14, 10), + + 'UP': ({'default': '\uf077'}, 17, 14), + 'DOWN': ({'default': '\uf078'}, 21, 17), + 'LEFT': ({'default': '\uf053'}, 20, 16), + 'RIGHT': ({'default': '\uf054'}, 22, 18), + 'SEL_YES': ({'default': 'SEL/YES'}, 18, 13), + 'BACK_NO': ({'default': 'BACK/NO'}, 16, 15), + + 'F1': ({'default': 'F1', 'alt': 'F5'}, 11, 7), + 'F2': ({'default': 'F2', 'alt': 'F6'}, 15, 11), + 'F3': ({'default': 'F3', 'alt': 'F7'}, 19, 12), + 'F4': ({'default': 'F4', 'alt': 'F8'}, 23, 19) +} + +FKEY2SWITCH = [BUTTONS['F1'][1], BUTTONS['F2'][1], BUTTONS['F3'][1], BUTTONS['F4'][1]] + +LED2BUTTON = {btn[2]: btn[1]-4 for btn in BUTTONS.values()} + +# Layout definitions + +LAYOUT_RIGHT = { + 'SIDE': ( + ('OPT_ADMIN', 'MIX_LEVEL'), + ('CTRL_PRESET', 'ZS3_SHOT'), + ('METRONOME', 'PAD_STEP'), + ('BACK_NO', 'SEL_YES'), + ('UP', 'ALT'), + ('DOWN', 'RIGHT') + ), + 'BOTTOM': ('F1', 'F2', 'F3', 'F4', 'REC', 'STOP', 'PLAY', 'LEFT') +} + +LAYOUT_LEFT = { + 'SIDE': ( + ('OPT_ADMIN', 'MIX_LEVEL'), + ('CTRL_PRESET', 'ZS3_SHOT'), + ('METRONOME', 'PAD_STEP'), + ('BACK_NO', 'SEL_YES'), + ('ALT', 'UP'), + ('LEFT', 'DOWN') + ), + 'BOTTOM': ('RIGHT', 'REC', 'STOP', 'PLAY', 'F1', 'F2', 'F3', 'F4') +} + +# ------------------------------------------------------------------------------ +# Zynthian Touchscreen Keypad V5 Class +# ------------------------------------------------------------------------------ + + +class zynthian_gui_touchkeypad_v5: + + def __init__(self, parent, side_width, left_side=True): + """ + Parameters + ---------- + parent : tkinter widget + Parent widget + side_width : int + Width of the side panel: base for the geometry + left_side : bool + Left or right side layout for the side frame + """ + self.shown = False + self.side_frame_width = side_width + self.bottom_frame_width = zynthian_gui_config.display_width - self.side_frame_width + self.side_frame_col = 0 if left_side else 1 + self.bottom_frame_col = 1 if left_side else 0 + self.font_size = zynthian_gui_config.font_size + self.bg_color = zynthian_gui_config.color_variant(zynthian_gui_config.color_panel_bg, -28) + self.bg_color_over = zynthian_gui_config.color_variant(zynthian_gui_config.color_panel_bg, -22) + self.border_color = zynthian_gui_config.color_bg + self.text_color = zynthian_gui_config.color_header_tx + + # configure side frame for 2x6 buttons + self.side_frame = tkinter.Frame(parent, + width=self.side_frame_width, + height=zynthian_gui_config.display_height, + bg=zynthian_gui_config.color_bg) + for column in range(2): + self.side_frame.columnconfigure(column, weight=1) + for row in range(6): + self.side_frame.rowconfigure(row, weight=1) + + # 2 columns by 6 buttons at the full diplay height and requested side frame width + self.side_button_width = self.side_frame_width // 2 + self.side_button_height = zynthian_gui_config.display_height // 6 + + # configure bottom frame for a single row of 8 buttons + self.bottom_frame = tkinter.Frame(parent, + width=self.bottom_frame_width, + # the height must correspond to the height of buttons in the side frame + height=zynthian_gui_config.display_height // 6, + bg=zynthian_gui_config.color_bg) + for column in range(8): + self.bottom_frame.columnconfigure(column, weight=1) + self.bottom_frame.rowconfigure(0, weight=1) + + # select layout as requested + layout = LAYOUT_LEFT if left_side else LAYOUT_RIGHT + + # buffers to remember the buttons and their contents and state + self.buttons = [None] * 20 # actual button widgets + self.btndefs = [None] * 20 # original definition of the button parameters + self.images = [None] * 20 # original image/icon used (if any) + self.btnstate = [None] * 20 # last state of the button (<=color) + self.tkimages = [None] * 20 # current image in tkinter format (avoid discarding by the garbage collector!) + + # create side frame buttons + for row in range(6): + for col in range(2): + btn = BUTTONS[layout['SIDE'][row][col]] + zynswitch = btn[1] + n = zynswitch - 4 + label = btn[0]['default'] + pady = (1, 0) if row == 5 else (0, 0) if row == 4 else (0, 1) + padx = (0, 1) if left_side else (1, 0) + self.btndefs[n] = btn + self.buttons[n] = self.add_button(n, self.side_frame, row, col, zynswitch, label, padx, pady) + # create bottom frame buttons + for col in range(8): + btn = BUTTONS[layout['BOTTOM'][col]] + zynswitch = btn[1] + n = zynswitch - 4 + label = btn[0]['default'] + padx = (0, 0) if col == 7 else (0, 1) + self.btndefs[n] = btn + self.buttons[n] = self.add_button(n, self.bottom_frame, 0, col, zynswitch, label, padx, (1, 0)) + + # update with user settings from the environment + self.apply_user_config() + + def add_button(self, n, parent, row, column, zynswitch, label, padx, pady): + """ + Create button + + Parameters: + ----------- + n : int + Number of the button + parent : tkinter widget + Parent widget + row : int + column : int + Position of the button in the grid + zynswitch : int + Number of the zynswitch to emulate + label : str + Default label for the button + padx : (int, int) + pady : (int, int) + Button padding + """ + button = tkinter.Button( + parent, + width=1, + height=1, + bg=self.bg_color, + fg=self.text_color, + activebackground=self.bg_color, + activeforeground=self.border_color, + highlightbackground=self.border_color, + highlightcolor=self.border_color, + highlightthickness=1, + bd=0, + relief='flat') + # set default button state (<=color) + self.btnstate[n] = self.text_color + if label.startswith('_'): + # button contains an icon/image instead of a label + img_width = int(1.8 * self.font_size) + img_name = label[1:] + if img_name.endswith('.svg'): + # convert SVG icon into PNG of appropriate size + if cairosvg: + png = BytesIO() + cairosvg.svg2png(url=img_name, write_to=png, output_width=img_width) + image = Image.open(png) + else: + png = img_name[:-4]+".png" + image = Image.open(png) + img_height = int(img_width * image.size[1] / image.size[0]) + image = image.resize((img_width, img_height), Image.Resampling.LANCZOS) + + elif img_name.endswith('.png'): + # PNG icons can be imported directly + image = Image.open(img_name) + img_height = int(img_width * image.size[1] / image.size[0]) + image = image.resize((img_width, img_height), Image.Resampling.LANCZOS) + else: + image = None + if image: + # store the original image for the purpose of later changes of color (useful for image icons) + self.images[n] = image + tkimage = ImageTk.PhotoImage(image) + # if we don't keep the image in the object, + # it will be discarded by garbage collection at the end of this method! + self.tkimages[n] = tkimage + button.config(image=tkimage, text='') + else: + # button has a simple text label: either standard text + # or an icon included in the "forkawesome" font (unicode char >= \uf000) + if label[0] >= '\uf000': + font = ("forkawesome", int(1.0 * self.font_size)) + else: + font = (zynthian_gui_config.font_family, int(0.9 * self.font_size)) + button.config(font=font, text=label.replace('/', "\n")) + button.grid_propagate(False) + button.grid(row=row, column=column, sticky='nswe', padx=padx, pady=pady) + button.bind('', lambda e: self.cb_button_push(zynswitch, e)) + button.bind('', lambda e: self.cb_button_release(zynswitch, e)) + return button + + def cb_button_push(self, n, event): + """ + Call ZYNSWITCH Push CUIA on button push + """ + zynthian_gui_config.zyngui.cuia_queue.put_nowait(f"zynswitch {n},P") + + def cb_button_release(self, n, event): + """ + Call ZYNSWITCH Release CUIA on button release + """ + zynthian_gui_config.zyngui.cuia_queue.put_nowait(f"zynswitch {n},R") + + def set_button_color(self, led_num, color, mode): + """ + Change color of a button according to the wsleds signal + + Parameters + ---------- + + led_num : int + Number of the RGB wsled corresponding to the button + color : int + Color requested by the wsled system + mode : str + A wanna-be abstraction (string name) of the mode/state - currently + just derived from the requested color by the `wsleds_v5touch` "fake NeoPixel" emulator + """ + # get the button number associated with the wsled number + n = LED2BUTTON[led_num] + # don't bother with update if nothing has really changed (redrawing images causes visible blinking!) + if self.btnstate[n] == (mode or color): + return + self.btnstate[n] = mode or color + # in case the color is still the original wsled integer number, convert it + label = self.btndefs[n][0]['default'] + if label.startswith('_'): + # image buttons must be recomposed to change the foreground color + image = self.images[n] + mask = image.convert("LA") + bgimage = Image.new("RGBA", image.size, color) + fgimage = Image.new("RGBA", image.size, (0, 0, 0, 0)) + composed = Image.composite(bgimage, fgimage, mask) + tkimage = ImageTk.PhotoImage(composed) + self.tkimages[n] = tkimage + self.buttons[n].config(image=tkimage) + else: + # plain text labels may just change the color and possibly also its label if a special label + # is associated with the requested mode (<=color) in the button definition + self.refresh_button_label(n, mode) + self.buttons[n].config(fg=color, activeforeground=color) + + def refresh_button_label(self, n, mode): + text = self.btndefs[n][0].get(mode, self.btndefs[n][0]['default']).replace('/', "\n") + self.buttons[n].config(text=text) + + def show(self): + if not self.shown: + self.side_frame.grid_propagate(False) + self.side_frame.grid(row=0, column=self.side_frame_col, rowspan=2, sticky="nws") + self.bottom_frame.grid_propagate(False) + self.bottom_frame.grid(row=1, column=self.bottom_frame_col, sticky="wse") + self.shown = True + + def hide(self): + if self.shown: + self.side_frame.grid_remove() + self.bottom_frame.grid_remove() + self.shown = False + + def apply_user_config(self): + for n in range(0, 20): + default = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_LABEL_{:02d}_DEFAULT'.format(n+1), None) + alt = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_LABEL_{:02d}_ALT'.format(n+1), None) + active = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_LABEL_{:02d}_ACTIVE'.format(n+1), None) + active2 = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_LABEL_{:02d}_ACTIVE2'.format(n+1), None) + if default: + self.btndefs[n][0]['default'] = default + if alt: + self.btndefs[n][0]['alt'] = alt + if active: + self.btndefs[n][0]['active'] = active + if active2: + self.btndefs[n][0]['active2'] = active2 + + def _fkey2btn(self, n): + mode = 'default' + if n >= 4: + mode = 'alt' + n -= 4 + return FKEY2SWITCH[n]-4, mode + + def set_fkey_label(self, n, label): + btn, mode = self._fkey2btn(n) + self.btndefs[btn][0][mode] = label + self.refresh_button_label(btn, label) + + def get_fkey_label(self, n): + btn, mode = self._fkey2btn(n) + return self.btndefs[btn][0][mode] + diff --git a/zyngui/zynthian_gui_touchscreen_calibration.py b/zyngui/zynthian_gui_touchscreen_calibration.py index fb7c5d43c..f0f095026 100644 --- a/zyngui/zynthian_gui_touchscreen_calibration.py +++ b/zyngui/zynthian_gui_touchscreen_calibration.py @@ -428,6 +428,8 @@ def hide(self): self.setCalibration(self.device_id, self.ctm) self.main_frame.grid_forget() self.shown = False + if zynthian_gui_config.touch_keypad: + zynthian_gui_config.touch_keypad.show() # Build display def build_view(self): @@ -448,6 +450,8 @@ def build_view(self): # Show display def show(self): + if zynthian_gui_config.touch_keypad: + zynthian_gui_config.touch_keypad.hide() self.main_frame.grid() self.onTimer() self.detect_thread = Thread( diff --git a/zyngui/zynthian_widget_sooperlooper.py b/zyngui/zynthian_widget_sooperlooper.py index b474afa26..ee74645d0 100644 --- a/zyngui/zynthian_widget_sooperlooper.py +++ b/zyngui/zynthian_widget_sooperlooper.py @@ -63,7 +63,7 @@ def __init__(self, parent): self.tri_size = int(0.5 * zynthian_gui_config.font_size) # int(0.70 * self.font_size_sl) - txt_y = zynthian_gui_config.display_height // 22 + txt_y = zynthian_gui_config.screen_height // 22 self.txt_x = 4 self.pos_canvas = [] diff --git a/zyngui/zynthian_wsleds_v5touch.py b/zyngui/zynthian_wsleds_v5touch.py new file mode 100644 index 000000000..0ea8118ae --- /dev/null +++ b/zyngui/zynthian_wsleds_v5touch.py @@ -0,0 +1,128 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ****************************************************************************** +# ZYNTHIAN PROJECT: Zynthian GUI +# +# Zynthian WSLeds Class for LED emulation on touchscreen keypad V5 +# +# Copyright (C) 2024 Pavel Vondřička +# +# ****************************************************************************** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# For a full copy of the GNU General Public License see the LICENSE.txt file. +# +# ****************************************************************************** + +import os + +# Zynthian specific modules +from zyngui import zynthian_gui_config +from zyngui.zynthian_wsleds_v5 import zynthian_wsleds_v5 + +# --------------------------------------------------------------------------- +# Fake NeoPixel emulation for onscreen touch keypad "buttons" +# --------------------------------------------------------------------------- + +class touchkeypad_button_colors: + """ + Fake NeoPixel emulation to change colors of onscreen touch keypad + """ + + def __init__(self, wsleds): + self.wsleds = wsleds + self.zyngui = wsleds.zyngui + # A wanna-be abstraction: derive a named "mode" from the requested colors + self.mode_map = {} + self.mode_map[wsleds.wscolor_default] = 'default' + self.mode_map[wsleds.wscolor_alt] = 'alt' + self.mode_map[wsleds.wscolor_active] = 'active' + self.mode_map[wsleds.wscolor_active2] = 'active2' + + def __setitem__(self, index, color): + mode = self.mode_map.get(color, None) + # request color change on the onscreen touchkeypad + if isinstance(color, int): + color = f"#{color:06x}" # color conversion to hex cod + # tkinter is not able to set RGBA/alpha color, + # so we need to blend the foreground color with the background color + if zynthian_gui_config.zyngui: + fgcolor = self.hex_to_rgb(color) + bgcolor = self.hex_to_rgb(self.wsleds.wscolor_off) + blended = self.ablend(1-self.wsleds.brightness, fgcolor, bgcolor) + color = self.rgb_to_hex(blended) + zynthian_gui_config.touch_keypad.set_button_color(index, color, mode) + + def show(self): + # nothing to do here + pass + + def ablend(self, a, fg, bg): + """ + Blend foreground and background color to imitate alpha transparency + """ + return (int((1-a)*fg[0]+a*bg[0]), + int((1-a)*fg[1]+a*bg[1]), + int((1-a)*fg[2]+a*bg[2])) + + def hex_to_rgb(self, hexstr): + rgb = [] + hex = hexstr[1:] + for i in (0, 2, 4): + decimal = int(hex[i:i+2], 16) + rgb.append(decimal) + return tuple(rgb) + + def rgb_to_hex(self, rgb): + r, g, b = rgb + return '#{:02x}{:02x}{:02x}'.format(r, g, b) + +# --------------------------------------------------------------------------- +# Zynthian WSLeds class for LED emulation on touchscreen keypad V5 +# --------------------------------------------------------------------------- + +class zynthian_wsleds_v5touch(zynthian_wsleds_v5): + """ + Emulation of wsleds for onscreen touch keypad V5 + """ + + def start(self): + self.wsleds = touchkeypad_button_colors(self) + self.light_on_all() + + def setup_colors(self): + # Predefined colors + self.wscolor_off = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_OFF', zynthian_gui_config.color_bg) + self.wscolor_white = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_WHITE', "#FCFCFC") + self.wscolor_red = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_RED', "#FE2C2F") + self.wscolor_green = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_GREEN', "#00FA00") + self.wscolor_yellow = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_YELLOW', "#F0EA00") + self.wscolor_orange = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_ORANGE', "#FF6A00") + self.wscolor_blue = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_BLUE', "#1070FE") + self.wscolor_blue_light = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_LIGHTBLUE', "#05FDFF") + self.wscolor_purple = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_PURPLE', "#D000E0") + self.wscolor_default = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_DEFAULT', self.wscolor_blue) + self.wscolor_alt = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_ALT', self.wscolor_purple) + self.wscolor_active = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_ACTIVE', self.wscolor_green) + self.wscolor_active2 = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_ACTIVE2', self.wscolor_orange) + self.wscolor_admin = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_ADMIN', self.wscolor_red) + self.wscolor_low = os.environ.get('ZYNTHIAN_TOUCH_KEYPAD_COLOR_LOW', "#D9EB37") + # Color Codes + self.wscolors_dict = { + str(self.wscolor_off): "0", + str(self.wscolor_blue): "B", + str(self.wscolor_green): "G", + str(self.wscolor_red): "R", + str(self.wscolor_orange): "O", + str(self.wscolor_yellow): "Y", + str(self.wscolor_purple): "P" +} diff --git a/zynthian_main.py b/zynthian_main.py index c39156ae6..25fe86109 100755 --- a/zynthian_main.py +++ b/zynthian_main.py @@ -28,6 +28,7 @@ import ctypes import logging from tkinter import EventType +from time import sleep # Zynthian specific modules from zyngui import zynthian_gui_config