The audio mixer is the zynthian UI's "home" view. You can always access
- the mixer by pushing the MIX button.
-
It's composed of vertical strips associated with chains. Initially it will be empty, showing the main strip alone.
- For each chain you create, a new strip is added to the mixer. There can be more mixer strips than can be displayed.
- They will scroll left / right if you select a strip that is off screen.
-
Each strip is an interactive widget that can be manipulated in several ways and it shows status info about the
- associated chain.
-
-
From top to bottom:
+
+
Audio Mixer
+
The audio mixer is the zynthian UI's "home" view. You can always access
+ the mixer by pushing the MIX button.
+
It's composed of vertical strips associated with chains. Initially it will be empty, showing the main strip alone.
+ For each chain you create, a new strip is added to the mixer. There can be more mixer strips than can be displayed.
+ They will scroll left / right if you select a strip that is off screen.
+
Each strip is an interactive widget that can be manipulated in several ways and it shows status info about the
+ associated chain.
+
From top to bottom:
+
+
Solo: Solo flag. Push to toggle. This is a non-exclusive solo mode. When a chain has solo enabled, all
other chains without solo enabled will be muted, allowing auditioning of one or more chains. The main solo
@@ -58,6 +59,7 @@
Audio Mixer
Legend: It shows an icon for the chain's type and a number for the assigned MIDI channel. Tap it to access
the chain's control view.
+
diff --git a/help/icons/white_icon_action_knob_rotate.png b/help/icons/white_icon_action_knob_rotate.png
index a4d805dcc..fbc64a375 100644
Binary files a/help/icons/white_icon_action_knob_rotate.png and b/help/icons/white_icon_action_knob_rotate.png differ
diff --git a/help/img/mixer_strip_legend_bg_black.png b/help/img/mixer_strip_legend_bg_black.png
index 834f6c2b0..c8d52ee4e 100644
Binary files a/help/img/mixer_strip_legend_bg_black.png and b/help/img/mixer_strip_legend_bg_black.png differ
diff --git a/help/style.css b/help/style.css
index e110f70d9..363a627f5 100644
--- a/help/style.css
+++ b/help/style.css
@@ -27,6 +27,10 @@ div.clear {
clear: both;
}
+div.right_container {
+ width: 65%;
+}
+
div.knobs_action_container,
div.left_float_container {
clear: both;
@@ -34,7 +38,6 @@ div.left_float_container {
width: 30%;
}
div.knob_action {
- margin-left: 1em;
margin-bottom: 2px;
padding-right: 4px;
font-size: 100%;
diff --git a/icons/audio.png b/icons/audio.png
new file mode 100644
index 000000000..87d8b30ed
Binary files /dev/null and b/icons/audio.png differ
diff --git a/icons/audio_generator.png b/icons/audio_generator.png
new file mode 100644
index 000000000..10d9a401d
Binary files /dev/null and b/icons/audio_generator.png differ
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/audio_recorder.png b/icons/audio_recorder.png
new file mode 100644
index 000000000..932fd490e
Binary files /dev/null and b/icons/audio_recorder.png differ
diff --git a/icons/back.png b/icons/back.png
index d161842bd..3ec372600 100644
Binary files a/icons/back.png and b/icons/back.png differ
diff --git a/icons/bluetooth.png b/icons/bluetooth.png
new file mode 100644
index 000000000..a753cba10
Binary files /dev/null and b/icons/bluetooth.png differ
diff --git a/icons/delete.png b/icons/delete.png
new file mode 100644
index 000000000..28b01de6f
Binary files /dev/null and b/icons/delete.png differ
diff --git a/icons/delete_all.png b/icons/delete_all.png
new file mode 100644
index 000000000..3d50f5c5e
Binary files /dev/null and b/icons/delete_all.png differ
diff --git a/icons/delete_chains.png b/icons/delete_chains.png
new file mode 100644
index 000000000..56ad0c3af
Binary files /dev/null and b/icons/delete_chains.png differ
diff --git a/icons/delete_sequences.png b/icons/delete_sequences.png
new file mode 100644
index 000000000..c08833311
Binary files /dev/null and b/icons/delete_sequences.png differ
diff --git a/icons/effects_loop.png b/icons/effects_loop.png
new file mode 100644
index 000000000..d7fba7dcb
Binary files /dev/null and b/icons/effects_loop.png 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/headphones.png b/icons/headphones.png
new file mode 100644
index 000000000..6760a348b
Binary files /dev/null and b/icons/headphones.png differ
diff --git a/icons/meter.png b/icons/meter.png
new file mode 100644
index 000000000..052242afa
Binary files /dev/null and b/icons/meter.png differ
diff --git a/icons/meters.png b/icons/meters.png
new file mode 100644
index 000000000..b63a3ce0c
Binary files /dev/null and b/icons/meters.png differ
diff --git a/icons/metronome.png b/icons/metronome.png
new file mode 100644
index 000000000..4803c541a
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_audio.png b/icons/midi_audio.png
new file mode 100644
index 000000000..82ef8818d
Binary files /dev/null and b/icons/midi_audio.png differ
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_instrument.png b/icons/midi_instrument.png
new file mode 100644
index 000000000..9b539f8eb
Binary files /dev/null and b/icons/midi_instrument.png differ
diff --git a/icons/midi_logo.png b/icons/midi_logo.png
new file mode 100644
index 000000000..7e9b0ba23
Binary files /dev/null and b/icons/midi_logo.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/midi_recorder.png b/icons/midi_recorder.png
new file mode 100644
index 000000000..0a6d6474c
Binary files /dev/null and b/icons/midi_recorder.png differ
diff --git a/icons/mixer.png b/icons/mixer.png
new file mode 100644
index 000000000..adf90c682
Binary files /dev/null and b/icons/mixer.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/panic.png b/icons/panic.png
new file mode 100644
index 000000000..b8e2b35ce
Binary files /dev/null and b/icons/panic.png differ
diff --git a/icons/sequencer.png b/icons/sequencer.png
new file mode 100644
index 000000000..36f3bb5e5
Binary files /dev/null and b/icons/sequencer.png differ
diff --git a/icons/settings.png b/icons/settings.png
new file mode 100644
index 000000000..c98096cb4
Binary files /dev/null and b/icons/settings.png differ
diff --git a/icons/snapshot.png b/icons/snapshot.png
new file mode 100644
index 000000000..4c722e286
Binary files /dev/null and b/icons/snapshot.png differ
diff --git a/icons/special_chain.png b/icons/special_chain.png
new file mode 100644
index 000000000..9419026bf
Binary files /dev/null and b/icons/special_chain.png differ
diff --git a/icons/stopped.png b/icons/stopped.png
index 48b10475b..fe62ede4f 100644
Binary files a/icons/stopped.png and b/icons/stopped.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/icons/zynpad_mode_loop.png b/icons/zynpad_mode_loop.png
index 9da3a5968..12503538f 100644
Binary files a/icons/zynpad_mode_loop.png and b/icons/zynpad_mode_loop.png differ
diff --git a/icons/zynpad_mode_loopall.png b/icons/zynpad_mode_loopall.png
index 0ef68eb0a..ffa4a0e99 100644
Binary files a/icons/zynpad_mode_loopall.png and b/icons/zynpad_mode_loopall.png differ
diff --git a/icons/zynpad_mode_loopsync.png b/icons/zynpad_mode_loopsync.png
index 540309326..ef1e5d598 100644
Binary files a/icons/zynpad_mode_loopsync.png and b/icons/zynpad_mode_loopsync.png differ
diff --git a/icons/zynpad_mode_oneshot.png b/icons/zynpad_mode_oneshot.png
index c73739cca..3c695be80 100644
Binary files a/icons/zynpad_mode_oneshot.png and b/icons/zynpad_mode_oneshot.png differ
diff --git a/icons/zynpad_mode_oneshotall.png b/icons/zynpad_mode_oneshotall.png
index a74e9f08e..e5e887965 100644
Binary files a/icons/zynpad_mode_oneshotall.png and b/icons/zynpad_mode_oneshotall.png differ
diff --git a/icons/zynpad_mode_oneshotsync.png b/icons/zynpad_mode_oneshotsync.png
index 5ea5515ab..911a8df8f 100644
Binary files a/icons/zynpad_mode_oneshotsync.png and b/icons/zynpad_mode_oneshotsync.png differ
diff --git a/zynautoconnect/zynthian_autoconnect.py b/zynautoconnect/zynthian_autoconnect.py
index bfd61ffa8..b01f5c22b 100755
--- a/zynautoconnect/zynthian_autoconnect.py
+++ b/zynautoconnect/zynthian_autoconnect.py
@@ -25,16 +25,19 @@
import os
import re
import usb
+import json
import jack
+import psutil
+import pexpect
import logging
-import json
+import alsaaudio
from time import sleep
from threading import Thread, Lock
# Zynthian specific modules
+import zynconf
from zyncoder.zyncore import lib_zyncore
from zyngui import zynthian_gui_config
-import zynconf
# -------------------------------------------------------------------------------
# Configure logging
@@ -65,7 +68,6 @@ def set_alias(self, alias):
def unset_alias(self, alias):
pass
-
# -------------------------------------------------------------------------------
# Define some Constants and Global Variables
# -------------------------------------------------------------------------------
@@ -89,10 +91,10 @@ def unset_alias(self, alias):
# List of hardware MIDI destination ports (including network, aubionotes, etc.)
hw_midi_dst_ports = []
hw_audio_dst_ports = [] # List of physical audio output ports
-# Map of all audio target port names to use as sidechain inputs, indexed by jack client regex
-sidechain_map = {}
-# List of currently active audio destination port names not to autoroute, e.g. sidechain inputs
-sidechain_ports = []
+sidechain_map = {} # Map of all audio target port names to use as sidechain inputs, indexed by jack client regex
+sidechain_ports = [] # List of currently active audio destination port names not to autoroute, e.g. sidechain inputs
+alsa_audio_srcs = {} # Map of alsa_in processes, indexed by alsa device name
+alsa_audio_dests = {} # Map of alsa_out processes, indexed by alsa device name
# These variables are initialized in the init() function. These are "example values".
max_num_devs = 16 # Max number of MIDI devices
@@ -114,6 +116,19 @@ def unset_alias(self, alias):
# Map of user friendly names indexed by device uid (alias[0])
midi_port_names = {}
+# Get the main jack audio device
+jack_audio_device = ""
+for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
+ try:
+ if 'jackd' in proc.info['name']:
+ cmdline = proc.info['cmdline']
+ for param in cmdline:
+ if param.startswith("hw:"):
+ jack_audio_device = param[3:]
+ break
+ except:
+ pass
+
# ------------------------------------------------------------------------------
# MIDI port helper functions
@@ -145,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)
@@ -322,6 +337,20 @@ def reset_midi_in_dev_all():
# ------------------------------------------------------------------------------
# Audio port helpers
+def update_system_audio_aliases():
+ for dir in (False, True):
+ ports = jclient.get_ports("system:", is_audio=True, is_input=dir, is_output=not dir)
+ for i, port in enumerate(ports):
+ if port.aliases:
+ parts = port.aliases[0].split(":")
+ if len(parts) != 4 or parts[0] != "alsa_pcm" and parts[1] != "hw":
+ continue
+ alias = f"{parts[2]} {i + 1}"
+ for a in port.aliases:
+ port.unset_alias(a)
+ port.set_alias(alias)
+
+
def add_sidechain_ports(jackname):
"""Add ports that should be treated as sidechain inputs
@@ -926,7 +955,173 @@ def audio_autoconnect():
def get_hw_audio_dst_ports():
- return 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
+
+ dirty = False
+ if zynthian_gui_config.hotplug_audio_enabled:
+ # Add new devices
+ for device in get_alsa_hotplug_audio_devices(False):
+ if device not in zynthian_gui_config.disabled_audio_in:
+ dirty |= start_alsa_in(device)
+ for device in get_alsa_hotplug_audio_devices(True):
+ if device not in zynthian_gui_config.disabled_audio_out:
+ dirty |= start_alsa_out(device)
+
+ # Remove disconnected devices
+ for device in list(alsa_audio_srcs):
+ try:
+ while True:
+ proc = alsa_audio_srcs[device]
+ line = proc.readline()
+ if line.startswith("err"):
+ proc.terminate()
+ alsa_audio_srcs.pop(device)
+ dirty = True
+ break
+ elif not line:
+ break
+ except:
+ continue
+ for device in list(alsa_audio_dests):
+ try:
+ while True:
+ proc = alsa_audio_dests[device]
+ line = proc.readline()
+ if line.startswith("err"):
+ proc.terminate()
+ alsa_audio_dests.pop(device)
+ dirty = True
+ break
+ elif not line:
+ break
+ except:
+ continue
+ if dirty:
+ # Rebuild chain audio routes
+ try:
+ sleep(0.5) # Have to wait for jack to finish registering ports
+ for chain in chain_manager.chains.values():
+ chain.rebuild_audio_graph()
+ except Exception as e:
+ logging.error(e)
+
+ 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):
+ if device in zynthian_gui_config.disabled_audio_in:
+ zynthian_gui_config.disabled_audio_in.remove(device)
+ else:
+ stop_alsa_in(device)
+ if device not in zynthian_gui_config.disabled_audio_in:
+ 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):
+ if device in zynthian_gui_config.disabled_audio_out:
+ zynthian_gui_config.disabled_audio_out.remove(device)
+ else:
+ stop_alsa_out(device)
+ if device not in zynthian_gui_config.disabled_audio_out:
+ 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):
+ if card == jack_audio_device:
+ continue
+ if card.startswith("hw:"):
+ device = card[8:card.find(",")]
+ if device != jack_audio_device:
+ devices.append(device)
+ return devices
+
+
+def start_alsa_in(device):
+ global alsa_audio_srcs
+ if device in alsa_audio_srcs:
+ return False
+ proc = pexpect.spawn(f"alsa_in -d hw:{device} -j zynain_{device}", encoding="utf-8", timeout=0.1)
+ if proc.exitstatus:
+ return False
+ alsa_audio_srcs[device] = proc
+ for i in range(10):
+ ports = jclient.get_ports(f"zynain_{device}")
+ if ports:
+ for i, port in enumerate(ports):
+ port.set_alias(f"{device} {i + 1}")
+ return True
+ sleep(0.1)
+ 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:
+ return False
+ alsa_audio_srcs[device].terminate()
+ alsa_audio_srcs.pop(device)
+ return True
+
+
+def start_alsa_out(device):
+ global alsa_audio_dests
+ if device in alsa_audio_dests:
+ return False
+ proc = pexpect.spawn(f"alsa_out -d hw:{device} -j zynaout_{device}", encoding="utf-8", timeout=0.1)
+ if proc.exitstatus:
+ return False
+ alsa_audio_dests[device] = proc
+ for i in range(10):
+ ports = jclient.get_ports(f"zynaout_{device}")
+ if ports:
+ for i, port in enumerate(ports):
+ port.set_alias(f"{device} {i + 1}")
+ return True
+ sleep(0.1)
+ 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:
+ return False
+ alsa_audio_dests[device].terminate()
+ 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
@@ -935,10 +1130,9 @@ def audio_connect_ffmpeg(timeout=2.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)
@@ -948,7 +1142,7 @@ def audio_connect_ffmpeg(timeout=2.0):
def get_audio_capture_ports():
"""Get list of hardware audio inputs"""
- return jclient.get_ports("system", is_output=True, is_audio=True, is_physical=True)
+ return jclient.get_ports("system", is_output=True, is_audio=True, is_physical=True) + jclient.get_ports("zynain", is_output=True, is_audio=True)
def build_midi_port_name(port):
@@ -1107,6 +1301,9 @@ def auto_connect_thread():
# Check if requested to run midi connect (slow)
if deferred_midi_connect:
do_midi = True
+ # Check if dynamic (hot-plug) audio changed
+ if update_hw_audio_ports():
+ do_audio = True
# Check if requested to run audio connect (slow)
if deferred_audio_connect:
do_audio = True
@@ -1175,6 +1372,7 @@ def init():
devices_out_name.append(None)
update_midi_in_dev_mode_all()
+ update_system_audio_aliases()
def start(sm):
@@ -1203,9 +1401,7 @@ def start(sm):
init()
# Get System Playback Ports
- hw_audio_dst_ports = jclient.get_ports(
- "system:playback", is_input=True, is_audio=True, is_physical=True)
-
+ hw_audio_dst_ports = get_hw_audio_dst_ports()
try:
with open(f"{zynconf.config_dir}/sidechain.json", "r") as file:
sidechain_map = json.load(file)
@@ -1225,7 +1421,7 @@ def start(sm):
def stop():
"""Reset state and stop autoconnect thread"""
- global exit_flag, jclient, thread, lock
+ global exit_flag, jclient, thread, lock, hw_audio_dst_ports
exit_flag = True
if thread:
thread.join()
@@ -1237,6 +1433,8 @@ def stop():
hw_audio_dst_ports = []
+ stop_all_alsa_in_out()
+
if jclient:
jclient.deactivate()
jclient = None
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 e319653cb..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 = ""
@@ -374,13 +374,16 @@ def get_input_pairs(self):
if self.chain_id == 0:
return self.audio_in.copy()
sources = []
+ input_ports = zynautoconnect.get_audio_capture_ports()
for i in range(0, len(self.audio_in), 2):
a = self.audio_in[i]
+ if a > len(input_ports):
+ continue
if i < len(self.audio_in) - 1:
b = self.audio_in[i + 1]
- sources.append(f"system:capture_({a}|{b})$")
+ sources.append(f"^{input_ports[a-1].name}$|^{input_ports[b-1].name}$")
else:
- sources.append(f"system:capture_({a})$")
+ sources.append(f"^{input_ports[a-1].name}$")
return sources
def rebuild_midi_graph(self):
diff --git a/zyngine/zynthian_chain_manager.py b/zyngine/zynthian_chain_manager.py
index a3691103d..f7980b9b1 100644
--- a/zyngine/zynthian_chain_manager.py
+++ b/zyngine/zynthian_chain_manager.py
@@ -1276,20 +1276,26 @@ def remove_midi_learn(self, proc, symbol):
logging.debug(f"(symbol={symbol} => zctrl={zctrl.symbol})")
for key in list(self.absolute_midi_cc_binding):
zctrls = self.absolute_midi_cc_binding[key]
- if zctrl in zctrls:
+ try:
zctrls.remove(zctrl)
+ except:
+ pass
if not zctrls:
self.absolute_midi_cc_binding.pop(key)
for key in list(self.chan_midi_cc_binding):
zctrls = self.chan_midi_cc_binding[key]
- if zctrl in zctrls:
+ try:
zctrls.remove(zctrl)
+ except:
+ pass
if not zctrls:
self.chan_midi_cc_binding.pop(key)
for key in list(self.chain_midi_cc_binding):
zctrls = self.chain_midi_cc_binding[key]
- if zctrl in zctrls:
+ try:
zctrls.remove(zctrl)
+ except:
+ pass
if not zctrls:
self.chain_midi_cc_binding.pop(key)
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..1f6d197f0 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
@@ -65,6 +64,7 @@ def __init__(self, name=None, command=None, prompt=None, cwd=None):
self.proc = None
self.proc_timeout = 30
self.proc_start_sleep = None
+ self.proc_exit = False
self.command = command
self.command_env = os.environ.copy()
self.command_prompt = prompt
@@ -81,6 +81,8 @@ def start(self):
try:
logging.debug("Command: {}".format(self.command))
+ self.proc_exit = False
+
# Turns out that environment's PWD is not set automatically
# when cwd is specified for pexpect.spawn(), so do it here.
if self.command_cwd:
@@ -89,8 +91,8 @@ def start(self):
# Setting cwd is because we've set PWD above. Some engines doesn't
# care about the process's cwd, but it is more consistent to set
# cwd when PWD has been set.
- self.proc = pexpect.spawn(
- self.command, timeout=self.proc_timeout, env=self.command_env, cwd=self.command_cwd)
+ self.proc = pexpect.spawn(self.command, timeout=self.proc_timeout,
+ env=self.command_env, cwd=self.command_cwd)
self.proc.delaybeforesend = 0
output = self.proc_get_output()
@@ -107,6 +109,7 @@ def stop(self):
if self.proc:
try:
logging.info("Stopping Engine " + self.name)
+ self.proc_exit = True
self.proc.terminate(True)
self.proc = None
except Exception as err:
@@ -130,8 +133,7 @@ def proc_cmd(self, cmd):
# logging.debug("proc output:\n{}".format(out))
except Exception as err:
out = ""
- logging.error(
- "Can't exec engine command: {} => {}".format(cmd, err))
+ logging.error("Can't exec engine command: {} => {}".format(cmd, err))
return out
@@ -572,12 +574,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 +617,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_jalv.py b/zyngine/zynthian_engine_jalv.py
index f070b5ffc..3bca19454 100644
--- a/zyngine/zynthian_engine_jalv.py
+++ b/zyngine/zynthian_engine_jalv.py
@@ -28,7 +28,10 @@
import copy
import shutil
import logging
-from subprocess import check_output, STDOUT
+from time import sleep
+from datetime import datetime
+from threading import Thread
+from subprocess import Popen, check_output, STDOUT, PIPE
from . import zynthian_lv2
from . import zynthian_engine
@@ -152,6 +155,11 @@ class zynthian_engine_jalv(zynthian_engine):
def __init__(self, eng_code, state_manager, dryrun=False, jackname=None):
super().__init__(state_manager)
+ self.proc_poll_thread = None
+
+ self.save_bank = None
+ self.save_preset_uri = None
+
if state_manager:
self.eng_info = self.state_manager.chain_manager.engine_info[eng_code]
else:
@@ -189,18 +197,21 @@ def __init__(self, eng_code, state_manager, dryrun=False, jackname=None):
jalv_bin = "jalv.qt5"
elif self.native_gui == "Qt4UI":
# jalv_bin = "jalv.qt4"
- jalv_bin = "jalv.gtk"
+ jalv_bin = "jalv.gtk3"
else: # elif self.native_gui=="X11UI":
- jalv_bin = "jalv.gtk"
- self.command = f"{jalv_bin} --jack-name {self.jackname} {self.plugin_url}"
+ jalv_bin = "jalv.gtk3"
+ self.command = [jalv_bin, "--jack-name", self.jackname, self.plugin_url]
else:
- self.command = f"jalv -n {self.jackname} {self.plugin_url}"
+ self.command = ["jalv", "-n", self.jackname, self.plugin_url]
# Some plugins need a X11 display for running headless (QT5, QT6),
# but some others can't run headless if there is a valid DISPLAY defined
if not self.plugin_name.endswith("v1"):
self.command_env['DISPLAY'] = "X"
- self.command_prompt = "\n> "
+ # Use jalv_asyncli (development version) =>
+ self.command[0] = "/zynthian/zynthian-sw/jalv_asyncli/build/" + self.command[0]
+
+ self.command_prompt = ">"
# Jalv which uses PWD as the root for presets
self.command_cwd = zynthian_engine.my_data_dir + "/presets/lv2"
@@ -271,6 +282,145 @@ def __init__(self, eng_code, state_manager, dryrun=False, jackname=None):
def load_preset_info(self):
self.preset_info = zynthian_lv2.get_plugin_presets_cache(self.plugin_name)
+ # ---------------------------------------------------------------------------
+ # Subprocess Management & IPC
+ # ---------------------------------------------------------------------------
+
+ def start(self):
+ if not self.proc:
+ logging.info("Starting Engine {}".format(self.name))
+ try:
+ logging.debug("Command: {}".format(self.command))
+ self.proc_exit = False
+ # Turns out that environment's PWD is not set automatically
+ # when cwd is specified for pexpect.spawn(), so do it here.
+ if self.command_cwd:
+ self.command_env['PWD'] = self.command_cwd
+ # Setting cwd is because we've set PWD above. Some engines doesn't
+ # care about the process's cwd, but it is more consistent to set
+ # cwd when PWD has been set.
+ self.proc = Popen(self.command, env=self.command_env, cwd=self.command_cwd, shell=False,
+ text=True, bufsize=1, stdout=PIPE, stderr=STDOUT, stdin=PIPE)
+ output = self.proc_get_output()
+ self.start_proc_poll_thread()
+ return output
+
+ except Exception as err:
+ logging.error(
+ "Can't start engine {} => {}".format(self.name, err))
+
+ def stop(self):
+ if self.proc:
+ try:
+ logging.info("Stopping Engine " + self.name)
+ self.proc_cmd("")
+ self.proc_exit = True
+ self.proc.terminate()
+ try:
+ self.proc.wait(timeout=5)
+ except TimeoutExpired:
+ self.proc.kill()
+ self.proc = None
+ except Exception as err:
+ logging.error(f"Can't stop engine {self.name} => {err}")
+
+ def proc_cmd(self, cmd):
+ #a = datetime.now()
+ self.proc.stdin.writelines([cmd + "\n"])
+ #tdus = (datetime.now() - a).microseconds
+ #logging.debug(f"COMMAND ({tdus}): {cmd}")
+
+ def proc_get_output(self):
+ res = ""
+ while not self.proc_exit:
+ line = self.proc.stdout.readline().strip()
+ if line == self.command_prompt:
+ break
+ elif line:
+ res += line
+ return res
+
+ def proc_poll_line(self):
+ return self.proc.stdout.readline()
+
+ def proc_poll_thread_task(self):
+ while not self.proc_exit:
+ line = self.proc.stdout.readline().strip()
+ if line:
+ self.proc_poll_parse_line(line)
+
+ def proc_poll_parse_line(self, line):
+ #logging.debug(f"{self.jackname} PARSE => " + line)
+ match line[0:5]:
+ case "#CTR>":
+ self.proc_parse_ctrl_value(line[6:])
+ case "#MON>":
+ self.proc_parse_mon_value(line[6:])
+ case "#PRS>":
+ self.proc_parse_preset(line[6:])
+ case _:
+ if line == self.command_prompt:
+ logging.debug(f"PROMPT {self.jackname} >")
+ elif line:
+ logging.debug(f"LOG {self.jackname} > " + line)
+
+ def proc_parse_ctrl_value(self, line):
+ parts = line.split("=")
+ if len(parts) == 2:
+ try:
+ val = float(parts[1])
+ except Exception as e:
+ logging.warning(f"Wrong controller value when parsing jalv output => {line}")
+ return
+ symparts = parts[0].split("#", maxsplit=1)
+ #logging.debug(f"#CTR> {symparts[1]} ({symparts[0]}) = {val}")
+ try:
+ zctrl = self.lv2_zctrl_dict[symparts[1]]
+ zctrl.set_value(val, False)
+ if zctrl.graph_path is None:
+ try:
+ zctrl.graph_path = int(symparts[0])
+ #logging.debug(f"UPDATING JALV ZCTRL INDEX FOR '{symparts[1]}' => {zctrl.graph_path}")
+ except:
+ logging.warning(f"Cant't parse controller index from jalv output: {line}")
+ except Exception as e:
+ # TODO This shouldn't happen when property parameters are fully implemented
+ logging.warning(f"Unknown controller when parsing jalv output => {line}")
+ else:
+ logging.warning(f"Wrong controller format when parsing jalv output => {line}")
+
+ def proc_parse_mon_value(self, line):
+ parts = line.split("=")
+ if len(parts) == 2:
+ try:
+ val = float(parts[1])
+ except Exception as e:
+ logging.warning(f"Wrong monitor value when parsing jalv output => {line}")
+ return
+ symparts = parts[0].split("#", maxsplit=1)
+ #logging.debug(f"#MON> {symparts[1]} ({symparts[0]}) = {val}")
+ try:
+ self.lv2_monitors_dict[symparts[1]] = val
+ except Exception as e:
+ # TODO This shouldn't happen when property parameters are fully implemented
+ logging.warning(f"Unknown monitor when parsing jalv output => {line}")
+ else:
+ logging.warning(f"Wrong monitor format when parsing jalv output => {line}")
+
+ def proc_parse_preset(self, line):
+ parts = line.split(" ", maxsplit=1)
+ if len(parts) == 2 and parts[1][0] == "(" and parts[1][-1] == ")":
+ self.add_preset(parts[0], parts[1][1:-1])
+ self.save_preset_uri = parts[0]
+ else:
+ logging.warning(f"Wrong preset format when parsing jalv output => {line}")
+
+ def start_proc_poll_thread(self):
+ self.proc_poll_thread = Thread(target=self.proc_poll_thread_task, args=())
+ self.proc_poll_thread.name = f"proc_poll_{self.jackname}"
+ self.proc_poll_thread.daemon = True # thread dies with the program
+ self.proc_poll_thread.start()
+
# ---------------------------------------------------------------------------
# Processor Management
# ---------------------------------------------------------------------------
@@ -308,7 +458,11 @@ def set_midi_chan(self, processor):
def get_bank_list(self, processor=None):
bank_list = []
for bank_label, info in self.preset_info.items():
- bank_list.append((str(info['bank_url']), None, bank_label, None))
+ if info['bank_url'] is None:
+ bank_uri = ""
+ else:
+ bank_uri = str(info['bank_url'])
+ bank_list.append((bank_uri, None, bank_label, None))
if len(bank_list) == 0:
bank_list.append(("", None, "None", None))
return bank_list
@@ -317,7 +471,9 @@ def set_bank(self, processor, bank):
return True
def get_user_bank_urid(self, bank_name):
- return "file://{}/presets/lv2/{}.presets.lv2/{}".format(self.my_data_dir, zynthian_engine_jalv.sanitize_text(self.plugin_name), zynthian_engine_jalv.sanitize_text(bank_name))
+ return "file://{}/presets/lv2/{}.presets.lv2/{}".format(self.my_data_dir,
+ zynthian_engine_jalv.sanitize_text(self.plugin_name),
+ zynthian_engine_jalv.sanitize_text(bank_name))
def create_user_bank(self, bank_name):
bundle_path = "{}/presets/lv2/{}.presets.lv2".format(
@@ -395,24 +551,7 @@ def get_preset_list(self, bank):
def set_preset(self, processor, preset, preload=False):
if not preset[0]:
return
- output = self.proc_cmd("preset {}".format(preset[0]))
-
- # Parse new controller values
- for line in output.split("\n"):
- try:
- parts = line.split(" = ")
- if len(parts) == 2:
- try:
- val = float(parts[1])
- except Exception as e:
- logging.warning(f"Wrong parameter value when loading LV2 preset => {line}")
- continue
- self.lv2_zctrl_dict[parts[0]]._set_value(val)
- except Exception as e:
- # TODO This shouldn't happen when property parameters are fully implemented
- #logging.warning(f"Unknown parameter when loading LV2 preset => {line}")
- pass
-
+ self.proc_cmd(f"preset {preset[0]}")
return True
def cmp_presets(self, preset1, preset2):
@@ -425,7 +564,7 @@ def cmp_presets(self, preset1, preset2):
return False
def is_preset_user(self, preset):
- return isinstance(preset[0], str) and preset[0].startswith("file://{}/presets/lv2/".format(self.my_data_dir))
+ return isinstance(preset[0], str) and preset[0].startswith(f"file://{self.my_data_dir}/presets/lv2/")
def preset_exists(self, bank, preset_name):
# TODO: This would be more robust using URI but that is created dynamically by save_preset()
@@ -440,58 +579,68 @@ def preset_exists(self, bank, preset_name):
return False
def save_preset(self, bank, preset_name):
- # Save preset (jalv)
if not bank:
- bank = ["", None, "None", None]
- res = self.proc_cmd("save preset %s,%s" %
- (bank[0], preset_name)).split("\n")
-
- if res[-1].startswith("ERROR"):
- logging.error("Can't save preset => {}".format(res))
+ self.save_bank = ["", None, "None", None]
else:
- preset_uri = res[-1].strip()
- logging.info("Saved preset '{}' => {}".format(
- preset_name, preset_uri))
+ self.save_bank = bank
- # Add to cache
- try:
- # Add bank if needed
- if bank[2] not in self.preset_info:
- self.preset_info[bank[2]] = {
- 'bank_url': bank[0],
- 'presets': []
- }
- # Add preset
- if not self.preset_exists(bank, preset_name):
- self.preset_info[bank[2]]['presets'].append(
- {'label': preset_name, "url": preset_uri})
- # Save presets cache
- zynthian_lv2.save_plugin_presets_cache(
- self.plugin_name, self.preset_info)
- # Return preset uri
- return preset_uri
- except Exception as e:
- logging.error(e)
+ # Reset save_uri
+ self.save_preset_uri = None
+ # Send "save preset" command to jalv
+ if self.save_bank[0]:
+ cmd = f"save preset {self.save_bank[0]},{preset_name}"
+ else:
+ cmd = f"save preset {preset_name}"
+ #logging.debug(f"SAVE PRESET COMMAND => {cmd}")
+ self.proc_cmd(cmd)
+ # Wait for save preset feedback
+ i = 0
+ while i < 20 and self.save_preset_uri == None:
+ sleep(0.1)
+ i += 1
+ return self.save_preset_uri
+
+ def add_preset(self, preset_uri, preset_name):
+ logging.info(f"Add preset '{preset_name}' => {preset_uri}")
+ # Add to cache
+ try:
+ # Add bank if needed
+ if self.save_bank[2] not in self.preset_info:
+ self.preset_info[self.save_bank[2]] = {
+ 'bank_url': self.save_bank[0],
+ 'presets': []
+ }
+ # Add preset
+ if not self.preset_exists(self.save_bank, preset_name):
+ self.preset_info[self.save_bank[2]]['presets'].append(
+ {'label': preset_name,
+ "url": preset_uri})
+ # Save presets cache
+ zynthian_lv2.save_plugin_presets_cache(self.plugin_name, self.preset_info)
+ # If added, return true
+ return True
+ except Exception as e:
+ logging.error(e)
+ return False
def delete_preset(self, bank, preset):
if self.is_preset_user(preset):
try:
# Remove from LV2 ttl
zynthian_engine_jalv.lv2_remove_preset(preset[0])
-
# Remove from cache
for i, p in enumerate(self.preset_info[bank[2]]['presets']):
if p['url'] == preset[0]:
del self.preset_info[bank[2]]['presets'][i]
- zynthian_lv2.save_plugin_presets_cache(
- self.plugin_name, self.preset_info)
+ zynthian_lv2.save_plugin_presets_cache(self.plugin_name, self.preset_info)
break
-
except Exception as e:
logging.error(e)
try:
- return len(self.preset_info[bank[2]]['presets'])
+ n = len(self.preset_info[bank[2]]['presets'])
+ if n > 0:
+ return n
except Exception as e:
pass
zynthian_engine_jalv.lv2_remove_bank(bank)
@@ -501,18 +650,13 @@ def rename_preset(self, bank, preset, new_preset_name):
if self.is_preset_user(preset):
try:
# Update LV2 ttl
- zynthian_engine_jalv.lv2_rename_preset(
- preset[0], new_preset_name)
-
+ zynthian_engine_jalv.lv2_rename_preset(preset[0], new_preset_name)
# Update cache
for i, p in enumerate(self.preset_info[bank[2]]['presets']):
if p['url'] == preset[0]:
- self.preset_info[bank[2]
- ]['presets'][i]['label'] = new_preset_name
- zynthian_lv2.save_plugin_presets_cache(
- self.plugin_name, self.preset_info)
+ self.preset_info[bank[2]]['presets'][i]['label'] = new_preset_name
+ zynthian_lv2.save_plugin_presets_cache(self.plugin_name, self.preset_info)
break
-
except Exception as e:
logging.error(e)
@@ -539,7 +683,7 @@ def get_lv2_controllers_dict(self):
'name': info['name'],
'group_symbol': info['group_symbol'],
'group_name': info['group_name'],
- 'graph_path': info['index'],
+ #'graph_path': info['index'],
'value': info['value'],
'labels': labels,
'ticks': values,
@@ -564,7 +708,7 @@ def get_lv2_controllers_dict(self):
'name': info['name'],
'group_symbol': info['group_symbol'],
'group_name': info['group_name'],
- 'graph_path': info['index'],
+ #'graph_path': info['index'],
'value': val,
'labels': ['off', 'on'],
'ticks': [int(info['range']['min']), int(info['range']['max'])],
@@ -580,7 +724,7 @@ def get_lv2_controllers_dict(self):
'name': info['name'],
'group_symbol': info['group_symbol'],
'group_name': info['group_name'],
- 'graph_path': info['index'],
+ #'graph_path': info['index'],
'value': int(info['value']),
'value_default': int(info['range']['default']),
'value_min': int(info['range']['min']),
@@ -602,7 +746,7 @@ def get_lv2_controllers_dict(self):
'name': info['name'],
'group_symbol': info['group_symbol'],
'group_name': info['group_name'],
- 'graph_path': info['index'],
+ #'graph_path': info['index'],
'value': val,
'labels': ['off', 'on'],
'ticks': [info['range']['min'], info['range']['max']],
@@ -618,7 +762,7 @@ def get_lv2_controllers_dict(self):
'name': info['name'],
'group_symbol': info['group_symbol'],
'group_name': info['group_name'],
- 'graph_path': info['index'],
+ #'graph_path': info['index'],
'value': info['value'],
'value_default': float(info['range']['default']),
'value_min': float(info['range']['min']),
@@ -643,15 +787,8 @@ def get_lv2_controllers_dict(self):
return zctrls
def get_monitors_dict(self):
- self.lv2_monitors_dict = {}
- for line in self.proc_cmd("monitors").split("\n"):
- try:
- parts = line.split(" = ")
- if len(parts) == 2:
- self.lv2_monitors_dict[parts[0]] = float(parts[1])
- except Exception as e:
- logging.error(e)
-
+ self.proc_cmd("monitors")
+ # Return current monitor values => No wait for the asynchronous response!
return self.lv2_monitors_dict
def get_controllers_dict(self, processor):
@@ -665,7 +802,10 @@ def get_controllers_dict(self, processor):
def send_controller_value(self, zctrl):
try:
- self.proc_cmd("set %d %.6f" % (zctrl.graph_path, zctrl.value))
+ if zctrl.graph_path is not None:
+ self.proc_cmd("set %d %.6f" % (zctrl.graph_path, zctrl.value))
+ else:
+ self.proc_cmd("%s=%.6f" % (zctrl.symbol, zctrl.value))
except:
if zctrl.midi_cc:
lib_zyncore.zmop_send_ccontrol_change(zctrl.processor.chain.zmop_index,
@@ -778,8 +918,8 @@ def zynapi_install(cls, dpath, bank_path):
# Try to copy LV2 bundles ...
if os.path.isdir(dpath):
# Find manifest.ttl
- manifest_files = check_output(
- "find \"{}\" -type f -iname manifest.ttl".format(dpath), shell=True).decode("utf-8").split("\n")
+ manifest_files = check_output(f"find \"{dpath}\" -type f -iname manifest.ttl",
+ shell=True).decode("utf-8").split("\n")
# Copy LV2 bundle directories to destiny ...
count = 0
for f in manifest_files:
@@ -787,8 +927,7 @@ def zynapi_install(cls, dpath, bank_path):
head, bname = os.path.split(bpath)
if bname:
shutil.rmtree(zynthian_engine.my_data_dir +"/presets/lv2/" + bname, ignore_errors=True)
- shutil.move(
- bpath, zynthian_engine.my_data_dir + "/presets/lv2/")
+ shutil.move(bpath, zynthian_engine.my_data_dir + "/presets/lv2/")
count += 1
if count > 0:
cls.refresh_zynapi_instance()
@@ -873,10 +1012,8 @@ def lv2_rename_bank(bank_path, new_bank_name):
brre = re.compile(r"([\s]+rdfs:label[\s]+\").*(\" )")
for i, p in enumerate(parts):
if bmre1.search(p) and bmre2.search(p):
- new_bank_name = zynthian_engine_jalv.sanitize_text(
- new_bank_name)
- parts[i] = brre.sub(lambda m: m.group(
- 1)+new_bank_name+m.group(2), p)
+ new_bank_name = zynthian_engine_jalv.sanitize_text(new_bank_name)
+ parts[i] = brre.sub(lambda m: m.group(1)+new_bank_name+m.group(2), p)
zynthian_engine_jalv.ttl_write_parts(man_fpath, parts)
return
@@ -898,10 +1035,8 @@ def lv2_rename_preset(preset_path, new_preset_name):
renamed = False
for i, p in enumerate(man_parts):
if bmre1.search(p) and bmre2.search(p):
- new_preset_name = zynthian_engine_jalv.sanitize_text(
- new_preset_name)
- man_parts[i] = brre.sub(lambda m: m.group(
- 1) + new_preset_name + m.group(2), p)
+ new_preset_name = zynthian_engine_jalv.sanitize_text(new_preset_name)
+ man_parts[i] = brre.sub(lambda m: m.group(1) + new_preset_name + m.group(2), p)
zynthian_engine_jalv.ttl_write_parts(man_fpath, man_parts)
renamed = True # TODO: This overrides subsequent assertion in prs_parts
break
@@ -909,8 +1044,7 @@ def lv2_rename_preset(preset_path, new_preset_name):
for i, p in enumerate(prs_parts):
if bmre2.search(p):
# new_preset_name = zynthian_engine_jalv.sanitize_text(new_preset_name)
- prs_parts[i] = brre.sub(lambda m: m.group(
- 1) + new_preset_name + m.group(2), p)
+ prs_parts[i] = brre.sub(lambda m: m.group(1) + new_preset_name + m.group(2), p)
zynthian_engine_jalv.ttl_write_parts(preset_path, prs_parts)
renamed = True
break
@@ -920,6 +1054,7 @@ def lv2_rename_preset(preset_path, new_preset_name):
@staticmethod
def lv2_remove_preset(preset_path):
+ logging.debug(f"Removing LV2 preset '{preset_path}'")
preset_path = preset_path[7:]
bundle_path, preset_fname = os.path.split(preset_path)
@@ -933,13 +1068,15 @@ def lv2_remove_preset(preset_path):
del parts[i]
zynthian_engine_jalv.ttl_write_parts(man_fpath, parts)
os.remove(preset_path)
- return
+ return True
+ return False
@staticmethod
# Remove a preset bank
# bank: Bank object to remove
# Returns: True on success
def lv2_remove_bank(bank):
+ logging.debug(f"Removing LV2 bank '{bank[0]}'")
try:
path = bank[0][7:bank[0].rfind("/")]
except Exception as e:
diff --git a/zyngine/zynthian_engine_sooperlooper.py b/zyngine/zynthian_engine_sooperlooper.py
index 8158db9a7..9a42251d7 100644
--- a/zyngine/zynthian_engine_sooperlooper.py
+++ b/zyngine/zynthian_engine_sooperlooper.py
@@ -34,6 +34,7 @@
from . import zynthian_controller
from zynconf import ServerPort
+from zyngine.zynthian_signal_manager import zynsigman
# ------------------------------------------------------------------------------
# Sooper Looper State Codes
@@ -62,6 +63,8 @@
SL_STATE_REDO_ALL = 19
SL_STATE_OFF_MUTED = 20
+SS_GUI_CONTROL_MODE = 2 #TODO: This should be sourced from a common place but should not import gui classes here
+
# ------------------------------------------------------------------------------
# Sooper Looper Engine Class
# ------------------------------------------------------------------------------
@@ -74,6 +77,22 @@ class zynthian_engine_sooperlooper(zynthian_engine):
SL_PORT = ServerPort["sooperlooper_osc"]
MAX_LOOPS = 6
+ # SL_LOOP_SEL_PARAM act on the selected loop - send with osc command /sl/#/set where #=-3 for selected or index of loop (0..5)
+ SL_LOOP_SEL_PARAM = [
+ 'record',
+ 'overdub',
+ 'multiply',
+ 'replace',
+ 'substitute',
+ 'insert',
+ 'trigger',
+ 'mute',
+ 'oneshot',
+ 'pause',
+ 'reverse',
+ 'single_pedal'
+ ]
+
# SL_LOOP_PARAMS act on individual loops - sent with osc command /sl/#/set
SL_LOOP_PARAMS = [
'feedback', # range 0 -> 1
@@ -155,6 +174,7 @@ class zynthian_engine_sooperlooper(zynthian_engine):
]
SL_STATES = {
+ # Dictionary of SL states with indication of which controllers are on/off in this SL state
SL_STATE_UNKNOWN: {
'name': 'unknown',
'ctrl_off': [],
@@ -164,7 +184,7 @@ class zynthian_engine_sooperlooper(zynthian_engine):
},
SL_STATE_OFF: {
'name': 'off',
- 'ctrl_off': [],
+ 'ctrl_off': ['mute'],
'ctrl_on': [],
'next_state': False,
'icon': ''
@@ -331,19 +351,17 @@ def __init__(self, state_manager=None):
custom_slb_fpath = None
# Build SL command line
- #if self.config_remote_display():
- # self.command = ["slgui", "-l 0", f"-P {self.osc_target_port}", f"-J {self.jackname}"]
- #else:
- self.command = ["sooperlooper", "-q", "-l 0", "-D no", f"-p {self.osc_target_port}", f"-j {self.jackname}"]
+ self.command = ["sooperlooper", "-q", "-D no", f"-p {self.osc_target_port}", f"-j {self.jackname}"]
if custom_slb_fpath:
self.command += ["-m", custom_slb_fpath]
self.state = [-1] * self.MAX_LOOPS # Current SL state for each loop
self.next_state = [-1] * self.MAX_LOOPS # Next SL state for each loop (-1 if no state change pending)
self.waiting = [0] * self.MAX_LOOPS # 1 if a change of state is pending
- self.selected_loop = 0
+ self.selected_loop = None
self.loop_count = 1
self.channels = 2
+ self.selected_loop_cc_binding = True # True for MIDI CC to control selected loop. False to target all loops
ui_dir = os.environ.get('ZYNTHIAN_UI_DIR', "/zynthian/zynthian-ui")
self.custom_gui_fpath = f"{ui_dir}/zyngui/zynthian_widget_sooperlooper.py"
@@ -361,18 +379,18 @@ def __init__(self, state_manager=None):
loop_labels.append(str(i + 1))
self._ctrls = [
#symbol, {options}, midi_cc
- ['record', {'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 102],
- ['overdub', {'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 103],
- ['multiply', {'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 104],
- ['replace', {'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 105],
- ['substitute', {'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 106],
- ['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', {'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 108],
- ['mute', {'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 109],
- ['oneshot', {'value': 0, 'value_max': 1, 'labels': ['off', 'on'], 'is_toggle': True}, 110],
- ['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}],
@@ -396,21 +414,22 @@ def __init__(self, state_manager=None):
['input_gain', {'name': 'input gain', 'value': 1.0, 'value_max': 1.0, 'is_integer': False, 'is_logarithmic': True}],
['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}]
+ ['single_pedal', {'name': 'single pedal', 'value': 0, 'value_max': 1, 'labels': ['>', '<'], 'is_toggle': True}],
+ ['selected_loop_cc', {'name': 'midi cc to selected loop', 'value': 127, 'labels':['off', 'on']}]
]
# Controller Screens
self._ctrl_screens = [
- ['Loop record 1', ['record', 'overdub', 'multiply', 'undo/redo']],
- ['Loop record 2', ['replace', 'substitute', 'insert', 'undo/redo']],
- ['Loop control', ['trigger', 'oneshot', 'mute', 'pause']],
- ['Loop time/pitch', ['reverse', 'rate', 'stretch_ratio', 'pitch_shift']],
+ ['Loop record 1', ['record:0', 'overdub:0', 'multiply:0', 'undo/redo']],
+ ['Loop record 2', ['replace:0', 'substitute:0', 'insert:0', 'undo/redo']],
+ ['Loop control', ['trigger:0', 'oneshot:0', 'mute:0', 'pause:0']],
+ ['Loop time/pitch', ['reverse:0', 'rate', 'stretch_ratio', 'pitch_shift']],
['Loop levels', ['wet', 'dry', 'feedback', 'selected_loop_num']],
- ['Global loop', ['selected_loop_num', 'loop_count', 'prev/next', 'single_pedal']],
+ ['Global loop', ['selected_loop_num', 'loop_count', 'prev/next', 'single_pedal:0']],
['Global levels', ['rec_thresh', 'input_gain']],
['Global quantize', ['quantize', 'mute_quantized', 'overdub_quantized', 'replace_quantized']],
['Global sync 1', ['sync_source', 'sync', 'playback_sync', 'relative_sync']],
- ['Global sync 2', ['round', 'use_feedback_play']],
+ ['Global sync 2', ['round', 'use_feedback_play', 'selected_loop_cc']]
]
self.start()
@@ -551,20 +570,35 @@ def get_controllers_dict(self, processor):
midi_chan = None
for ctrl in self._ctrls:
ctrl[1]['processor'] = processor
- zctrl = zynthian_controller(self, ctrl[0], ctrl[1])
- processor.controllers_dict[zctrl.symbol] = zctrl
- if midi_chan is not None and len(ctrl) > 2:
- self.state_manager.chain_manager.add_midi_learn(midi_chan, ctrl[2], zctrl)
+ if ctrl[0] in self.SL_LOOP_SEL_PARAM:
+ # Create a zctrl for each loop
+ for i in range(self.MAX_LOOPS):
+ zctrl = zynthian_controller(self, f"{ctrl[0]}:{i}", ctrl[1])
+ processor.controllers_dict[zctrl.symbol] = zctrl
+ else:
+ zctrl = zynthian_controller(self, ctrl[0], ctrl[1])
+ processor.controllers_dict[zctrl.symbol] = zctrl
return processor.controllers_dict
def send_controller_value(self, zctrl):
- #logging.warning(f"{zctrl.symbol} {zctrl.value}")
- if self.osc_server is None or zctrl.symbol in ['oneshot', 'trigger'] and zctrl.value == 0:
+ if zctrl.symbol == "selected_loop_cc":
+ self.selected_loop_cc_binding = zctrl.value != 0
+ return
+ if ":" in zctrl.symbol:
+ symbol, chan = zctrl.symbol.split(":")
+ if self.selected_loop_cc_binding:
+ if int(chan) != self.selected_loop:
+ return
+ chan = -3
+ else:
+ symbol = zctrl.symbol
+ chan = -3
+ if self.osc_server is None or symbol in ['oneshot', 'trigger'] and zctrl.value == 0:
# Ignore off signals
return
- elif zctrl.symbol in ("mute", "pause"):
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', zctrl.symbol))
- elif zctrl.symbol == 'single_pedal':
+ elif symbol in ("mute", "pause"):
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', symbol))
+ elif symbol == 'single_pedal':
""" Single pedal logic
Idle -> Record
Record->Play
@@ -585,58 +619,58 @@ def send_controller_value(self, zctrl):
self.pedal_taps = 0
# Triple tap
if self.pedal_taps == 2:
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'undo_all'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'undo_all'))
# Double tap
elif self.pedal_taps == 1:
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'pause'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'pause'))
# Single tap
elif self.state[self.selected_loop] in (SL_STATE_UNKNOWN, SL_STATE_OFF, SL_STATE_OFF_MUTED):
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'record'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'record'))
elif self.state[self.selected_loop] == SL_STATE_RECORDING:
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'record'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'record'))
elif self.state[self.selected_loop] in (SL_STATE_PLAYING, SL_STATE_OVERDUBBING):
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'overdub'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'overdub'))
elif self.state[self.selected_loop] == SL_STATE_PAUSED:
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'trigger'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'trigger'))
# Pedal release: so check loop state, pedal press duration, etc.
else:
# Long press
if pedal_dur > 1.5:
if self.pedal_taps:
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'undo_all'))
- elif zctrl.symbol == 'selected_loop_num':
+ 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, True)
- elif zctrl.symbol in self.SL_LOOP_PARAMS: # Selected loop
- self.osc_server.send(self.osc_target, '/sl/-3/set', ('s', zctrl.symbol), ('f', zctrl.value))
- elif zctrl.symbol in self.SL_LOOP_GLOBAL_PARAMS: # All loops
- self.osc_server.send(self.osc_target, '/sl/-1/set', ('s', zctrl.symbol), ('f', zctrl.value))
- elif zctrl.symbol in self.SL_GLOBAL_PARAMS: # Global params
- self.osc_server.send(self.osc_target, '/set', ('s', zctrl.symbol), ('f', zctrl.value))
+ 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
+ self.osc_server.send(self.osc_target, '/sl/-1/set', ('s', symbol), ('f', zctrl.value))
+ elif symbol in self.SL_GLOBAL_PARAMS: # Global params
+ self.osc_server.send(self.osc_target, '/set', ('s', symbol), ('f', zctrl.value))
elif zctrl.is_toggle:
# Use is_toggle to indicate the SL function is a toggle, i.e. press to engage, press to release
- if zctrl.symbol == 'record' and zctrl.value == 0 and self.state[self.selected_loop] == SL_STATE_REC_STARTING:
+ if symbol == 'record' and zctrl.value == 0 and self.state[self.selected_loop] == SL_STATE_REC_STARTING:
# TODO: Implement better toggle of pending state
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'undo'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'undo'))
return
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', zctrl.symbol))
- #if zctrl.symbol == 'trigger':
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', symbol))
+ #if symbol == 'trigger':
#zctrl.set_value(0, False) # Make trigger a pulse
- elif zctrl.symbol == 'undo/redo':
+ elif symbol == 'undo/redo':
# Use single controller to perform undo (CCW) and redo (CW)
if zctrl.value == 0:
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'undo'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'undo'))
elif zctrl.value == 2:
- self.osc_server.send(self.osc_target, '/sl/-3/hit', ('s', 'redo'))
+ self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'redo'))
zctrl.set_value(1, False)
- elif zctrl.symbol == 'prev/next':
+ elif symbol == 'prev/next':
# Use single controller to perform prev(CCW) and next (CW)
if zctrl.value < 63:
self.select_loop(self.selected_loop - 1, True)
elif zctrl.value > 63:
self.select_loop(self.selected_loop + 1, True)
zctrl.set_value(63, False)
- elif zctrl.symbol == 'loop_count':
+ elif symbol == 'loop_count':
for loop in range(self.loop_count, zctrl.value):
self.osc_server.send(self.osc_target, '/loop_add', ('i', self.channels), ('f', 30), ('i', 0))
if zctrl.value < self.loop_count:
@@ -656,7 +690,6 @@ def cb_osc_all(self, path, args, types, src):
return
try:
processor = self.processors[0]
- #logging.debug(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])
@@ -683,7 +716,7 @@ def cb_osc_all(self, path, args, types, src):
self.monitors_dict['state'] = self.state[loop]
self.monitors_dict['next_state'] = self.next_state[loop]
self.monitors_dict['waiting'] = self.waiting[loop]
- self.update_state()
+ self.update_state(loop)
elif path == '/info':
# args: s:hosturl s:version i:loopcount
@@ -712,11 +745,10 @@ def cb_osc_all(self, path, args, types, src):
if self.loop_count > 1:
# Set defaults for new loops
self.osc_server.send(self.osc_target, f"/sl/{self.loop_count - 1 - i}/set", ('s', 'sync'), ('f', 1))
- if self.loop_count > 1:
- self.select_loop(self.loop_count - 1, True)
+ 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
@@ -749,8 +781,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:
@@ -760,44 +790,67 @@ def cb_osc_all(self, path, args, types, src):
# Specific functions
# ---------------------------------------------------------------------------
- # Update 'state' controllers to match state of selected loop
- def update_state(self):
+ # Update 'state' controllers of loop
+ def update_state(self, loop):
try:
processor = self.processors[0]
except:
return
try:
- current_state = self.state[self.selected_loop]
+ current_state = self.state[loop]
+ #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']:
- processor.controllers_dict[symbol].set_readonly(False)
- processor.controllers_dict[symbol].set_value(0, False)
+ if symbol in self.SL_LOOP_SEL_PARAM:
+ symbol += f":{loop}"
+ processor.controllers_dict[symbol].set_readonly(False)
+ processor.controllers_dict[symbol].set_value(0, False)
+ # Turn on all controllers that are on in this state
for symbol in self.SL_STATES[current_state]['ctrl_on']:
- processor.controllers_dict[symbol].set_readonly(False)
- processor.controllers_dict[symbol].set_value(1, False)
- next_state = self.next_state[self.selected_loop]
+ if symbol in self.SL_LOOP_SEL_PARAM:
+ symbol += f":{loop}"
+ processor.controllers_dict[symbol].set_readonly(False)
+ processor.controllers_dict[symbol].set_value(1, False)
+ next_state = self.next_state[loop]
+ # Set next_state for controllers that are part of logical sequence
if self.SL_STATES[next_state]['next_state']:
for symbol in self.SL_STATES[next_state]['ctrl_on']:
- processor.controllers_dict[symbol].set_value(1, False)
- processor.controllers_dict[symbol].set_readonly(True)
+ if symbol in self.SL_LOOP_SEL_PARAM:
+ symbol += f":{loop}"
+ processor.controllers_dict[symbol].set_value(1, False)
+ processor.controllers_dict[symbol].set_readonly(True)
+
except Exception as e:
logging.error(e)
#self.processors[0].status = self.SL_STATES[self.state]['icon']
def select_loop(self, loop, send=False):
+ try:
+ processor = self.processors[0]
+ except:
+ return
if loop < 0 or loop >= self.loop_count:
return # TODO: Handle -1 == all loops
self.selected_loop = int(loop)
+ """
self.monitors_dict['state'] = self.state[self.selected_loop]
self.monitors_dict['next_state'] = self.next_state[self.selected_loop]
self.monitors_dict['waiting'] = self.waiting[self.selected_loop]
self.update_state()
- try:
- self.processors[0].controllers_dict['selected_loop_num'].set_value(loop + 1, False)
- except:
- pass
+ """
+ processor.controllers_dict['selected_loop_num'].set_value(loop + 1, False)
if send and self.osc_server:
self.osc_server.send(self.osc_target, '/set', ('s', 'selected_loop_num'), ('f', self.selected_loop))
+ self._ctrl_screens[0][1] = [f'record:{self.selected_loop}', f'overdub:{self.selected_loop}', f'multiply:{self.selected_loop}', 'undo/redo']
+ self._ctrl_screens[1][1] = [f'replace:{self.selected_loop}', f'substitute:{self.selected_loop}', f'insert:{self.selected_loop}', 'undo/redo']
+ self._ctrl_screens[2][1] = [f'trigger:{self.selected_loop}', f'oneshot:{self.selected_loop}', f'mute:{self.selected_loop}', f'pause:{self.selected_loop}']
+ self._ctrl_screens[3][1][0] = f'reverse:{self.selected_loop}'
+ self._ctrl_screens[5][1][3] = f'single_pedal:{self.selected_loop}'
+ processor.refresh_controllers()
+
+ zynsigman.send_queued(zynsigman.S_GUI, SS_GUI_CONTROL_MODE, mode='control')
+
def prev_loop(self):
self.processors[0].controllers_dict['prev/next'].nudge(-1)
diff --git a/zyngine/zynthian_engine_zynaddsubfx.py b/zyngine/zynthian_engine_zynaddsubfx.py
index 56db836f9..0c8ab1d8b 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_lv2.py b/zyngine/zynthian_lv2.py
index 6dde6afba..879541ce5 100755
--- a/zyngine/zynthian_lv2.py
+++ b/zyngine/zynthian_lv2.py
@@ -196,12 +196,11 @@ def init_lilv():
# world.set_option(lilv.OPTION_FILTER_LANG, world.new_bool(False))
world.load_all()
world.ns.ev = lilv.Namespace(world, "http://lv2plug.in/ns/ext/event#")
- world.ns.presets = lilv.Namespace(
- world, "http://lv2plug.in/ns/ext/presets#")
- world.ns.portprops = lilv.Namespace(
- world, "http://lv2plug.in/ns/ext/port-props#")
- world.ns.portgroups = lilv.Namespace(
- world, "http://lv2plug.in/ns/ext/port-groups#")
+ world.ns.presets = lilv.Namespace(world, "http://lv2plug.in/ns/ext/presets#")
+ world.ns.portprops = lilv.Namespace(world, "http://lv2plug.in/ns/ext/port-props#")
+ world.ns.portgroups = lilv.Namespace(world, "http://lv2plug.in/ns/ext/port-groups#")
+ world.ns.parameters = lilv.Namespace(world, "http://lv2plug.in/ns/ext/parameters#")
+ world.ns.patch = lilv.Namespace(world, "http://lv2plug.in/ns/ext/patch#")
# ------------------------------------------------------------------------------
@@ -747,69 +746,89 @@ def sanitize_fname(s):
# LV2 Port management
# ------------------------------------------------------------------------------
+def get_node_value(node):
+ if node is None:
+ return None
+ elif node.is_int():
+ return int(node)
+ elif node.is_float():
+ return float(node)
+ elif node.is_bool():
+ return bool(node)
+ else:
+ return str(node)
+
def get_plugin_ports(plugin_url):
wplugins = world.get_all_plugins()
plugin = wplugins[plugin_url]
ports_info = {}
+ # Control ports
for i in range(plugin.get_num_ports()):
- port = plugin.get_port_by_index(i)
- if port.is_a(lilv.LILV_URI_INPUT_PORT) and port.is_a(lilv.LILV_URI_CONTROL_PORT):
- port_name = str(port.get_name())
- port_symbol = str(port.get_symbol())
-
- is_toggled = port.has_property(world.ns.lv2.toggled)
- is_integer = port.has_property(world.ns.lv2.integer)
- is_enumeration = port.has_property(world.ns.lv2.enumeration)
- is_logarithmic = port.has_property(world.ns.portprops.logarithmic)
+ control = plugin.get_port_by_index(i)
+ if control.is_a(lilv.LILV_URI_INPUT_PORT) and control.is_a(lilv.LILV_URI_CONTROL_PORT):
+ name = str(control.get_name())
+ symbol = str(control.get_symbol())
+
+ is_toggled = control.has_property(world.ns.lv2.toggled)
+ is_integer = control.has_property(world.ns.lv2.integer)
+ is_enumeration = control.has_property(world.ns.lv2.enumeration)
+ is_logarithmic = control.has_property(world.ns.portprops.logarithmic)
+
envelope = None
for env_type in ["delay", "attack", "hold", "decay", "sustain", "fade", "release"]:
- if str(port.get("http://lv2plug.in/ns/lv2core#designation")) == f"http://lv2plug.in/ns/ext/parameters#{env_type}":
+ # "http://lv2plug.in/ns/lv2core#designation"
+ if str(control.get(world.ns.lv2.designation)) == f"http://lv2plug.in/ns/ext/parameters#{env_type}":
envelope = env_type
- not_on_gui = port.has_property(world.ns.portprops.notOnGUI)
- display_priority = port.get(world.ns.lv2.displayPriority)
+
+ not_on_gui = control.has_property(world.ns.portprops.notOnGUI)
+ display_priority = control.get(world.ns.lv2.displayPriority)
if display_priority is None:
display_priority = 0
else:
display_priority = int(display_priority)
- # logging.debug("PORT {} properties =>".format(port.get_symbol()))
- # for node in port.get_properties():
+ # logging.debug("CONTROL {} properties =>".format(control.get_symbol()))
+ # for node in control.get_properties():
# logging.debug(" => {}".format(get_node_value(node)))
- pgroup_index = None
- pgroup_name = None
- pgroup_symbol = None
- pgroup = port.get(world.ns.portgroups.group)
- if pgroup is not None:
- # pgroup_key = str(pgroup).split("#")[-1]
- pgroup_index = world.get(pgroup, world.ns.lv2.index, None)
- if pgroup_index is not None:
- pgroup_index = int(pgroup_index)
- # logging.warning("Port group <{}> has no index.".format(pgroup_key))
- pgroup_name = world.get(pgroup, world.ns.lv2.name, None)
- if pgroup_name is None:
- pgroup_name = world.get(pgroup, world.ns.rdfs.label, None)
- if pgroup_name is not None:
- pgroup_name = str(pgroup_name)
- # logging.warning("Port group <{}> has no name.".format(pgroup_key))
- pgroup_symbol = world.get(pgroup, world.ns.lv2.symbol, None)
- if pgroup_symbol is not None:
- pgroup_symbol = str(pgroup_symbol)
- # logging.warning("Port group <{}> has no symbol.".format(pgroup_key))
+ group_index = None
+ group_name = None
+ group_symbol = None
+ group = control.get(world.ns.portgroups.group)
+ if group is not None:
+ # group_key = str(group).split("#")[-1]
+ group_index = world.get(group, world.ns.lv2.index, None)
+ if group_index is not None:
+ group_index = int(group_index)
+ # logging.warning("Control group <{}> has no index.".format(group_key))
+ group_name = world.get(group, world.ns.lv2.name, None)
+ if group_name is None:
+ group_name = world.get(group, world.ns.rdfs.label, None)
+ if group_name is not None:
+ group_name = str(group_name)
+ # logging.warning("Control group <{}> has no name.".format(group_key))
+ group_symbol = world.get(group, world.ns.lv2.symbol, None)
+ if group_symbol is not None:
+ group_symbol = str(group_symbol)
+ # logging.warning("Control group <{}> has no symbol.".format(group_key))
# else:
- # logging.debug("Port <{}> has no group.".format(port_symbol))
+ # logging.debug("Control <{}> has no group.".format(symbol))
sp = []
- for p in port.get_scale_points():
+ for p in control.get_scale_points():
sp.append({
'label': str(p.get_label()),
'value': get_node_value(p.get_value())
})
sp = sorted(sp, key=lambda k: k['value'])
- r = port.get_range()
+ r = control.get_range()
+ try:
+ vdef = get_node_value(r[0])
+ except:
+ vdef = vmin
try:
vmin = get_node_value(r[1])
except:
@@ -824,18 +843,14 @@ def get_plugin_ports(plugin_url):
vmax = max(sp, key=lambda x: x['value'])
else:
vmax = 1.0
- try:
- vdef = get_node_value(r[0])
- except:
- vdef = vmin
- info = {
+ ports_info[i] = {
'index': i,
- 'symbol': port_symbol,
- 'name': port_name,
- 'group_index': pgroup_index,
- 'group_name': pgroup_name,
- 'group_symbol': pgroup_symbol,
+ 'symbol': symbol,
+ 'name': name,
+ 'group_index': group_index,
+ 'group_name': group_name,
+ 'group_symbol': group_symbol,
'value': vdef,
'range': {
'default': vdef,
@@ -851,19 +866,97 @@ def get_plugin_ports(plugin_url):
'display_priority': display_priority,
'scale_points': sp
}
- ports_info[i] = info
# logging.debug("PORT {} => {}".format(i, info))
- return ports_info
+ # Property parameters
+ i = len(ports_info)
+ for control in world.find_nodes(plugin.get_uri(), world.ns.patch.writable, None):
+ symbol = world.get_symbol(control)
+ name = str(world.get(control, world.ns.rdfs.label, None))
+
+ range_type = str(world.get(control, world.ns.rdfs.range, None))
+ vdef = get_node_value(world.get(control, world.ns.lv2.default, None))
+ vmin = get_node_value(world.get(control, world.ns.lv2.minimum, None))
+ vmax = get_node_value(world.get(control, world.ns.lv2.maximum, None))
+
+ is_toggled = world.get(control, world.ns.lv2.toggled, None) is not None
+ is_integer = world.get(control, world.ns.lv2.integer, None) is not None
+ is_enumeration = world.get(control, world.ns.lv2.enumeration, None) is not None
+ is_logarithmic = world.get(control, world.ns.portprops.logarithmic, None) is not None
+
+ envelope = None
+ for env_type in ["delay", "attack", "hold", "decay", "sustain", "fade", "release"]:
+ if str(world.get(control, world.ns.lv2.designation, None)) == f"http://lv2plug.in/ns/ext/parameters#{env_type}":
+ envelope = env_type
+
+ not_on_gui = world.get(control, world.ns.portprops.notOnGUI, None) is not None
+ display_priority = world.get(control, world.ns.lv2.displayPriority, None)
+ if display_priority is None:
+ display_priority = 0
+ else:
+ display_priority = int(display_priority)
+
+ # logging.debug("CONTROL {} properties =>".format(control.get_symbol()))
+ # for node in control.get_properties():
+ # logging.debug(" => {}".format(get_node_value(node)))
+
+ group_index = None
+ group_name = None
+ group_symbol = None
+ group = world.get(control, world.ns.portgroups.group, None)
+ if group is not None:
+ # group_key = str(group).split("#")[-1]
+ group_index = world.get(group, world.ns.lv2.index, None)
+ if group_index is not None:
+ group_index = int(group_index)
+ # logging.warning("Control group <{}> has no index.".format(group_key))
+ group_name = world.get(group, world.ns.lv2.name, None)
+ if group_name is None:
+ group_name = world.get(group, world.ns.rdfs.label, None)
+ if group_name is not None:
+ group_name = str(group_name)
+ # logging.warning("Control group <{}> has no name.".format(group_key))
+ group_symbol = world.get(group, world.ns.lv2.symbol, None)
+ if group_symbol is not None:
+ group_symbol = str(group_symbol)
+ # logging.warning("Control group <{}> has no symbol.".format(group_key))
+ # else:
+ # logging.debug("Control <{}> has no group.".format(symbol))
+
+ sp = []
+ for p in world.find_nodes(control, world.ns.lv2.scalePoint, None):
+ sp.append({
+ 'label': str(world.get(p, world.ns.rdfs.label, None)),
+ 'value': get_node_value(world.get(p, world.ns.rdf.value, None))
+ })
+ sp = sorted(sp, key=lambda k: k['value'])
+
+ ports_info[i] = {
+ 'index': i,
+ 'symbol': symbol,
+ 'name': name,
+ 'group_index': group_index,
+ 'group_name': group_name,
+ 'group_symbol': group_symbol,
+ 'value': vdef,
+ 'range': {
+ 'default': vdef,
+ 'min': vmin,
+ 'max': vmax
+ },
+ 'is_toggled': is_toggled,
+ 'is_integer': is_integer,
+ 'is_enumeration': is_enumeration,
+ 'is_logarithmic': is_logarithmic,
+ 'envelope': envelope,
+ 'not_on_gui': not_on_gui,
+ 'display_priority': display_priority,
+ 'scale_points': sp
+ }
+ i += 1
+ return ports_info
-def get_node_value(node):
- if node.is_int():
- return int(node)
- elif node.is_float():
- return float(node)
- else:
- return str(node)
# ------------------------------------------------------------------------------
# Main program
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 6a35834a1..c205796db 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:
@@ -2001,22 +1983,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}")
@@ -2072,8 +2050,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:
@@ -2128,8 +2105,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(
@@ -2187,8 +2163,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
@@ -2205,8 +2180,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
@@ -2248,8 +2222,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)
@@ -2262,8 +2235,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):
@@ -2767,10 +2739,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:
@@ -2782,10 +2754,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 6d68865bb..261a414af 100644
--- a/zyngui/__init__.py
+++ b/zyngui/__init__.py
@@ -5,6 +5,7 @@
"zynthian_gui_info",
"zynthian_gui_help",
"zynthian_gui_selector",
+ "zynthian_gui_selector_info",
"zynthian_gui_details",
"zynthian_gui_admin",
"zynthian_gui_snapshot",
@@ -34,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/multitouch.py b/zyngui/multitouch.py
index ec365378d..bc4879999 100644
--- a/zyngui/multitouch.py
+++ b/zyngui/multitouch.py
@@ -26,6 +26,7 @@
# Based on code from https://github.com/pimoroni/python-multitouch
import struct
+import tkinter
import logging
from enum import Enum
from glob import glob
@@ -323,8 +324,21 @@ def _process_touch_events(self):
event.time = now
if event._type == MultitouchTypes.MULTI_PRESS:
- event.widget = zynthian_gui_config.top.winfo_containing(
- event.x_root, event.y_root)
+ # Find a widget for the touch event
+ try:
+ event.widget = zynthian_gui_config.top.winfo_containing(event.x_root, event.y_root)
+ except:
+ gui_obj = zynthian_gui_config.zyngui.get_current_screen_obj()
+ if isinstance(gui_obj, tkinter.Frame):
+ event.widget = gui_obj
+ #logging.debug("Using current screen object for touch event")
+ else:
+ try:
+ event.widget = gui_obj.main_frame
+ #logging.debug("Using main_frame for touch event")
+ except:
+ logging.error("Can't find a widget for touch event")
+ continue
event.offset_x = event.widget.winfo_rootx()
event.offset_y = event.widget.winfo_rooty()
event.x = event.x_root - event.offset_x # Reassert because offset has changed
@@ -472,8 +486,7 @@ def _on_touch_timeout(self, try_single_touch=True):
event = self._g_pending
self._g_pending = None
try:
- event.tag = event.widget.find_overlapping(
- event.x, event.y, event.x, event.y)[0]
+ event.tag = event.widget.find_overlapping(event.x, event.y, event.x, event.y)[0]
except:
event.tag = None
for ev_handler in self._on_press:
diff --git a/zyngui/zynthian_gui.py b/zyngui/zynthian_gui.py
index d9408714d..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()
@@ -854,7 +820,8 @@ def show_help(self, topic=None):
if not topic:
topic = self.current_screen
if self.screens['help'].load_file(f"./help/{topic}.html"):
- self.show_screen("help")
+ pass
+ #self.show_screen("help")
elif topic != "help":
logging.warning(f"No help for '{topic}'")
@@ -982,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
@@ -1286,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:
@@ -1305,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()
@@ -1323,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)
@@ -1335,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)
@@ -1439,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
@@ -1469,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
@@ -1502,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! ")
@@ -1523,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
@@ -1592,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()
@@ -1683,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):
@@ -1708,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):
@@ -1754,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
@@ -1771,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
@@ -1884,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.
@@ -1903,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!")
@@ -1921,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!")
@@ -1939,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!")
@@ -1956,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!")
@@ -1993,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):
@@ -2162,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
@@ -2233,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)
@@ -2329,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:
@@ -2348,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)
@@ -2421,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):
@@ -2448,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:
@@ -2470,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
@@ -2490,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]
@@ -2525,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()
@@ -2612,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 01d5c3b76..a8f15ae19 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
#
# ******************************************************************************
#
@@ -37,14 +37,14 @@
import zynautoconnect
from zyncoder.zyncore import lib_zyncore
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 Admin GUI Class
# -------------------------------------------------------------------------------
-class zynthian_gui_admin(zynthian_gui_selector):
+class zynthian_gui_admin(zynthian_gui_selector_info):
data_dir = os.environ.get('ZYNTHIAN_DATA_DIR', "/zynthian/zynthian-data")
sys_dir = os.environ.get('ZYNTHIAN_SYS_DIR', "/zynthian/zynthian-sys")
@@ -62,7 +62,7 @@ def __init__(self):
self.wifi_status = "???"
self.filling_list = False
- super().__init__('Action', True)
+ super().__init__('Action')
self.state_manager = self.zyngui.state_manager
@@ -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,130 +109,116 @@ 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", ["Configure MIDI input devices.", "midi_input.png"]))
+ self.list_data.append((self.zyngui.midi_out_config, 0, "MIDI Output Devices", ["Configure MIDI output devices.", "midi_output.png"]))
# 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", ["Send active MIDI channel messages to active chain only.", "midi_logo.png"]))
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", ["Send active MIDI channel messages to all chains with same MIDI channel.", "midi_logo.png"]))
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", ["MIDI Program Change messages recall snapshots", "midi_logo.png"]))
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", ["MIDI Program Change messages recall ZS3.", "midi_logo.png"]))
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", ["Select bank when MIDI Program Change received", "midi_logo.png"]))
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", ["Don't select bank when MIDI Program Change received", "midi_logo.png"]))
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", ["Load preset for preview when a MIDI note-on command is received", "midi_logo.png"]))
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", ["Do not load preset for preview when a MIDI note-on command is received", "midi_logo.png"]))
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", ["MIDI ports are indexed by their device name and the physical USB port to which they are plugged", "midi_logo.png"]))
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", ["MIDI ports are indexed by their device name only.", "midi_logo.png"]))
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", ["System messages are sent to MIDI outputs.", "midi_logo.png"]))
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", ["System messages are not sent to MIDI outputs.", "midi_logo.png"]))
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", ["MIDI note transpose.\nThis effects all MIDI messages and is in addition to individual chain transpose.", "midi_logo.png"]))
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", ["Raspberry Pi onboard (low fidelity) headphone output is enabled", "headphone.png"]))
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", ["Raspberry Pi onboard (low fidelity) headphone output is disabled", "headphone.png"]))
+
+ self.list_data.append((self.hotplug_audio_menu, 0, "Hotplug USB Audio", ["Configure USB audio hotplug.\n\nWhen enabled, USB audio devices will be detected and available. This does not include any device that is already configured as the main audio device which must always reamain connected.", None]))
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", ["Soundcard parameters are saved with snapshot", "meter.png"]))
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", ["Soundcard parameters are not saved with snapshot", "meter.png"]))
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", ["Peak programme meters are enabled.", "meter.png"]))
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", ["Peak programme meters are disabled.\nThis saves a little CPU power.", "meter.png"]))
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.network_info, 0, "Network Info", ["Show network details, e.g. IP address, etc.", None]))
+ self.list_data.append((self.wifi_config, 0, f"Wi-Fi Config ({self.wifi_status})", ["Configure Wi-Fi connections.", None]))
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", ["Display of zynthian UI and processors' native GUI via VNC enabled.\nThis uses more CPU. It is advised to disable during performance.", None]))
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", ["Display of zynthian UI and processors' native GUI via VNC disabled.", None]))
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}", ["Select touch interface mode.\n\nFor touch-only devices with 5\" screen or less, select touch-widgets.\nFor large touch screen, select V5...\nFor full hardware device, e.g. V5, select None", None]))
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", ["Adjust display and LED brightness.", None]))
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.show_cv_config, 0, "CV Settings", ["Control Voltage configuration.", None]))
+ self.list_data.append((self.zyngui.calibrate_touchscreen, 0, "Calibrate Touchscreen", ["Show touchscreen calibration.\nTouch each crosshair until it changes color.\nScreen closes after 15s of inactivity.", None]))
+ self.list_data.append((self.zyngui.cuia_screen_clean, 0, "Clean Screen", ["10s countdown with no touch trigger. Allows screen to be cleaned without triggering any action.", None]))
+ self.list_data.append((self.bluetooth, 0, "Bluetooth", ["Scan, enable and configure Bluetooth devices.\n\nMust enable Bluetooth here to access BLE MIDI devices. Also supports HID devices.", "bluetooth.png"]))
self.list_data.append((None, 0, "> TEST"))
- self.list_data.append((self.test_audio, 0, "Test Audio"))
- self.list_data.append((self.test_midi, 0, "Test MIDI"))
+ self.list_data.append((self.test_audio, 0, "Test Audio", ["Play an audio track to test audio output.\n\nPress BACK to cancel playback.", "headphones.png"]))
+ self.list_data.append((self.test_midi, 0, "Test MIDI", ["Play a MIDI track to test MIDI output.\n\nThis will play the MIDI through any loaded chains.\nPress BACK to cancel playback.", "midi_logo.png"]))
if zynthian_gui_config.control_test_enabled:
- self.list_data.append((self.control_test, 0, "Test control HW"))
+ self.list_data.append((self.control_test, 0, "Test control HW", ["Test system hardware.", None]))
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", ["End workflow capture session", None]))
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", ["Start workflow capture session.\n\nZynthian display, encoder and button actions are saved to file until this option is deselected.", None]))
if self.state_manager.update_available:
- self.list_data.append((self.update_software, 0, "Update Software"))
+ self.list_data.append((self.update_software, 0, "Update Software", ["Updates zynthian firmware and software from Internet.\n\nThis option is only shown when there are updates availale, as indicated by the \u21bb icon in the topbar.\nUpdates may take several minutes. Do not poweroff during an update.", None]))
# self.list_data.append((self.update_system, 0, "Update Operating System"))
# self.list_data.append((None, 0, "> POWER"))
# self.list_data.append((self.restart_gui, 0, "Restart UI"))
if zynthian_gui_config.debug_thread:
- self.list_data.append((self.exit_to_console, 0, "Exit"))
- self.list_data.append((self.reboot, 0, "Reboot"))
- self.list_data.append((self.power_off, 0, "Power Off"))
+ self.list_data.append((self.exit_to_console, 0, "Exit", ["Stop zynthian UI but do not reboot.", None]))
+ self.list_data.append((self.reboot, 0, "Reboot", ["Reboot (restart) zynthian.", None]))
+ self.list_data.append((self.power_off, 0, "Power Off", ["Turn off zynthian.\n\nPower is still fed to the device but it is effectively off.", None]))
super().fill_list()
self.filling_list = False
@@ -257,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):
@@ -277,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")
@@ -329,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()
@@ -359,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)
@@ -389,6 +370,55 @@ 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:
+ options[f"\u2612 Hotplug Audio"] = "disable_hotplug"
+ options["Input Devices"] = None
+ for device in zynautoconnect.get_alsa_hotplug_audio_devices(False):
+ if device in zynthian_gui_config.disabled_audio_in:
+ options[f"\u2610 {device} in"] = "enable_input"
+ else:
+ options[f"\u2612 {device} in"] = "disable_input"
+ options["Output Devices"] = None
+ for device in zynautoconnect.get_alsa_hotplug_audio_devices(True):
+ if device in zynthian_gui_config.disabled_audio_out:
+ options[f"\u2610 {device} out"] = "enable_output"
+ else:
+ options[f"\u2612 {device} out"] = "disable_output"
+ else:
+ options[f"\u2610 Hotplug Audio"] = "enable_hotplug"
+ return options
+
+ def hotplug_audio_menu(self):
+ self.zyngui.screens['option'].config("Hotplug Audio", self.get_hotplug_menu_options(), self.hotplug_audio_cb, False)
+ self.zyngui.show_screen('option')
+
+ def hotplug_audio_cb(self, option, value):
+ zynautoconnect.pause()
+ match value:
+ case "enable_hotplug":
+ self.zyngui.state_manager.start_busy("hotplug", "Enabling hotplug audio")
+ zynautoconnect.enable_hotplug()
+ case "disable_hotplug":
+ self.zyngui.state_manager.start_busy("hotplug", "Disabling hotplug audio")
+ zynautoconnect.disable_hotplug()
+ case "enable_input":
+ self.zyngui.state_manager.start_busy("hotplug", f"Enabling {option[2:]}")
+ zynautoconnect.enable_audio_input_device(option[2:-3])
+ case "disable_input":
+ self.zyngui.state_manager.start_busy("hotplug", f"Disabling {option[2:]}")
+ zynautoconnect.enable_audio_input_device(option[2:-3], False)
+ case "enable_output":
+ self.zyngui.state_manager.start_busy("hotplug", f"Enabling {option[2:]}")
+ zynautoconnect.enable_audio_output_device(option[2:-4])
+ case "disable_output":
+ self.zyngui.state_manager.start_busy("hotplug", f"Disabling {option[2:]}")
+ zynautoconnect.enable_audio_output_device(option[2:-4], False)
+ self.zyngui.screens['option'].options = self.get_hotplug_menu_options()
+ self.zyngui.state_manager.end_busy("hotplug")
+ zynautoconnect.resume()
+
def toggle_dpm(self):
zynthian_gui_config.enable_dpm = not zynthian_gui_config.enable_dpm
self.update_list()
@@ -420,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
# -------------------------------------------------------------------------
@@ -553,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")
@@ -613,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")
@@ -623,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 c5e043433..de76a19ef 100644
--- a/zyngui/zynthian_gui_audio_in.py
+++ b/zyngui/zynthian_gui_audio_in.py
@@ -27,32 +27,53 @@
# 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
+ def build_view(self):
+ self.check_ports = 0
+ self.capture_ports = zynautoconnect.get_audio_capture_ports()
+ return super().build_view()
+
+ def refresh_status(self):
+ super().refresh_status()
+ self.check_ports += 1
+ if self.check_ports > 10:
+ self.check_ports = 0
+ ports = zynautoconnect.get_audio_capture_ports()
+ if self.capture_ports != ports:
+ self.capture_ports = ports
+ self.fill_list()
+
def fill_list(self):
self.list_data = []
- for i, scp in enumerate(zynautoconnect.get_audio_capture_ports()):
+ for i, scp in enumerate(self.capture_ports):
+ if scp.aliases:
+ suffix = f" ({scp.aliases[0]})"
+ else:
+ suffix = ""
if i + 1 in self.chain.audio_in:
self.list_data.append(
- (i + 1, scp.name, f"\u2612 Audio input {i + 1}"))
+ (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}"))
+ (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 a93faccee..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,13 +36,15 @@
# ------------------------------------------------------------------------------
-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
+ self.playback_ports = zynautoconnect.get_hw_audio_dst_ports()
if super().build_view():
zynsigman.register_queued(
zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_STATE, self.update_rec)
@@ -65,11 +65,21 @@ def update_rec(self, state):
def set_chain(self, chain):
self.chain = chain
+ def refresh_status(self):
+ super().refresh_status()
+ self.check_ports += 1
+ if self.check_ports > 10:
+ self.check_ports = 0
+ ports = zynautoconnect.get_hw_audio_dst_ports()
+ if self.playback_ports != ports:
+ self.playback_ports = ports
+ self.fill_list()
+
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":
@@ -77,38 +87,43 @@ 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 = []
# Direct physical outputs
self.list_data.append((None, None, "> Direct Outputs"))
- ports = zynautoconnect.get_hw_audio_dst_ports()
- port_count = len(ports)
- for i in range(1, port_count + 1, 2):
- if i < port_count:
- port_names.append((f"Output {i}", f"system:playback_{i}$"))
- port_names.append((f"Output {i + 1}", f"system:playback_{i + 1}$"))
- port_names.append((f"Outputs {i}+{i + 1}", f"system:playback_[{i},{i + 1}]$"))
+ port_count = len(self.playback_ports)
+ for i in range(0, port_count, 2):
+ if self.playback_ports[i].aliases:
+ suffix = f" ({self.playback_ports[i].aliases[0]})"
else:
- port_names.append((f"Output {i}", f"system:playback_{i}$"))
- for title, processor in port_names:
+ suffix = ""
+ 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}$", [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)
@@ -117,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_bluetooth.py b/zyngui/zynthian_gui_bluetooth.py
index dbf5a6678..95b596156 100644
--- a/zyngui/zynthian_gui_bluetooth.py
+++ b/zyngui/zynthian_gui_bluetooth.py
@@ -30,7 +30,7 @@
from subprocess import Popen, PIPE
# Zynthian specific modules
-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
@@ -39,7 +39,7 @@
# ------------------------------------------------------------------------------
-class zynthian_gui_bluetooth(zynthian_gui_selector):
+class zynthian_gui_bluetooth(zynthian_gui_selector_info):
def __init__(self):
self.proc = None
@@ -49,7 +49,7 @@ def __init__(self):
self.ble_controllers = {}
self.ble_devices = {} # Map of BLE devices, indexed by device address
self.pending_actions = [] # List of BLE commands to queue
- super().__init__('Bluetooth', True)
+ super().__init__('Bluetooth')
self.select_path.set("Bluetooth")
def build_view(self):
@@ -73,16 +73,16 @@ def fill_list(self):
if zynthian_gui_config.bluetooth_enabled:
self.list_data.append(
- ("stop_bluetooth", None, "\u2612 Enable Bluetooth"))
+ ("stop_bluetooth", None, "\u2612 Enable Bluetooth", ["Bluetooth is enabled.\n\nSelect to disable Bluetooth.", "bluetooth.png"]))
if len(self.ble_controllers) == 0:
self.list_data.append(
- (None, None, "No Bluetooth controllers detected!"))
+ (None, None, "No Bluetooth controllers detected!", ["There are not Bluetooth controllers attached to zynthian. You may connect a Bluetooth USB device.", "bluetooth.png"]))
super().fill_list()
return
for ctrl in sorted(self.ble_controllers.keys()):
chk = "\u2612" if self.ble_controllers[ctrl]["enabled"] else "\u2610"
self.list_data.append(
- ("enable_controller", ctrl, f" {chk} {self.ble_controllers[ctrl]['alias']}"))
+ ("enable_controller", ctrl, f" {chk} {self.ble_controllers[ctrl]['alias']}", ["Enable/disable Bluetooth controller.\n\nOnly enable a single controller. It is advised to use a USB Bluetooth adapter because the Raspberry Pi onboard adapter has poor range.", "bluetooth.png"]))
self.list_data.append((None, None, "Devices"))
for addr, data in self.ble_devices.items():
# [name, paired, trusted, connected, is_midi]
@@ -93,10 +93,10 @@ def fill_list(self):
if data[3]:
title += "\uf293 "
title += data[0]
- self.list_data.append((f"BLE:{addr}", addr, title))
+ self.list_data.append((f"BLE:{addr}", addr, title, ["Enable/disable this USB device.\n\nEnabling a device will pair it with zynthian. This state will be remembered.", "bluetooth.png"]))
else:
self.list_data.append(
- ("start_bluetooth", None, "\u2610 Enable Bluetooth"))
+ ("start_bluetooth", None, "\u2610 Enable Bluetooth", ["Bluetooth is disabled.\n\nSelect to enable Bluetooth.", "bluetooth.png"]))
super().fill_list()
diff --git a/zyngui/zynthian_gui_chain_menu.py b/zyngui/zynthian_gui_chain_menu.py
index c0ac75244..fb6554a95 100644
--- a/zyngui/zynthian_gui_chain_menu.py
+++ b/zyngui/zynthian_gui_chain_menu.py
@@ -26,18 +26,17 @@
import logging
# 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 App Selection GUI Class
# ------------------------------------------------------------------------------
-class zynthian_gui_chain_menu(zynthian_gui_selector):
+class zynthian_gui_chain_menu(zynthian_gui_selector_info):
def __init__(self):
- super().__init__('Menu', True)
+ super().__init__('Menu')
def fill_list(self):
self.list_data = []
@@ -49,22 +48,45 @@ def fill_list(self):
mixer_avail = False
self.list_data.append((None, 0, "> ADD CHAIN"))
if mixer_avail:
- self.list_data.append(
- (self.add_synth_chain, 0, "Add Instrument Chain"))
- self.list_data.append((self.add_audiofx_chain, 0, "Add Audio Chain"))
- self.list_data.append((self.add_midifx_chain, 0, "Add MIDI Chain"))
+ self.list_data.append((self.add_synth_chain, 0,
+ "Add Instrument Chain",
+ ["Create a new chain with a MIDI-controlled synth engine. The chain receives MIDI input and generates audio output.",
+ "midi_instrument.png"]))
+ self.list_data.append((self.add_audiofx_chain, 0,
+ "Add Audio Chain",
+ ["Create a new chain for audio FX processing. The chain receives audio input and generates audio output.",
+ "audio.png"]))
+ self.list_data.append((self.add_midifx_chain, 0,
+ "Add MIDI Chain",
+ ["Create a new chain for MIDI processing. The chain receives MIDI input and generates MIDI output.",
+ "midi_logo.png"]))
if mixer_avail:
- self.list_data.append(
- (self.add_midiaudiofx_chain, 0, "Add MIDI+Audio Chain"))
- self.list_data.append(
- (self.add_generator_chain, 0, "Add Audio Generator Chain"))
- self.list_data.append((self.add_special_chain, 0, "Add Special Chain"))
+ self.list_data.append((self.add_midiaudiofx_chain, 0,
+ "Add MIDI+Audio Chain",
+ ["Create a new chain for combined audio + MIDI processing. The chain receives audio & MIDI input and generates audio & MIDI output. Use it with vocoders, autotune, etc.",
+ "midi_audio.png"]))
+ self.list_data.append((self.add_generator_chain, 0,
+ "Add Audio Generator Chain",
+ ["Create a new chain for audio generation. The chain doesn't receive any input and generates audio output. Internet radio, test signals, etc.",
+ "audio_generator.png"]))
+ self.list_data.append((self.add_special_chain, 0,
+ "Add Special Chain",
+ ["Create a new chain for special processing. The chain receives audio & MIDI input and generates audio & MIDI output. use it for MOD-UI, puredata, etc.",
+ "special_chain.png"]))
self.list_data.append((None, 0, "> REMOVE"))
- self.list_data.append((self.remove_sequences, 0, "Remove Sequences"))
- self.list_data.append((self.remove_chains, 0, "Remove Chains"))
- self.list_data.append((self.remove_all, 0, "Remove All"))
-
+ self.list_data.append((self.remove_sequences, 0,
+ "Remove Sequences",
+ ["Clean all sequencer data while keeping existing chains.",
+ "delete_sequences.png"]))
+ self.list_data.append((self.remove_chains, 0,
+ "Remove Chains",
+ ["Clean all chains while keeping sequencer data.",
+ "delete_chains.png"]))
+ self.list_data.append((self.remove_all, 0,
+ "Remove All",
+ ["Clean all chains and sequencer data. Start from scratch!",
+ "delete_all.png"]))
super().fill_list()
def select_action(self, i, t='S'):
diff --git a/zyngui/zynthian_gui_chain_options.py b/zyngui/zynthian_gui_chain_options.py
index 7cb58d0f7..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,79 +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()
@@ -145,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):
@@ -166,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:
@@ -183,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
@@ -323,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):
@@ -386,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 29f636812..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
@@ -571,6 +580,9 @@ def get_external_storage_dirs(exdpath):
rbpi_headphones = int(os.environ.get('ZYNTHIAN_RBPI_HEADPHONES', 0))
enable_dpm = int(os.environ.get('ZYNTHIAN_DPM', True))
+hotplug_audio_enabled = os.environ.get('ZYNTHIAN_HOTPLUG_AUDIO', False) == "True"
+disabled_audio_in = os.environ.get('ZYNTHIAN_HOTPLUG_AUDIO_DISABLED_IN', "").split(',')
+disabled_audio_out = os.environ.get('ZYNTHIAN_HOTPLUG_AUDIO_DISABLED_OUT', 'headphones,b1,b2').split(',')
# ------------------------------------------------------------------------------
# Networking Options
@@ -589,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
@@ -634,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"
@@ -686,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
@@ -701,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")
@@ -719,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:
@@ -735,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 e48d06b39..b9d57a020 100644
--- a/zyngui/zynthian_gui_control.py
+++ b/zyngui/zynthian_gui_control.py
@@ -5,7 +5,7 @@
#
# Zynthian GUI Instrument-Control Class
#
-# Copyright (C) 2015-2023 Fernando Moyano
+# Copyright (C) 2015-2024 Fernando Moyano
#
# ******************************************************************************
#
@@ -106,6 +106,7 @@ def build_view(self):
self.click_listbox()
elif pending_click_listbox:
self.click_listbox()
+ self.set_mode_control()
return True
def hide(self):
@@ -147,6 +148,8 @@ def backbutton_short_touch_action(self):
def cb_control_mode(self, mode):
self.set_button_status(2, (mode == "select"))
+ if mode == "control":
+ self.set_mode_control()
def configure_processors(self, curproc=None):
if not curproc:
@@ -371,7 +374,7 @@ def get_zcontroller(self, i):
def set_selector_screen(self):
for i in range(0, len(self.zgui_controllers)):
- self.zgui_controllers[i].set_hl(zynthian_gui_config.color_ctrl_bg_off)
+ self.zgui_controllers[i].enable(False)
self.set_selector()
def set_mode_select(self):
@@ -384,7 +387,7 @@ def set_mode_select(self):
fg=zynthian_gui_config.color_ctrl_tx_off)
self.select(self.index)
self.set_select_path()
- zynsigman.send_queued(zynsigman.S_GUI, self.SS_GUI_CONTROL_MODE, mode=self.mode)
+ self.set_button_status(2, True)
def set_mode_control(self):
self.mode = 'control'
@@ -395,9 +398,9 @@ def set_mode_control(self):
selectforeground=zynthian_gui_config.color_ctrl_tx,
fg=zynthian_gui_config.color_ctrl_tx)
for i in range(0, len(self.zgui_controllers)):
- self.zgui_controllers[i].unset_hl()
+ self.zgui_controllers[i].enable()
self.set_select_path()
- zynsigman.send_queued(zynsigman.S_GUI, self.SS_GUI_CONTROL_MODE, mode=self.mode)
+ self.set_button_status(2, False)
def previous_page(self, wrap=False):
i = self.index - 1
@@ -416,9 +419,6 @@ def next_page(self, wrap=False):
self.select(i)
self.click_listbox()
- def select_action(self, i, t='S'):
- self.set_mode_control()
-
def back_action(self):
if self.mode == 'select':
self.set_mode_control()
@@ -458,6 +458,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()
@@ -479,7 +483,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
@@ -492,6 +496,7 @@ def switch_select(self, t):
self.next_page(True)
elif self.mode == 'select':
self.click_listbox()
+ self.set_mode_control()
elif t == 'B':
self.zyngui.cuia_chain_options()
@@ -499,9 +504,9 @@ def switch_select(self, t):
def select(self, index=None, set_zctrl=True):
super().select(index, set_zctrl)
- if self.mode == 'select':
- self.set_controller_screen()
- self.set_selector_screen()
+ #if self.mode == 'select':
+ self.set_controller_screen()
+ #self.set_selector_screen()
def zynpot_cb(self, i, dval):
if self.mode == 'control' and self.zcontrollers:
@@ -663,28 +668,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:
@@ -692,7 +699,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:
@@ -702,10 +709,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
@@ -760,25 +766,9 @@ def show_xy(self, params=None):
# GUI Callback function
# --------------------------------------------------------------------------
- def cb_listbox_release(self, event):
- if self.zyngui.cb_touch_release(event):
- return "break"
-
- now = monotonic()
- dts = now - self.listbox_push_ts
- rdts = now - self.last_release
- self.last_release = now
- if self.swiping:
- self.swipe_nudge(dts)
- else:
- if rdts < 0.03:
- return # Debounce
- cursel = self.listbox.nearest(event.y)
- if self.index != cursel:
- self.select(cursel)
- self.select_listbox(self.get_cursel(), False)
- self.click_listbox()
- return "break"
+ def cb_listbox_click(self, t):
+ # Override listbox click - we don't want short/bold press
+ return
def cb_listbox_motion(self, event):
return super().cb_listbox_motion(event)
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_controller.py b/zyngui/zynthian_gui_controller.py
index 49238f250..b0c56ff65 100644
--- a/zyngui/zynthian_gui_controller.py
+++ b/zyngui/zynthian_gui_controller.py
@@ -79,6 +79,7 @@ def __init__(self, index, parent, zctrl, hidden=False, selcounter=False, graph=z
self.title_width = 1
self.index = index
+ self.enabled = True
# Create Canvas
if not hidden:
@@ -298,6 +299,13 @@ def restore_color_graph(self):
def set_color_readonly(self):
self.itemconfig(self.graph, outline=zynthian_gui_config.color_ctrl_bg_off)
+ def enable(self, enable=True):
+ self.enabled = enable
+ if enable:
+ self.unset_hl()
+ else:
+ self.set_hl(zynthian_gui_config.color_ctrl_bg_off)
+
def set_hl(self, color=zynthian_gui_config.color_hl):
try:
self.color_graph = color
@@ -603,7 +611,7 @@ def zynpot_cb(self, dval):
def nudge(self, dval, fine=False):
if self.preselection is not None:
self.zyngui.screens["control"].zctrl_touch(self.preselection)
- if self.zctrl:
+ elif self.enabled and self.zctrl:
return self.zctrl.nudge(dval, fine=fine)
else:
return False
@@ -621,14 +629,16 @@ def cb_canvas_push(self, event):
#logging.debug(f"CONTROL {self.index} PUSH => {self.canvas_push_ts} ({self.canvas_motion_x0},{self.canvas_motion_y0})")
def cb_canvas_release(self, event):
- if self.canvas_push_ts:
+ if self.canvas_push_ts and self.enabled:
dts = (datetime.now()-self.canvas_push_ts).total_seconds()
self.canvas_push_ts = None
#logging.debug(f"CONTROL {self.index} RELEASE => {dts}, {motion_rate}")
if self.active_motion_axis == 0:
if zynthian_gui_config.enable_touch_controller_switches:
if dts < zynthian_gui_config.zynswitch_bold_seconds:
- self.zyngui.cuia_v5_zynpot_switch((self.index, 'S'))
+ if self.zctrl.is_toggle:
+ self.zctrl.toggle()
+ #self.zyngui.cuia_v5_zynpot_switch((self.index, 'S'))
elif zynthian_gui_config.zynswitch_bold_seconds <= dts < zynthian_gui_config.zynswitch_long_seconds:
self.zyngui.cuia_v5_zynpot_switch((self.index, 'B'))
elif dts >= zynthian_gui_config.zynswitch_long_seconds:
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_engine.py b/zyngui/zynthian_gui_engine.py
index 0ef68d7ed..50349a4b7 100644
--- a/zyngui/zynthian_gui_engine.py
+++ b/zyngui/zynthian_gui_engine.py
@@ -214,13 +214,10 @@ def get_info(self, eng_code=None):
def update_info(self):
eng_info = self.get_info()
quality_stars = "★" * eng_info["QUALITY"]
- self.info_canvas.itemconfigure(
- self.quality_stars_label, text=quality_stars)
+ self.info_canvas.itemconfigure(self.quality_stars_label, text=quality_stars)
complexity_stars = "⚈" * eng_info["COMPLEX"]
- self.info_canvas.itemconfigure(
- self.complexity_stars_label, text=complexity_stars)
- self.info_canvas.itemconfigure(
- self.description_label, text=eng_info["DESCR"])
+ self.info_canvas.itemconfigure(self.complexity_stars_label, text=complexity_stars)
+ self.info_canvas.itemconfigure(self.description_label, text=eng_info["DESCR"])
# self.description_label.delete("1.0", tkinter.END)
# self.description_label.insert("1.0", eng_info["DESCR"])
@@ -243,23 +240,20 @@ def get_engines_by_cat(self):
self.chain_manager.get_engine_info()
self.engine_info = self.chain_manager.engine_info
self.proc_type = self.zyngui.modify_chain_status["type"]
- self.engines_by_cat = self.chain_manager.filtered_engines_by_cat(
- self.proc_type, all=self.show_all)
+ self.engines_by_cat = self.chain_manager.filtered_engines_by_cat(self.proc_type, all=self.show_all)
self.engine_cats = list(self.engines_by_cat.keys())
logging.debug(f"CATEGORIES => {self.engine_cats}")
# self.engines_by_cat = sorted(self.engines_by_cat.items(), key=lambda kv: "!" if kv[0] is None else kv[0])
def recall_context_index(self):
try:
- self.index = self.context_index[self.proc_type +
- "#" + str(self.cat_index)]
+ self.index = self.context_index[self.proc_type + "#" + str(self.cat_index)]
except:
self.index = 0
self.update_context_index()
def update_context_index(self):
- self.context_index[self.proc_type + "#" +
- str(self.cat_index)] = self.index
+ self.context_index[self.proc_type + "#" + str(self.cat_index)] = self.index
def build_view(self):
self.show_all = False
@@ -296,8 +290,7 @@ def fill_list(self):
# Add category header when showing several cats...
if len(cats) > 1:
- self.list_data.append(
- (None, len(self.list_data), "> {}".format(cat)))
+ self.list_data.append((None, len(self.list_data), "> {}".format(cat)))
# Split engines in standalone & plugins
# standalone = []
@@ -315,14 +308,11 @@ def add_engines(engines):
info = infos[eng]
if self.show_all:
if info["ENABLED"]:
- self.list_data.append(
- (eng, i, "\u2612 " + info["TITLE"], info["NAME"]))
+ self.list_data.append((eng, i, "\u2612 " + info["TITLE"], info["NAME"]))
else:
- self.list_data.append(
- (eng, i, "\u2610 " + info["TITLE"], info["NAME"]))
+ self.list_data.append((eng, i, "\u2610 " + info["TITLE"], info["NAME"]))
else:
- self.list_data.append(
- (eng, i, info["TITLE"], info["NAME"]))
+ self.list_data.append((eng, i, info["TITLE"], info["NAME"]))
# if len(standalone) > 0:
# self.list_data.append((None, None, "> Standalone"))
@@ -335,8 +325,7 @@ def add_engines(engines):
# Display help if no engines are enabled ...
if len(self.list_data) == 0:
- self.list_data.append(
- (None, len(self.list_data), "Bold-push to enable some engines"))
+ self.list_data.append((None, len(self.list_data), "Bold-push to enable some engines"))
self.index = 0
self.update_context_index()
@@ -370,10 +359,8 @@ def select_action(self, i, t='S'):
self.zyngui.modify_chain_status["chain_id"], self.zyngui.modify_chain_status["type"])
if self.zyngui.modify_chain_status["type"] == "Audio Effect":
# Check for fader position
- post_fader = "post_fader" in self.zyngui.modify_chain_status and self.zyngui.modify_chain_status[
- "post_fader"]
- fader_pos = self.chain_manager.get_chain(
- self.zyngui.modify_chain_status["chain_id"]).fader_pos
+ post_fader = "post_fader" in self.zyngui.modify_chain_status and self.zyngui.modify_chain_status["post_fader"]
+ fader_pos = self.chain_manager.get_chain(self.zyngui.modify_chain_status["chain_id"]).fader_pos
if post_fader and slot_count > fader_pos or not post_fader and slot_count > 0:
ask_parallel = True
else:
@@ -382,8 +369,9 @@ def select_action(self, i, t='S'):
ask_parallel = slot_count > 0
if ask_parallel:
# Adding to slot with existing processor - choose parallel/series
- self.zyngui.screens['option'].config(
- "Chain Mode", {"Series": False, "Parallel": True}, self.cb_add_parallel)
+ self.zyngui.screens['option'].config("Chain Mode",
+ {"Series": False, "Parallel": True},
+ self.cb_add_parallel)
self.zyngui.show_screen('option')
return
else:
@@ -436,14 +424,18 @@ def set_selector(self, zs_hidden=False):
self.zselector.zctrl.engine = self
if self.zsel2:
self.zsel2.zctrl.set_options({'symbol': "cat_index", 'name': "Category", 'short_name': "Category",
- 'value_min': 0, 'value_max': len(self.engine_cats) - 1, 'value': self.cat_index})
+ 'value_min': 0, 'value_max': len(self.engine_cats) - 1,
+ 'value': self.cat_index})
self.zsel2.config(self.zsel2.zctrl)
self.zsel2.show()
else:
- zsel2_ctrl = zynthian_controller(self, "cat_index", {
- 'name': "Category", 'short_name': "Category", 'value_min': 0, 'value_max': len(self.engine_cats) - 1, 'value': self.cat_index})
+ zsel2_ctrl = zynthian_controller(self, "cat_index",
+ {'name': "Category", 'short_name': "Category", 'value_min': 0,
+ 'value_max': len(self.engine_cats) - 1, 'value': self.cat_index})
self.zsel2 = zynthian_gui_controller(zynthian_gui_config.select_ctrl - 1, self.main_frame,
- zsel2_ctrl, zs_hidden, selcounter=True, orientation=self.layout['ctrl_orientation'])
+ zsel2_ctrl, zs_hidden,
+ selcounter=True,
+ orientation=self.layout['ctrl_orientation'])
if not self.zselector_hidden:
self.zsel2.grid(row=self.layout['ctrl_pos'][2][0],
column=self.layout['ctrl_pos'][2][1], sticky="news", pady=(0, 1))
diff --git a/zyngui/zynthian_gui_help.py b/zyngui/zynthian_gui_help.py
index 9505da68b..41284533a 100644
--- a/zyngui/zynthian_gui_help.py
+++ b/zyngui/zynthian_gui_help.py
@@ -38,11 +38,39 @@ class zynthian_gui_help:
ui_dir = os.environ.get('ZYNTHIAN_UI_DIR', "/zynthian/zynthian-ui")
+ # Scale for touch swipe action after-roll
+ touch_swipe_roll_scale = [1, 0, 1, 1, 2, 2, 2, 4, 4, 4, 4, 4] # 1, 0, 1, 0, 1, 0, 1, 0,
+
def __init__(self):
self.shown = False
self.zyngui = zynthian_gui_config.zyngui
+
+ self.touch_motion_step = int(1.8 * zynthian_gui_config.font_size)
+ self.touch_swipe_speed = 0
+ # Set approx. here to avoid errors. Set accurately when list item selected
+ self.touch_motion_last_dy = 0
+ self.touch_swiping = False
+ self.touch_push_ts = 0
+ 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
+ self.main_frame.on_done_loading(self.done_loading)
+ self.main_frame.bind("", self.cb_touch_push)
+ self.main_frame.bind("", self.cb_touch_release)
+ self.main_frame.bind("", self.cb_touch_motion)
+
+ def done_loading(self):
+ self.zyngui.show_screen("help")
def load_file(self, fpath):
if os.path.isfile(fpath):
@@ -66,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:
@@ -79,7 +108,67 @@ def refresh_loading(self):
def switch_select(self, t='S'):
pass
- def cb_push(self, event):
- self.zyngui.cuia_back()
+ def arrow_up(self):
+ self.main_frame.yview_scroll(-4, "units")
+
+ def arrow_down(self):
+ self.main_frame.yview_scroll(4, "units")
+
+ # --------------------------------------------------------------------------
+ # Keyboard & Mouse/Touch Callbacks
+ # --------------------------------------------------------------------------
+
+ def cb_touch_push(self, event):
+ if self.zyngui.cb_touch(event):
+ return "break"
+ self.touch_push_ts = event.time # Timestamp of initial touch
+ # logging.debug("LISTBOX PUSH => %s" % (self.listbox_push_ts))
+ self.touch_y0 = event.y # Touch y-coord of initial touch
+ self.touch_x0 = event.x # Touch x-coord of initial touch
+ # True if swipe action in progress (disables press action)
+ self.touch_swiping = False
+ self.touch_swipe_speed = 0 # Speed of swipe used for rolling after release
+ return "break" # Don't select entry on push
+
+ def cb_touch_motion(self, event):
+ dy = self.touch_y0 - event.y
+ offset_y = int(dy / self.touch_motion_step)
+ if offset_y:
+ self.touch_swiping = True
+ self.main_frame.yview_scroll(offset_y, "units")
+ self.touch_swipe_dir = abs(dy) // dy
+ self.touch_y0 = event.y + self.touch_swipe_dir * (abs(dy) % self.touch_motion_step)
+ # Use time delta between last motion and release to determine speed of swipe
+ self.touch_push_ts = event.time
+
+ def cb_touch_release(self, event):
+ if self.zyngui.cb_touch_release(event):
+ return "break"
+ dts = (event.time - self.touch_push_ts)/1000
+ rdts = event.time - self.touch_last_release_ts
+ self.touch_last_release_ts = event.time
+ if self.touch_swiping:
+ self.touch_swipe_nudge(dts)
+ else:
+ if rdts < 30:
+ return # Debounce
+ if dts < zynthian_gui_config.zynswitch_bold_seconds:
+ pass
+ elif zynthian_gui_config.zynswitch_bold_seconds <= dts < zynthian_gui_config.zynswitch_long_seconds:
+ self.zyngui.cuia_back()
+
+ def touch_swipe_nudge(self, dts):
+ self.touch_swipe_speed = int(len(self.touch_swipe_roll_scale) - ((dts - 0.02) / 0.06) * len(self.touch_swipe_roll_scale))
+ self.touch_swipe_speed = min(
+ self.touch_swipe_speed, len(self.touch_swipe_roll_scale) - 1)
+ self.touch_swipe_speed = max(self.touch_swipe_speed, 0)
+
+ def swipe_update(self):
+ if self.touch_swipe_speed > 0:
+ self.touch_swipe_speed -= 1
+ self.main_frame.yview_scroll(self.touch_swipe_dir * self.touch_swipe_roll_scale[self.touch_swipe_speed], "units")
+
+ def plot_zctrls(self):
+ self.swipe_update()
# -------------------------------------------------------------------------------
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..a0e267b87 100644
--- a/zyngui/zynthian_gui_keyboard.py
+++ b/zyngui/zynthian_gui_keyboard.py
@@ -56,35 +56,34 @@ def __init__(self):
self.selected_button = 45 # Index of highlighted button
self.mode = OSK_QWERTY # Keyboard layout mode
self.ctrl_order = zynthian_gui_config.layout['ctrl_order']
+ self.last_key = None # Last pressed key
# 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 +137,9 @@ def refresh_keys(self):
elif self.alt:
if self.shift:
self.keys = ['\\', '|', '@', '/', '*', '=', '\"', '\'', '?', '¡',
- 'Ä', 'Ë', 'Ï', 'Ö', 'Ü', 'Â', 'Ê', 'Î', 'Ô', 'Û',
- 'Ñ', 'Ç', 'Ẅ', 'Ŵ', 'Ĉ', 'Ÿ', 'Ŷ', 'Ŝ', 'Ĝ', 'Ḧ',
- 'Ĥ', 'Ĵ', 'Ẑ', 'Ẍ', '{', '}', '~', '^', ':', '_']
+ 'Ä', 'Ë', 'Ï', 'Ö', 'Ü', 'Â', 'Ê', 'Î', 'Ô', 'Û',
+ 'Ñ', 'Ç', 'Ẅ', 'Ŵ', 'Ĉ', 'Ÿ', 'Ŷ', 'Ŝ', 'Ĝ', 'Ḧ',
+ 'Ĥ', 'Ĵ', 'Ẑ', 'Ẍ', '{', '}', '~', '^', ':', '_']
else:
self.keys = ['á', 'é', 'í', 'ó', 'ú', 'à', 'è', 'ì', 'ò', 'ù',
'ä', 'ë', 'ï', 'ö', 'ü', 'â', 'ê', 'î', 'ô', 'û',
@@ -159,8 +158,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 +171,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])
@@ -186,26 +190,31 @@ def add_button(self, label, col, row, colspan=1):
# Function to handle bold touchscreen press and hold
def bold_press(self):
- self.deferred_key_press(self.btn_delete, True)
+ self.deferred_key_press(self.last_key, True)
+ if self.last_key != self.btn_delete:
+ self.hold_timer = Timer(0.2, self.bold_press)
+ self.hold_timer.start()
+
# 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(':')
- key = int(index)
- if key == self.btn_delete:
+ self.last_key = int(index)
+ if self.last_key not in [self.btn_enter, self.btn_cancel]:
+ self.deferred_key_press(self.last_key)
self.hold_timer = Timer(0.8, self.bold_press)
self.hold_timer.start()
- self.deferred_key_press(key)
# Function to handle key release
# event: Mouse event
def on_key_release(self, event=None):
self.hold_timer.cancel()
+ if self.last_key in [self.btn_enter, self.btn_cancel]:
+ self.deferred_key_press(self.last_key)
def deferred_key_press(self, key, bold=False):
self.keypress_queue.append((key, bold))
@@ -239,11 +248,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 +265,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 +279,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 +309,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_main_menu.py b/zyngui/zynthian_gui_main_menu.py
index b7a6e57f9..4be6bcad4 100644
--- a/zyngui/zynthian_gui_main_menu.py
+++ b/zyngui/zynthian_gui_main_menu.py
@@ -26,18 +26,17 @@
import logging
# 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 App Selection GUI Class
# ------------------------------------------------------------------------------
-class zynthian_gui_main_menu(zynthian_gui_selector):
+class zynthian_gui_main_menu(zynthian_gui_selector_info):
def __init__(self):
- super().__init__('Menu', True)
+ super().__init__('Menu')
def fill_list(self):
self.list_data = []
@@ -50,37 +49,61 @@ def fill_list(self):
mixer_avail = False
self.list_data.append((None, 0, "> ADD CHAIN"))
if mixer_avail:
- self.list_data.append(
- (self.add_synth_chain, 0, "Add Instrument Chain"))
- self.list_data.append((self.add_audiofx_chain, 0, "Add Audio Chain"))
- self.list_data.append((self.add_midifx_chain, 0, "Add MIDI Chain"))
+ self.list_data.append((self.add_synth_chain, 0,
+ "Add Instrument Chain",
+ ["Create a new chain with a MIDI-controlled synth engine. The chain receives MIDI input and generates audio output.",
+ "midi_instrument.png"]))
+ self.list_data.append((self.add_audiofx_chain, 0,
+ "Add Audio Chain",
+ ["Create a new chain for audio FX processing. The chain receives audio input and generates audio output.",
+ "audio.png"]))
+ self.list_data.append((self.add_midifx_chain, 0,
+ "Add MIDI Chain",
+ ["Create a new chain for MIDI processing. The chain receives MIDI input and generates MIDI output.",
+ "midi_logo.png"]))
if mixer_avail:
- self.list_data.append(
- (self.add_midiaudiofx_chain, 0, "Add MIDI+Audio Chain"))
- self.list_data.append(
- (self.add_generator_chain, 0, "Add Audio Generator Chain"))
- self.list_data.append((self.add_special_chain, 0, "Add Special Chain"))
+ self.list_data.append((self.add_midiaudiofx_chain, 0,
+ "Add MIDI+Audio Chain",
+ ["Create a new chain for combined audio + MIDI processing. The chain receives audio & MIDI input and generates audio & MIDI output. Use it with vocoders, autotune, etc.",
+ "midi_audio.png"]))
+ self.list_data.append((self.add_generator_chain, 0,
+ "Add Audio Generator Chain",
+ ["Create a new chain for audio generation. The chain doesn't receive any input and generates audio output. Internet radio, test signals, etc.",
+ "audio_generator.png"]))
+ self.list_data.append((self.add_special_chain, 0,
+ "Add Special Chain",
+ ["Create a new chain for special processing. The chain receives audio & MIDI input and generates audio & MIDI output. use it for MOD-UI, puredata, etc.",
+ "special_chain.png"]))
self.list_data.append((None, 0, "> REMOVE"))
- self.list_data.append((self.remove_sequences, 0, "Remove Sequences"))
- self.list_data.append((self.remove_chains, 0, "Remove Chains"))
- self.list_data.append((self.remove_all, 0, "Remove All"))
+ self.list_data.append((self.remove_sequences, 0,
+ "Remove Sequences",
+ ["Clean all sequencer data while keeping existing chains.",
+ "delete_sequences.png"]))
+ self.list_data.append((self.remove_chains, 0,
+ "Remove Chains",
+ ["Clean all chains while keeping sequencer data.",
+ "delete_chains.png"]))
+ self.list_data.append((self.remove_all, 0,
+ "Remove All",
+ ["Clean all chains and sequencer data. Start from scratch!",
+ "delete_all.png"]))
# Add list of Apps
self.list_data.append((None, 0, "> MAIN"))
- self.list_data.append((self.snapshots, 0, "Snapshots"))
- self.list_data.append((self.step_sequencer, 0, "Sequencer"))
- self.list_data.append((self.audio_recorder, 0, "Audio Recorder"))
- self.list_data.append((self.midi_recorder, 0, "MIDI Recorder"))
- self.list_data.append((self.tempo_settings, 0, "Tempo Settings"))
- self.list_data.append((self.audio_levels, 0, "Audio Levels"))
- self.list_data.append((self.audio_mixer_learn, 0, "Mixer Learn"))
+ self.list_data.append((self.snapshots, 0, "Snapshots", ["Show snapshots management menu.", "snapshot.png"]))
+ self.list_data.append((self.step_sequencer, 0, "Sequencer", ["Show sequencer's zynpad view.", "sequencer.png"]))
+ self.list_data.append((self.audio_recorder, 0, "Audio Recorder", ["Show audio recorder/player.", "audio_recorder.png"]))
+ self.list_data.append((self.midi_recorder, 0, "MIDI Recorder", ["Show SMF recorder/player.", "midi_recorder.png"]))
+ self.list_data.append((self.tempo_settings, 0, "Tempo Settings", ["Show tempo & sync options.", "metronome.png"]))
+ self.list_data.append((self.audio_levels, 0, "Audio Levels", ["Show audio levels view.", "meters.png"]))
+ self.list_data.append((self.audio_mixer_learn, 0, "Mixer Learn", ["Enter mixer's MIDI learn mode", "mixer.png"]))
# Add list of System / configuration views
self.list_data.append((None, 0, "> SYSTEM"))
- self.list_data.append((self.admin, 0, "Admin"))
+ self.list_data.append((self.admin, 0, "Admin", ["Show admin menu.", "settings.png"]))
self.list_data.append(
- (self.all_sounds_off, 0, "PANIC! All Sounds Off"))
+ (self.all_sounds_off, 0, "PANIC! All Sounds Off", ["Stop all notes and sequences.", "panic.png"]))
super().fill_list()
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_midi_recorder.py b/zyngui/zynthian_gui_midi_recorder.py
index df864754d..10f993d00 100644
--- a/zyngui/zynthian_gui_midi_recorder.py
+++ b/zyngui/zynthian_gui_midi_recorder.py
@@ -5,7 +5,7 @@
#
# Zynthian GUI MIDI Recorder Class
#
-# Copyright (C) 2015-2023 Fernando Moyano
+# Copyright (C) 2015-2024 Fernando Moyano
#
# ******************************************************************************
#
@@ -31,7 +31,7 @@
# Zynthian specific modules
import zynconf
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
from zyngui.zynthian_gui_controller import zynthian_gui_controller
# Python wrapper for zynsmf (ensures initialised and wraps load() function)
from zynlibs.zynsmf import zynsmf
@@ -42,7 +42,7 @@
# ------------------------------------------------------------------------------
-class zynthian_gui_midi_recorder(zynthian_gui_selector):
+class zynthian_gui_midi_recorder(zynthian_gui_selector_info):
def __init__(self):
self.capture_dir_sdc = os.environ.get(
@@ -54,7 +54,7 @@ def __init__(self):
self.smf_timer = None # 1s timer used to check end of SMF playback
- super().__init__('MIDI Recorder', True)
+ super().__init__('MIDI Recorder')
self.bpm_zgui_ctrl = None
@@ -92,7 +92,7 @@ def fill_list(self):
flist += self.get_filelist(exd, "USB")
i = 1
for finfo in sorted(flist, key=lambda d: d['mtime'], reverse=True):
- self.list_data.append((finfo['fpath'], i, finfo['title']))
+ self.list_data.append((finfo['fpath'], i, finfo['title'], ["Play MIDI file.\nBold select to show more options.", None]))
i += 1
super().fill_list()
@@ -160,11 +160,11 @@ def update_status_playback(self):
def update_status_recording(self, fill=False):
if self.list_data:
if self.zyngui.state_manager.status_midi_recorder:
- self.list_data[0] = ("STOP_RECORDING", 0,
- "■ Stop MIDI Recording")
+ self.list_data[0] = (("STOP_RECORDING", 0,
+ "■ Stop MIDI Recording", ["Toggle recording to MIDI file.", None]))
else:
- self.list_data[0] = ("START_RECORDING", 0,
- "⬤ Start MIDI Recording")
+ self.list_data[0] = (("START_RECORDING", 0,
+ "⬤ Start MIDI Recording", ["Toggle recording to MIDI file", None]))
if fill:
self.listbox.delete(0)
self.listbox.insert(0, self.list_data[0][2])
@@ -173,10 +173,10 @@ def update_status_recording(self, fill=False):
def update_status_loop(self, fill=False):
if self.list_data:
if zynthian_gui_config.midi_play_loop:
- self.list_data[1] = ("LOOP", 0, "\u2612 Loop Play")
+ self.list_data[1] = (("LOOP", 0, "\u2612 Loop Play", ["Toggle loop playback.", None]))
libsmf.setLoop(True)
else:
- self.list_data[1] = ("LOOP", 0, "\u2610 Loop Play")
+ self.list_data[1] = (("LOOP", 0, "\u2610 Loop Play", ["Toggle loop playback.", None]))
libsmf.setLoop(False)
if fill:
self.listbox.delete(1)
@@ -213,8 +213,8 @@ def show_smf_options(self):
smf = self.list_data[self.index]
smf_fname = smf[2]
options = {}
- options["Rename"] = smf
- options["Delete"] = smf
+ options["Rename"] = [smf, ["Rename MIDI file", None]]
+ options["Delete"] = [smf, ["Delete MIDI file", None]]
self.zyngui.screens['option'].config(
f"MIDI file {smf_fname}", options, self.smf_options_cb)
self.zyngui.show_screen('option')
@@ -230,7 +230,8 @@ def toggle_menu(self):
def smf_options_cb(self, option, smf):
if option == "Rename":
- self.zyngui.show_keyboard(self.rename_smf, smf[2])
+ name = os.path.basename(smf[0])[:-4]
+ self.zyngui.show_keyboard(self.rename_smf, name)
elif option == "Delete":
self.delete_smf(smf)
diff --git a/zyngui/zynthian_gui_mixer.py b/zyngui/zynthian_gui_mixer.py
index 53d913998..ba840c6a9 100644
--- a/zyngui/zynthian_gui_mixer.py
+++ b/zyngui/zynthian_gui_mixer.py
@@ -198,6 +198,8 @@ def __init__(self, parent, x, y, width, height):
self.fader_bg), "press", self.on_fader_press)
self.parent.zyngui.multitouch.tag_bind(self.parent.main_canvas, "fader:%s" % (
self.fader_bg), "motion", self.on_fader_motion)
+ self.parent.zyngui.multitouch.tag_bind(self.parent.main_canvas, "fader:%s" % (
+ self.fader_bg), "motion", self.on_fader_motion)
self.parent.main_canvas.tag_bind(
f"fader:{self.fader_bg}", "", self.on_fader_press)
self.parent.main_canvas.tag_bind(
@@ -338,7 +340,7 @@ def draw_fader(self):
label_parts = ["No info"]
for i, label in enumerate(label_parts):
- # self.parent.main_canvas.itemconfig(self.fader_text, text=label, state=tkinter.NORMAL)
+ self.parent.main_canvas.itemconfig(self.fader_text, text=label, state=tkinter.NORMAL)
bounds = self.parent.main_canvas.bbox(self.fader_text)
if bounds[1] < self.fader_text_limit:
while bounds and bounds[1] < self.fader_text_limit:
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_preset.py b/zyngui/zynthian_gui_preset.py
index ce24b609c..09bb80fcf 100644
--- a/zyngui/zynthian_gui_preset.py
+++ b/zyngui/zynthian_gui_preset.py
@@ -78,19 +78,19 @@ def show_preset_options(self):
preset[2] = preset[2][1:]
preset_name = preset[2]
if self.processor.engine.is_preset_fav(preset):
- options["\u2612 Favourite"] = preset
+ options["\u2612 Favourite"] = [preset, ["Remove from favorites list", None]]
else:
- options["\u2610 Favourite"] = preset
+ options["\u2610 Favourite"] = [preset, ["Add to favorites list", None]]
if engine.is_preset_user(preset):
if hasattr(engine, "rename_preset"):
- options["Rename"] = preset
+ options["Rename"] = [preset, ["Rename preset", None]]
if hasattr(engine, "delete_preset"):
- options["Delete"] = preset
+ options["Delete"] = [preset, ["Delete preset", None]]
global_options = {}
if hasattr(engine, "save_preset"):
- global_options["Save new preset"] = True
+ global_options["Save new preset"] = [True, ["Save as new preset", None]]
if self.processor.eng_code.startswith("JV/"):
- global_options["Scan for new presets"] = True
+ global_options["Scan for new presets"] = [True, ["Scan new presets, e.g. added via webconf", None]]
if global_options:
options["Global"] = None
options.update(global_options)
diff --git a/zyngui/zynthian_gui_save_preset.py b/zyngui/zynthian_gui_save_preset.py
index 68cfd0cf0..1647a6a46 100644
--- a/zyngui/zynthian_gui_save_preset.py
+++ b/zyngui/zynthian_gui_save_preset.py
@@ -56,8 +56,7 @@ def save_preset(self):
else:
options["None"] = ["", None, "None", None]
if len(options) > 0:
- self.zyngui.screens['option'].config(
- "Select bank...", options, self.save_preset_select_bank_cb)
+ self.zyngui.screens['option'].config("Select bank...", options, self.save_preset_select_bank_cb)
self.zyngui.show_screen('option')
self.zyngui.screens['option'].select(index)
else:
@@ -66,8 +65,7 @@ def save_preset(self):
def save_preset_select_bank_cb(self, bank_name, bank_info):
self.save_preset_bank_info = bank_info
if bank_info == "NEW_BANK":
- self.zyngui.show_keyboard(
- self.save_preset_select_name_cb, "NewBank")
+ self.zyngui.show_keyboard(self.save_preset_select_name_cb, "NewBank")
else:
self.save_preset_select_name_cb()
@@ -76,50 +74,43 @@ def save_preset_select_name_cb(self, create_bank_name=None):
create_bank_name = create_bank_name.strip()
self.save_preset_create_bank_name = create_bank_name
if self.processor.preset_name:
- self.zyngui.show_keyboard(
- self.save_preset_cb, self.processor.preset_name + " COPY")
+ self.zyngui.show_keyboard(self.save_preset_cb, self.processor.preset_name + " COPY")
else:
self.zyngui.show_keyboard(self.save_preset_cb, "New Preset")
def save_preset_cb(self, preset_name):
preset_name = preset_name.strip()
- # If must create new bank, calculate URID
- if self.save_preset_create_bank_name:
- create_bank_urid = self.processor.engine.get_user_bank_urid(
- self.save_preset_create_bank_name)
- self.save_preset_bank_info = (
- create_bank_urid, None, self.save_preset_create_bank_name, None)
- if self.processor.engine.preset_exists(self.save_preset_bank_info, preset_name):
- self.zyngui.show_confirm("Do you want to overwrite preset '{}'?".format(
- preset_name), self.do_save_preset, preset_name)
- else:
- self.do_save_preset(preset_name)
+ if preset_name:
+ # If must create new bank, calculate URID
+ if self.save_preset_create_bank_name:
+ create_bank_urid = self.processor.engine.get_user_bank_urid(self.save_preset_create_bank_name)
+ self.save_preset_bank_info = (create_bank_urid, None, self.save_preset_create_bank_name, None)
+ if self.processor.engine.preset_exists(self.save_preset_bank_info, preset_name):
+ self.zyngui.show_confirm(f"Do you want to overwrite preset '{preset_name}'?",
+ self.do_save_preset, preset_name)
+ else:
+ self.do_save_preset(preset_name)
def do_save_preset(self, preset_name):
preset_name = preset_name.strip()
- self.zyngui.state_manager.start_busy(
- "Save Preset", f"Saving preset {preset_name}")
+ self.zyngui.state_manager.start_busy("Save Preset", f"Saving preset {preset_name}")
try:
# Save preset
- preset_uri = self.processor.engine.save_preset(
- self.save_preset_bank_info, preset_name)
+ preset_uri = self.processor.engine.save_preset(self.save_preset_bank_info, preset_name)
if preset_uri:
# If must create new bank, do it!
if self.save_preset_create_bank_name:
- self.processor.engine.create_user_bank(
- self.save_preset_create_bank_name)
- logging.info("Created new bank '{}' => {}".format(
- self.save_preset_create_bank_name, self.save_preset_bank_info[0]))
+ self.processor.engine.create_user_bank(self.save_preset_create_bank_name)
+ logging.info(f"Created new bank '{self.save_preset_create_bank_name}' => {self.save_preset_bank_info[0]}")
if self.save_preset_bank_info:
- self.processor.set_bank_by_id(
- self.save_preset_bank_info[0])
+ self.processor.set_bank_by_id(self.save_preset_bank_info[0])
self.processor.load_preset_list()
self.processor.set_preset_by_id(preset_uri)
+ self.index = self.processor.get_preset_index()
else:
- logging.error("Can't save preset '{}' to bank '{}'".format(
- preset_name, self.save_preset_bank_info[2]))
+ logging.error(f"Can't save preset '{preset_name}' to bank '{self.save_preset_bank_info[2]}'")
except Exception as e:
logging.error(e)
diff --git a/zyngui/zynthian_gui_selector.py b/zyngui/zynthian_gui_selector.py
index f2a3f9de6..8fee5e762 100644
--- a/zyngui/zynthian_gui_selector.py
+++ b/zyngui/zynthian_gui_selector.py
@@ -95,8 +95,7 @@ def __init__(self, selcap='Select', wide=False, loading_anim=True, info=True):
for i in range(self.layout['rows']):
self.main_frame.rowconfigure(i, weight=1, uniform='ctrl_row')
self.main_frame.columnconfigure(self.layout['list_pos'][1], weight=3)
- self.main_frame.columnconfigure(
- self.layout['list_pos'][1] + 1, weight=1)
+ self.main_frame.columnconfigure(self.layout['list_pos'][1] + 1, weight=1)
# Row 4 expands to fill unused space
# self.main_frame.rowconfigure(4, weight=1) #TODO: Validate row 4 is still required after chagnes to layout implementation (BW)
@@ -113,12 +112,15 @@ def __init__(self, selcap='Select', wide=False, loading_anim=True, info=True):
pady = (0, 1)
else:
pady = (0, 0)
- self.listbox.grid(row=self.layout['list_pos'][0], column=self.layout['list_pos']
- [1], rowspan=self.layout['rows'], padx=padx, pady=pady, sticky="news")
+ self.listbox.grid(row=self.layout['list_pos'][0],
+ column=self.layout['list_pos'][1],
+ rowspan=self.layout['rows'],
+ padx=padx,
+ pady=pady,
+ sticky="news")
# Bind listbox events
self.listbox_push_ts = 0
- self.last_release = 0
self.listbox.bind("", self.cb_listbox_push)
self.listbox.bind("", self.cb_listbox_release)
self.listbox.bind("", self.cb_listbox_motion)
@@ -135,17 +137,16 @@ def __init__(self, selcap='Select', wide=False, loading_anim=True, info=True):
highlightthickness=0,
bg=zynthian_gui_config.color_bg)
# Position at top of column containing selector
- self.loading_canvas.grid(
- row=0, column=self.layout['list_pos'][1] + 1, rowspan=2, sticky="news")
+ self.loading_canvas.grid(row=0, column=self.layout['list_pos'][1] + 1, rowspan=2, sticky="news")
self.loading_push_ts = None
self.loading_canvas.bind("", self.cb_loading_push)
- self.loading_canvas.bind(
- "", self.cb_loading_release)
+ self.loading_canvas.bind("", self.cb_loading_release)
# Setup Loading Logo Animation
self.loading_index = 0
- self.loading_item = self.loading_canvas.create_image(
- 3, 3, image=zynthian_gui_config.loading_imgs[0], anchor=tkinter.NW)
+ self.loading_item = self.loading_canvas.create_image(3, 3,
+ image=zynthian_gui_config.loading_imgs[0],
+ anchor=tkinter.NW)
else:
self.loading_canvas = None
self.loading_index = 0
@@ -158,8 +159,7 @@ def __init__(self, selcap='Select', wide=False, loading_anim=True, info=True):
def update_layout(self):
super().update_layout()
- ctrl_width = self.width * \
- self.layout['ctrl_width'] * self.sidebar_shown
+ ctrl_width = self.width * self.layout['ctrl_width'] * self.sidebar_shown
if self.layout['columns'] == 2:
lb_width = int(self.width - ctrl_width)
lb_weight = 3
@@ -167,10 +167,8 @@ def update_layout(self):
lb_width = int(self.width - 2 * ctrl_width)
lb_weight = 2
ctrl_width = int(ctrl_width)
- self.main_frame.columnconfigure(
- self.layout['list_pos'][1], minsize=lb_width, weight=lb_weight)
- self.main_frame.columnconfigure(
- self.layout['list_pos'][1] + 1, minsize=ctrl_width, weight=self.sidebar_shown)
+ self.main_frame.columnconfigure(self.layout['list_pos'][1], minsize=lb_width, weight=lb_weight)
+ self.main_frame.columnconfigure(self.layout['list_pos'][1] + 1, minsize=ctrl_width, weight=self.sidebar_shown)
def build_view(self):
self.fill_list()
@@ -199,8 +197,8 @@ def refresh_loading(self):
self.loading_index += 1
if self.loading_index > len(zynthian_gui_config.loading_imgs) + 1:
self.loading_index = 0
- self.loading_canvas.itemconfig(
- self.loading_item, image=zynthian_gui_config.loading_imgs[self.loading_index])
+ self.loading_canvas.itemconfig(self.loading_item,
+ image=zynthian_gui_config.loading_imgs[self.loading_index])
else:
self.reset_loading()
except:
@@ -209,8 +207,7 @@ def refresh_loading(self):
def reset_loading(self, force=False):
if self.loading_canvas and (self.loading_index > 0 or force):
self.loading_index = 0
- self.loading_canvas.itemconfig(
- self.loading_item, image=zynthian_gui_config.loading_imgs[0])
+ self.loading_canvas.itemconfig(self.loading_item, image=zynthian_gui_config.loading_imgs[0])
def fill_listbox(self):
self.listbox.delete(0, tkinter.END)
@@ -222,8 +219,9 @@ def fill_listbox(self):
label += item[5]
self.listbox.insert(tkinter.END, label)
if item[0] is None:
- self.listbox.itemconfig(
- i, {'bg': zynthian_gui_config.color_panel_hl, 'fg': zynthian_gui_config.color_tx_off})
+ self.listbox.itemconfig(i,
+ {'bg': zynthian_gui_config.color_panel_hl,
+ 'fg': zynthian_gui_config.color_tx_off})
# Can't find any engine currently using this "format" feature:
# last_param = item[len(item) - 1]
# if isinstance(last_param, dict) and 'format' in last_param:
@@ -234,18 +232,25 @@ def set_selector(self, zs_hidden=True):
val = self.get_counter_from_index(self.index)
vmax = self.get_counter_from_index(len(self.list_data) - 1)
if self.zselector:
- self.zselector.zctrl.set_options({'symbol': self.selector_caption, 'name': self.selector_caption,
- 'short_name': self.selector_caption, 'value_min': 0, 'value_max': vmax, 'value': val})
+ self.zselector.zctrl.set_options({'symbol': self.selector_caption,
+ 'name': self.selector_caption,
+ 'short_name': self.selector_caption,
+ 'value_min': 0,
+ 'value_max': vmax,
+ 'value': val})
self.zselector.config(self.zselector.zctrl)
self.zselector.show()
else:
zselector_ctrl = zynthian_controller(None, self.selector_caption, {
- 'value_min': 0, 'value_max': vmax, 'value': val})
+ 'value_min': 0,
+ 'value_max': vmax,
+ 'value': val})
self.zselector = zynthian_gui_controller(zynthian_gui_config.select_ctrl, self.main_frame,
- zselector_ctrl, zs_hidden, selcounter=True, orientation=self.layout['ctrl_orientation'])
+ zselector_ctrl, zs_hidden,
+ selcounter=True,
+ orientation=self.layout['ctrl_orientation'])
if not self.zselector_hidden:
- self.zselector.grid(
- row=self.layout['ctrl_pos'][3][0], column=self.layout['ctrl_pos'][3][1], sticky="news")
+ self.zselector.grid(row=self.layout['ctrl_pos'][3][0], column=self.layout['ctrl_pos'][3][1], sticky="news")
def plot_zctrls(self):
self.swipe_update()
@@ -256,8 +261,7 @@ def plot_zctrls(self):
self.zselector.plot_value()
def swipe_nudge(self, dts):
- self.swipe_speed = int(len(self.swipe_roll_scale) -
- ((dts - 0.02) / 0.06) * len(self.swipe_roll_scale))
+ self.swipe_speed = int(len(self.swipe_roll_scale) - ((dts - 0.02) / 0.06) * len(self.swipe_roll_scale))
self.swipe_speed = min(
self.swipe_speed, len(self.swipe_roll_scale) - 1)
self.swipe_speed = max(self.swipe_speed, 0)
@@ -265,8 +269,7 @@ def swipe_nudge(self, dts):
def swipe_update(self):
if self.swipe_speed > 0:
self.swipe_speed -= 1
- self.listbox.yview_scroll(
- self.swipe_dir * self.swipe_roll_scale[self.swipe_speed], tkinter.UNITS)
+ self.listbox.yview_scroll(self.swipe_dir * self.swipe_roll_scale[self.swipe_speed], tkinter.UNITS)
def fill_list(self):
self.fill_listbox()
@@ -451,8 +454,7 @@ def cb_listbox_motion(self, event):
self.swiping = True
self.listbox.yview_scroll(offset_y, tkinter.UNITS)
self.swipe_dir = abs(dy) // dy
- self.listbox_y0 = event.y + self.swipe_dir * \
- (abs(dy) % self.list_entry_height)
+ self.listbox_y0 = event.y + self.swipe_dir * (abs(dy) % self.list_entry_height)
# Use time delta between last motion and release to determine speed of swipe
self.listbox_push_ts = event.time
@@ -472,9 +474,9 @@ def cb_listbox_release(self, event):
if self.index != cursel:
self.select(cursel)
if dts < zynthian_gui_config.zynswitch_bold_seconds:
- self.zyngui.zynswitch_defered('S', 3)
+ self.cb_listbox_click('S')
elif zynthian_gui_config.zynswitch_bold_seconds <= dts < zynthian_gui_config.zynswitch_long_seconds:
- self.zyngui.zynswitch_defered('B', 3)
+ self.cb_listbox_click('B')
def cb_listbox_wheel(self, event):
if event.num == 5 or event.delta == -120:
@@ -483,6 +485,9 @@ def cb_listbox_wheel(self, event):
self.select(self.index - 1)
return "break" # Consume event to stop scrolling of listbox
+ def cb_listbox_click(self, t):
+ self.zyngui.zynswitch_defered(t, 3)
+
def cb_loading_push(self, event):
self.loading_push_ts = event.time
# logging.debug("LOADING PUSH => %s" % self.loading_push_ts)
diff --git a/zyngui/zynthian_gui_selector_info.py b/zyngui/zynthian_gui_selector_info.py
new file mode 100644
index 000000000..2eb9f816c
--- /dev/null
+++ b/zyngui/zynthian_gui_selector_info.py
@@ -0,0 +1,153 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# ******************************************************************************
+# ZYNTHIAN PROJECT: Zynthian GUI
+#
+# Zynthian GUI Selector with Extended Info Class
+#
+# Copyright (C) 2015-2024 Fernando Moyano
+#
+# ******************************************************************************
+#
+# 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 tkinter
+import logging
+from PIL import Image, ImageTk
+
+# Zynthian specific modules
+from zyngui import zynthian_gui_config
+from zyngui.zynthian_gui_selector import zynthian_gui_selector
+
+# ------------------------------------------------------------------------------
+# Zynthian Selector with Extended Info GUI Class
+# ------------------------------------------------------------------------------
+
+
+class zynthian_gui_selector_info(zynthian_gui_selector):
+
+ def __init__(self, selcap='Select'):
+ # Custom layout for GUI engine
+ self.layout = {
+ 'name': 'gui_selector_ext',
+ 'columns': 2,
+ 'rows': 4,
+ 'ctrl_pos': [
+ (0, 1),
+ (1, 1),
+ (2, 1),
+ (3, 1)
+ ],
+ 'list_pos': (0, 0),
+ 'ctrl_orientation': 'horizontal',
+ 'ctrl_order': (0, 1, 2, 3),
+ 'ctrl_width': 0.25
+ }
+ self.icon_canvas = None
+ self.info_canvas = None
+ super().__init__(selcap, True, False, True)
+
+ # Canvas for extended info image
+ self.icon_canvas = tkinter.Canvas(self.main_frame,
+ width=1, # zynthian_gui_config.fw2, #self.width // 4 - 2,
+ height=1, # zynthian_gui_config.fh2, #self.height // 2 - 1,
+ bd=0,
+ highlightthickness=0,
+ bg=zynthian_gui_config.color_bg)
+ self.icon_canvas.bind('', self.cb_info_press)
+ # Position at top of column containing selector
+ self.icon_canvas.grid(row=0, column=self.layout['list_pos'][1] + 1, rowspan=2, sticky="news")
+
+ # Canvas for extended info text
+ self.info_canvas = tkinter.Canvas(
+ self.main_frame,
+ width=1, # zynthian_gui_config.fw2, #self.width // 4 - 2,
+ height=1, # zynthian_gui_config.fh2, #self.height // 2 - 1,
+ bd=0,
+ highlightthickness=0,
+ bg=zynthian_gui_config.color_bg)
+ self.info_canvas.bind('', self.cb_info_press)
+ # Position at top of column containing selector
+ self.info_canvas.grid(row=2, column=self.layout['list_pos'][1] + 1, rowspan=2, sticky="news")
+
+ # Info layout geometry
+ self.side_width = int(self.layout['ctrl_width'] * self.width)
+
+ # Info icon layout
+ self.icons = {}
+ self.icon_size = (self.side_width, self.side_width)
+ self.icon_image = self.icon_canvas.create_image(self.side_width // 2, 0, anchor="n")
+
+ # Info text layout
+ info_fs = min(int(0.8 * zynthian_gui_config.font_size), self.side_width // 16)
+ xpos = int(0.8 * info_fs)
+ ypos = int(-0.3 * info_fs)
+ self.description_label = self.info_canvas.create_text(
+ xpos, ypos,
+ anchor=tkinter.NW,
+ justify=tkinter.LEFT,
+ width=self.side_width - xpos,
+ text="",
+ # font=(zynthian_gui_config.font_family, int(0.8 * zynthian_gui_config.font_size)),
+ font=("sans-serif", info_fs),
+ fill=zynthian_gui_config.color_panel_tx)
+
+ def update_layout(self):
+ super().update_layout()
+ if self.info_canvas:
+ self.icon_canvas.configure(height=int(0.5 * self.height))
+ self.info_canvas.configure(height=int(0.5 * self.height))
+
+ def get_info(self):
+ try:
+ return self.list_data[self.index][3]
+ except:
+ return ["", ""]
+
+ def update_info(self):
+ info = self.get_info()
+ if info:
+ self.info_canvas.itemconfigure(self.description_label, text=info[0])
+ self.icon_canvas.itemconfigure(self.icon_image, image=self.get_icon(info[1]))
+
+ def get_icon(self, icon_fname):
+ if not icon_fname:
+ return zynthian_gui_config.loading_imgs[0]
+ elif icon_fname not in self.icons:
+ try:
+ img = Image.open(f"/zynthian/zynthian-ui/icons/{icon_fname}")
+ icon = ImageTk.PhotoImage(img.resize(self.icon_size))
+ self.icons[icon_fname] = icon
+ return icon
+ except Exception as e:
+ logging.error(f"Can't load info icon {icon_fname} => {e}")
+ return zynthian_gui_config.loading_imgs[0]
+ else:
+ return self.icons[icon_fname]
+
+ def select(self, index=None, set_zctrl=True):
+ super().select(index, set_zctrl)
+ self.update_info()
+
+ def send_controller_value(self, zctrl):
+ if not self.shown:
+ return
+ if zctrl == self.zselector.zctrl:
+ self.select(zctrl.value)
+
+ def cb_info_press(self, event):
+ self.zyngui.cuia_help()
+
+# ------------------------------------------------------------------------------
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_gui_zs3_options.py b/zyngui/zynthian_gui_zs3_options.py
index 219e2cda5..a24a9801a 100644
--- a/zyngui/zynthian_gui_zs3_options.py
+++ b/zyngui/zynthian_gui_zs3_options.py
@@ -115,6 +115,9 @@ def zs3_restoring_options_cb(self):
if chain is None:
continue
label = chain.get_name()
+ while f"\u2612 {label}" in options or f"\u2610 {label}" in options:
+ # Make each option title unique so that they are not omitted from the options menu
+ label += " "
try:
restore_flag = chain_state["restore"]
except:
@@ -152,7 +155,15 @@ def zs3_rename_cb(self, title):
def zs3_update(self):
logging.info("Updating ZS3 '{}'".format(self.zs3_id))
+ restore_chains = []
+ state = self.zyngui.state_manager.zs3[self.zs3_id]
+ if "chains" in state:
+ for chain_id, chain_state in state["chains"].items():
+ if "restore" in chain_state and not chain_state["restore"]:
+ restore_chains.append(chain_id)
self.zyngui.state_manager.save_zs3(self.zs3_id)
+ for chain_id in restore_chains:
+ self.zyngui.state_manager.toggle_zs3_chain_restore_flag(self.zs3_id, chain_id)
self.zyngui.close_screen()
def zs3_delete(self):
diff --git a/zyngui/zynthian_widget_sooperlooper.py b/zyngui/zynthian_widget_sooperlooper.py
index b3e6c1e8d..8b515cc56 100644
--- a/zyngui/zynthian_widget_sooperlooper.py
+++ b/zyngui/zynthian_widget_sooperlooper.py
@@ -3,9 +3,9 @@
# ******************************************************************************
# ZYNTHIAN PROJECT: Zynthian GUI
#
-# Zynthian Widget Class for "Zynthian Audio Player" (zynaudioplayer#one)
+# Zynthian Widget Class for "Sooperlooper"
#
-# Copyright (C) 2022 Brian Walton
+# Copyright (C) 2022-2024 Brian Walton
#
# ******************************************************************************
#
@@ -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 = []
@@ -342,6 +342,8 @@ def on_button(self, btn):
if btn in ['undo', 'redo']:
liblo.send(self.osc_url, '/sl/-3/hit', ('s', btn))
else:
+ if btn in self.processor.engine.SL_LOOP_SEL_PARAM:
+ btn += f":{self.processor.engine.selected_loop}"
self.processor.controllers_dict[btn].toggle()
def on_slider_wheel(self, event):
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