From b1a0ad199457d46b04b50a1b79e166301e108ae4 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Mon, 8 Sep 2025 23:38:14 +0200 Subject: [PATCH 01/57] config file for VSCode debugger from zynthian webpage. should be private for me. --- launch.json | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 launch.json diff --git a/launch.json b/launch.json new file mode 100644 index 000000000..c1bdcd449 --- /dev/null +++ b/launch.json @@ -0,0 +1,36 @@ +{ + // Peter development nach: https://wiki.zynthian.org/index.php/Contributing_to_Zynthian_Development + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Zynthian", + "type": "debugpy", + "request": "launch", + "program": "zynthian_main.py", + "python": "/zynthian/venv/bin/python3", + "console": "integratedTerminal", + "justMyCode": true, + "env": { + "DISPLAY": ":0" + } + }, + { + "name": "Zynthian Debug", + "type": "debugpy", + "request": "launch", + "program": "zynthian_main.py", + "python": "/zynthian/venv/bin/python3", + "console": "integratedTerminal", + "justMyCode": true, + "subProcess": true, + "env": { + "DISPLAY": ":0", + "ZYNTHIAN_LOG_LEVEL": "10" + } + } + ] +} + From 430fb7884eeb5b099af465d7afadba3175a4b6d7 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 9 Sep 2025 01:09:04 +0200 Subject: [PATCH 02/57] Minimalistic Keystation Pro 88 Mk1 driver. Just Zynpots. No buttons, no encoders, no faders. --- zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py | 1 + 1 file changed, 1 insertion(+) create mode 120000 zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py diff --git a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py new file mode 120000 index 000000000..2a6a1608c --- /dev/null +++ b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py @@ -0,0 +1 @@ +/root/Peter_ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py \ No newline at end of file From b48ed0b990a32fba66aaf38a5da3cfb8144097dc Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 9 Sep 2025 01:20:45 +0200 Subject: [PATCH 03/57] Start of development driver for ableton zynthian_ctrldev_ableton_push_1 --- zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py | 1 + 1 file changed, 1 insertion(+) create mode 120000 zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py new file mode 120000 index 000000000..24dae1717 --- /dev/null +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -0,0 +1 @@ +/root/Peter_ctrldev/zynthian_ctrldev_ableton_push_1.py \ No newline at end of file From 2ba3bb099dc727a82f50c69e7e42c991206873d6 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 9 Sep 2025 01:22:02 +0200 Subject: [PATCH 04/57] Modificateion needed to make my touchscreen driver work. --- zynthian.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/zynthian.sh b/zynthian.sh index e5f71f6c5..9abf2d166 100755 --- a/zynthian.sh +++ b/zynthian.sh @@ -249,6 +249,13 @@ fi splash_zynthian while true; do + +# brumby touchscreen mouse function not working, I have to reload +#modprobe -r hid_multitouch # Beispieltreiber (ändern!) +# sudo modprobe hid_multitouch +nohup bash -c "sleep 10 && modprobe -r hid_multitouch" >/dev/null 2>&1 & + + clean_zynthian_last_message # Start Zynthian GUI & Synth Engine From ab829ae191a52f711b60f18babfd79bfa54e83bf Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 9 Sep 2025 22:21:49 +0200 Subject: [PATCH 05/57] =?UTF-8?q?EIgentlich=20alles=20zur=C3=BCckge=C3=A4n?= =?UTF-8?q?dert,=20weil=20ich=20den=20Branch=20oram-2506.1=20von=20oram-25?= =?UTF-8?q?06=20abgezweigt=20habe.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zyngine/zynthian_state_manager.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/zyngine/zynthian_state_manager.py b/zyngine/zynthian_state_manager.py index 5ee21e2f4..ab3c5c5d1 100644 --- a/zyngine/zynthian_state_manager.py +++ b/zyngine/zynthian_state_manager.py @@ -731,15 +731,34 @@ def zynmidi_read(self): # Crop data until find the 0xF7 mark while sysex_data[-1] != 0xF7: del sysex_data[-1] + # logging.debug(f" SYSEX DATA => {sysex_data}") ev = bytes(sysex_data) # Try to manage with a control device driver if self.ctrldev_manager.midi_event(izmip, ev): - self.status_midi = True - self.last_event_flag = True - continue - + self.status_midi = True + self.last_event_flag = True + continue + + """ # brumby 250906-2230 + ret_value = self.ctrldev_manager.midi_event(izmip, ev) + if isinstance(ret_value, bool): + # logging.info(f"returns boolean: {ret_value}") + if ret_value: + # if true, the driver self processed the event. if false, the event must be processed as usual + self.status_midi = True + self.last_event_flag = True + continue # process new event from midi_in + elif isinstance(ret_value, bytes) or isinstance(ret_value, bytearray): + # driver modified the event and returns it + logging.info(f"returns event: {ret_value.hex()}") + evhead = ret_value[0] # update in Queue used varable. + ev = ret_value # now process returned new event down the line + else: + logging.error("wrong return type. not boolean, not midi_event") + """ + evtype = (evhead >> 4) & 0x0F chan = evhead & 0x0F From 06ebf3ac0984726e9a35fdb93f4750072a8bdb10 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 9 Sep 2025 22:35:51 +0200 Subject: [PATCH 06/57] Try to get them into git again. --- zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py | 1 - zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py | 1 - 2 files changed, 2 deletions(-) delete mode 120000 zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py delete mode 120000 zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py deleted file mode 120000 index 24dae1717..000000000 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ /dev/null @@ -1 +0,0 @@ -/root/Peter_ctrldev/zynthian_ctrldev_ableton_push_1.py \ No newline at end of file diff --git a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py deleted file mode 120000 index 2a6a1608c..000000000 --- a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py +++ /dev/null @@ -1 +0,0 @@ -/root/Peter_ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py \ No newline at end of file From ce4760a18fe1618b3ea52a3efa148e629475ed7b Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 9 Sep 2025 22:40:05 +0200 Subject: [PATCH 07/57] KEystation Pro 88 MK1 suppert for Zynthian. Also as commented Driver demo for new ctrldev structure. --- .../zynthian_ctrldev_keystation_pro_88_mk1.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py diff --git a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py new file mode 100644 index 000000000..883a6afb9 --- /dev/null +++ b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# +# Minimalistic Zynthian Control Device Driver for M-Audio Keystation Pro 88 +# used for Zynthina with touch screen (but without rotary encoders) +# The device driver can controll the gui with the 4 knobs +# +# The keystation pro 88 no LED, so there is no feedback possible on the device +# also it doesn't send key on and of messages, just program change on press, +# so its not possible to detect long and short presses. +# +# Rotary Encoders are simulatet with Knobs 18, 19, 10, 11 +# +# this sample driver shows how easy it is to write a custom driver +# for a specific MIDI controller +# +# everything that is not essential is commented out +# + +import logging +from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_base + +logger = logging.getLogger('zynthian') + +class zynthian_ctrldev_keystation_pro_88_mk1(zynthian_ctrldev_base): + + # Device identification + # dev_id = ["Keystation Pro 88"] # not essential + + # found no way to list the dev_ids on linux console. + # They are different in Zynthian to that what I found in linux console # + # with debugging it is easy to find the correct dev_ids. Found no other way. You could try + # device name with " IN 1" and "IN 2" at the end + dev_ids = ["Keystation Pro 88 IN 1", "Keystation Pro 88 IN 2"] # these values are ESSENTIAL for the driver to connect it to the device + + # driver_name = "Keystation Pro 88 Minimal" # not essential, just for information in logs + + # driver_description = "Minimalistic Zynthian Control Device Driver für M-Audio Keystation Pro 88" + # not essential. just for information in logs + + # driver_version = "0.1" + + # Helper variables for potentiometers. Hack, because ZYNPOT_ABS didn't work for me... + zynpot_0 = 0 + zynpot_1 = 0 + zynpot_2 = 0 + zynpot_3 = 0 + + def midi_event(self, ev): + """Easy MIDI event handler for Keystation Pro 88""" + # logger.debug(f"MIDI Event empfangen: {ev}") + if len(ev) > 0: + status= ev[0] & 0xF0 # Which midi message type (note on, note off, control change, etc. ) + # channel = ev[0] & 0x0F # not usesd, + + if len(ev) == 3: # most times 3 bytes and we need 3 bytes + # status = ev[0] & 0xF0 # Which midi message type (note on, note off, control change, etc. ) + # channel = ev[0] & 0x0F # not usesd, just for information + data1 = ev[1] # Note number or controller number + data2 = ev[2] # Note velocity or controller value + + + # We simulate a rotary encoder with the knobs + # We send the difference between the last value and the new value + # to the state manager ("ZYNPOT_ABS" would be easieer to use, but it doesn't work for me. It was never called in my tests) + # The state manager will handle the rest + # We have to store the last value of the knob + # We have 4 knobs for 4 virtual rotary encoders + # Knob 18 -> ZYNPOT 0 + # Knob 19 -> ZYNPOT 1 + # Knob 10 -> ZYNPOT 2 + # Knob 11 -> ZYNPOT 3 + + # yes I know, first time use of a knob lets jump the value from 0 to the knob value + # but I don't know how to get the current value of the knob at start up + # maybe someone can help me with that + + # 0xB0 is Control Change on MIDI Channel 1 + # 10 is the controller number for the first knob + # data2 is the value of the knob (0-127) + + + if status == 0xb0 and data1 == 42: # 42 is knob 18 in "keystations Preset 10. (Press Recall and choose 10)" + # if controller sends relative values, that is: negative values for left turn and positive values for right turn, + # you can use: + # pot = data2 + # but my keystation pro 88 MK1 sends only absolute knob values from 0 to 127 + # so I have to calculate the difference to the last value + pot = data2-self.zynpot_0 # calculates relative change of the value to use "ZYNPOT" instead of "ZYNPOT_ABS" + self.zynpot_0 = data2 # store the new value for the next change + + self.state_manager.send_cuia("ZYNPOT", [0, pot]) + return True # Event processed. restarts event loop + + if status == 0xb0 and data1 == 34: # 34 is knob 19 in "Preset-Recall 10" + pot = data2-self.zynpot_1 + self.zynpot_1 = data2 + self.state_manager.send_cuia("ZYNPOT", [1, pot]) + return True # Event processed. restarts event loop + + if ev[0] == 0xb0 and data1 == 10: # 10 is knob 10 in "Preset-Recall 10" + pot = data2-self.zynpot_2 + self.zynpot_2 = data2 + self.state_manager.send_cuia("ZYNPOT", [2, pot]) + return True # Event processed. restarts event loop + + if ev[0] == 0xb0 and data1 == 2: # 2 is knob 11 in "Preset-Recall 10" + pot = data2-self.zynpot_3 + self.zynpot_3 = data2 + self.state_manager.send_cuia("ZYNPOT", [3, pot]) + return True # Event processed. restarts event loop + + """ if len(ev) == 3: # PC has 3 bytes. + + # To use Buttons on Keystation 88 pro MK1 for "back" and "OK" is no good idea, because + # all Buttons are sending just a program change when pressed. + # You would miss them as Buttons for changing in Zynthina. But nevertheless here as example how to use it + + # status = ev[0] & 0xF0 # Which midi message type (note on, note off, control change, etc. ) + # channel = ev[0] & 0x0F # not usesd, just for information + data1 = ev[1] # Note number or controller number + + + # Buttons for "Ok" and "Back" + if status == 0xC0 and data1 == 0: # Button "Back" + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) + # self.state_manager.send_cuia("BACK") + return True # Event processed. restarts event loop + + if status == 0xC0 and data1 == 1: # Button "OK" + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) + # self.state_manager.send_cuia("SELECT") + return True # Event processed. restarts event loop """ + + return False # event not processed by this driver. Zynthian queue has to process it further down the row + + From c44b426a073d9116c5255f6f9662823f860cc662 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 9 Sep 2025 22:50:18 +0200 Subject: [PATCH 08/57] Ableton (Akai) Push 1 control device driver for Zynthian first commit --- .../zynthian_ctrldev_ableton_push_1.py | 799 ++++++++++++++++++ 1 file changed, 799 insertions(+) create mode 100644 zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py new file mode 100644 index 000000000..e447c6078 --- /dev/null +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -0,0 +1,799 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, 0) +# lib_zyncore.dev_send_midi_event(self.idev_out, sysex_data, len(sysex_data)) +# lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) + +# 20250821: Brumby INITIAL Version copied from Launchpad Mini Mk3. +# Ergolge: +# 1. Device wird erkannt und die einzelnen Functionen werden aufgerufen +# 2. Es werden Session angezeigt, Sogar mit den 4 verschiedenen Farben. Allerdings an falscher Stelle! +# 3. Die Shifttaste leuchtet, wenn man sie drückt! +### +# 20250828 +# 4. Pad-Anzeige funktioniert. Es kann noch nicht geschaltet werden. Farben sind wohl auch nicht ganz richtig +# 5. Start und Stop geht, aber die oben und Unten sind vertauscht +### +# 250829 +# 6 Zustand von 4 wieder erreicht. +# 5 geht nicht mehr! +# 7 Zustand von 5 ist wieder erreicht. Toggle row ist unten statt oben. +### +# 8 Ergolg Pads funktionieren!. Allerdings lassen sich unbelegte Platze einschalten. +# 9 Die Gui Potis funktionieren +# Erinnerung: Die Potis CC71-CC79 haben auch ein Touchfunktion. Allerdings mit anfassen des Knopfes und Loslassen. +# Das können wir deshalb nicht als keypress verwenden. +# Die Tasten unter dem Display aber schon! +# 10 poti 0-3 werden als Zypod 1-4 erkannt +# todo Tasten unterhalb des Displays als ZYNPOD-Buttons +# 11 ABL_OK und ABL_ESC funktionieren nicht. +# -- ABL_OK hat nicht den richtigen GUISTEUERWERT +# -- ABL_ESC wird nicht mit der Taste angesteuert +# 12 ABL_OK und ABL_ESC funktionieren. +# +# Bemerkung: Alle Tasten, die eine Funktion haben, leuchten. Es wird aber KEIN Leuchstatus gelöscht, wenn er schon gesetzt war! +# +# todo Display mit Sysex ansteuern + +# 250830 0310 +# 13 Ich kann die Statusmeldung überschreiben und aus irgendeinem Grund wird Hallo Welt ausgegeben. + +# 20250831-0057 +# Das Display funktioniert. Ich kann Texte beliebig positionieren und den Bildschirm löschen. + +#20250901-0015 +#Sicherung funktionsfähig + + +#### +#### +#### + + +## from pushmod.blospot.com +#### PUSH 1 SYSEX ####################################### + +# 71 is the manufacturer ID (Akai Electric Co. Ltd.) +# 127 is the device ID (default it 127 - All Devices) +# 21 is the product ID (Push) +# The Device ID can be sent as 0 as well. + +# Identity request 240,126,0,6,1,247 +# Set pad color (RGB) 240,71,127,21,4,0,8,,0,,,,,,,247 +# Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 +# Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 +# Set key aftertouch 240,71,127,21,92,0,1,0,247 +# Set channel aftertouch 240,71,127,21,92,0,1,1,247 +# Set Live version 240,71,127,21,96,0,4,65,,,,247 +# Set Live mode 240,71,127,21,98,0,1,0,247 +SYSEX_DATA_SET_LIVE_MODE= [240,71,127,21,98,0,1,0,247] + +# Set User mode 240,71,127,21,98,0,1,1,247 +SYSEX_DATA_SET_USER_MODE= [240,71,127,21,98,0,1,1,247] + +# Set touch strip mode 240,71,127,21,99,0,1,,247 +# Request white calibration information 240,71,127,21,107,0,0,247 +# Contrast request 240,71,127,21,122,0,0,247 +# Contrast set 240,71,127,21,122,0,1,, 247 +# Brightness request 240,71,127,21,124,0,0,247 +# Brightness set 240,71,127,21,124,0,1,,247 +######### END PUSH 1 SYSEX ############################ + + +### Monochromatic Keys/Pads ####################### +# 0 - Off +ABL_KEY_LED_OFF = 0 +# 1 - Dim +ABL_KEY_LED_DIM = 1 +# 2 - Dim Blink +ABL_KEY_LED_DIM_BLINK = 2 +# 3 - Dim Blink Fast +ABL_KEY_LED_DIM_BLINK_FAST = 3 +# 4 - Lit +ABL_KEY_LED_LIT = 4 +# 5 - Lit Blink +ABL_KEY_LED_LIT_BLINK = 5 +# 6 - Lit Blink Fast +ABL_KEY_LED_LIT_BLINK_FAST = 6 +# 7 -> 127 - Lit +######### END MONOCHROMATIC LED ################## + +#Bi-color LED table +#These are the colors which will be set on the bi-color (red/green) buttons: + +ABL_BI_LED_OFF = 0 # 0 - Off (Black) +ABL_BI_RED_DIM = 1 # 1 - Red Dim +ABL_BI_RED_DIM_BLINK = 2 # 2 - Red Dim Blink +ABL_BI_RED_DIM_BLINK_FAST = 3 # 3 - Red Dim Blink Fast +ABL_BI_RED = 4 # 4 - Red +# 5 - Red Blink +ABL_BI_RED_BLINK = 5 +# 6 - Red Blink Fast +ABL_BI_RED_BLINK_FAST = 6 +# 7 - Orange Dim +ABL_BI_ORANGE_DIM = 7 +# 8 - Orange Dim Blink +ABL_BI_ORANGE_DIM_BLINK = 8 +# 9 - Orange Dim Blink Fast +ABL_BI_ORANGE_DIM_BLINK_FAST = 9 +#10 - Orange +ABL_BI_ORANGE = 10 +#11 - Orange Blink +ABL_BI_ORANGE_BLINK = 11 +#12 - Orange Blink Fast +ABL_BI_ORANGE_BLINK_FAST = 12 +#13 - Yellow (Lime) Dim +ABL_BI_YELLOW_DIM = 13 +#14 - Yellow Dim Blink +ABL_BI_YELLOW_DIM_BLINK = 14 +#15 - Yellow Dim Blink Fast +ABL_BI_YELLOW_DIM_BLINK_FAST = 15 +#16 - Yellow (Lime) +ABL_BI_YELLOW = 16 +#17 - Yellow Blink +ABL_BI_YELLOW_BLINK = 17 +#18 - Yellow Blink Fast +ABL_BI_YELLOW_BLINK_FAST = 18 +#19 - Green Dim +ABL_BI_GREEN_DIM = 19 +#20 - Green Dim Blink +ABL_BI_GREEN_DIM_BLINK = 20 +#21 - Green Dim Blink Fast +ABL_BI_GREEN_DIM_BLINK_FAST = 21 +#22 - Green +ABL_BI_GREEN = 22 +#23 - Green Blink +ABL_BI_GREEN_BLINK = 23 +#24 - Green Blink Fast +ABL_BI_GREEN_BLINK_FAST = 24 +#25 -> 127 - Green + +# note werte Push 1 Midimapping +ABL_PAD_START = 36 +ABL_PAD_END = 99 + +# CC Werte der Tasten +ABL_REC = 86 +ABL_PLAY = 85 + +ABL_OK = 62 +ABL_ESC = 63 # Abbruch, Zurück + +ABL_TRACK = 112 + +ABL_ARROW_LEFT = 44 +ABL_ARROW_RIGHT = 45 +ABL_ARROW_UP = 46 +ABL_ARROR_DOWN = 47 +ABL_SHIFT = 49 + +# DISPLAY Buttons Row 1 +ABL_BUTTON_DISPL_R1_0 = 20 +# ... +ABL_BUTTON_DISPL_R1_8 = 27 +# Display Buttons Row 2 +ABL_BUTTON_DISPL_R2_0 = 102 +# ... +ABL_BUTTON_DISPL_R2_8 = 109 + + + +# ****************************************************************************** +# ZYNTHIAN PROJECT: Zynthian Control Device Driver +# +# Zynthian Control Device Driver for "Ableton Push 1" +# +# Copyright (C) 2025 Julius Brumby +# +# ****************************************************************************** +# +# 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 logging + +# Brumbs imports +#from ableton_push1_display import Push1Display +from time import sleep + +# Zynthian specific modules +from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad # , zynthian_ctrldev_zynmixer +from zyncoder.zyncore import lib_zyncore +from zynlibs.zynseq import zynseq + +# ------------------------------------------------------------------------------------------------------------------ +# Ableton Push 1 +# ------------------------------------------------------------------------------------------------------------------ + + +class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad): # , zynthian_ctrldev_zynmixer): + + + logging.error("Klassenaufruf - Ableton Push 1 - BRUMBY") + # Im Weblog wird angezeigt, dass der Treiber geladen wurde + + dev_ids = ["Ableton Push IN 2"] + driver_name = "Ableton Push v1" + driver_description = "Interface Ableton Push v1 with zynpad and zynmixer" + + # Folgende Farben sind wohl die Sequencer Farben?? + # siehe: https://pushmod.blogspot.com/p/pad-color-table.html + # ORIGINAL PAD_COLOURS = [71, 104, 76, 51, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] + PAD_COLOURS = [61, 36, 63, 54, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] + STARTING_COLOUR = 123 + STOPPING_COLOUR = 120 + + # pad_modes + PAD_MODE_SEQ = 0 + PAD_MODE_DRUMS = 1 + PAD_MODE_SCALES = 2 + + # pad_mode_active = PAD_MODE_SEQ + pad_mode_active = PAD_MODE_SCALES + + # Function to initialise class + def __init__(self, state_manager, idev_in, idev_out=None): + logging.info("__init__ Ableton Push 1 - BRUMBY") + self.shift = False + super().__init__(state_manager, idev_in, idev_out) + + # self.pad_mode_active = self.PAD_MODE_SCALES + + # self.zynmixer = state_manager.zynmixer # Mixer object + + # Initialize display + self.display = Display(idev_out) + self.display.clear() + sleep(0.1) # necessary delays, otherwise the next command is ignored + + self.display.brightnes(63) + sleep(0.1) + + self.display.write_xy(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) + sleep(0.1) + +# Positionierungshilfe +# self.display.write_xy(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) +# sleep(0.1) + + self.display.write_xy(b'** Zynthian Push1Driver 0.1 **', 17,2) + sleep(0.1) + + self.display.write_xy(b'++ Make MusicNot War ++', 20,3) + + def init(self): + logging.error("init Ableton Push 1 - BRUMBY") + + # Hier muss die Trackstaste zum Leuchten gebracht werden am Push 1 + # Enable session mode on launchkey + # Track-Taste CC112 # ABL_TRACK + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 112, ABL_KEY_LED_LIT) # 2! + # CC62 = OK; CC63 = Back (ABL_OK, ABL_ESC + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 62, ABL_KEY_LED_LIT_BLINK) # 2! + + # Monochrome Tasten die hell leuchten sollen + for t in [ 36,37,38,39,40,41,42,43, ABL_PLAY, ABL_OK, ABL_ESC, ABL_ARROW_LEFT, ABL_ARROW_RIGHT, ABL_ARROW_UP, ABL_ARROR_DOWN]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL_KEY_LED_LIT) # 2! + + # monochrome Tasten die dim leuchten sollen + for t in [ABL_REC, ABL_SHIFT]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL_KEY_LED_DIM) # 2! + + # Bicolortasten die dim leuchten sollen CC20-27 + 102-109 + for t in [21, 23]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, 13) # 2! + + ### lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) + self.cols = 8 + self.rows = 8 # war 2 20250829-2134 + super().init() # aktiviert. Muss aktiviert sein! + self.pads_off() + + + def end(self): + # logging.error("end Ableton Push 1 - BRUMBY") + super().end() + ### Disable session mode on launchkey + ## lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 0) # device, channel, note, velocity + + + # this function is called by zynseq when a sequencer state is changed + # we will update pad LED to show state + def update_seq_state(self, bank, seq, state, mode, group): + + # Onlyreturn if Push1 driver is not in sequencer_mode_view + if not self.pad_mode_active == self.PAD_MODE_SEQ: return + + # logging.info(f"BRUMBY bank={bank}; seq={seq}; state={state}; mode={mode}; group:{group}") + if self.idev_out is None or bank != self.zynseq.bank: + return + + col, row = self.zynseq.get_xy_from_pad(seq) + note = ABL_PAD_END +1 -(row+1) * 8 + col + # logging.info(f"BRUMBY-P col={col}; row={row} ergibt note:{note}") + + # Alles abfangen, was ausserhalb des Pad-Bereichs ist BRUMBY_NEU. + #if (note > ABL_PAD_END) or (note < ABL_PAD_START): + # return + + try: + if mode == 0 or group > 16: + chan = 0 + vel = 0 + elif state == zynseq.SEQ_STOPPED: + chan = 0 + vel = self.PAD_COLOURS[group] + elif state == zynseq.SEQ_PLAYING: + chan = 2 + vel = self.PAD_COLOURS[group] + elif state in [zynseq.SEQ_STOPPING, zynseq.SEQ_STOPPINGSYNC]: + chan = 1 + vel = self.STOPPING_COLOUR + elif state == zynseq.SEQ_STARTING: + chan = 1 + vel = self.STARTING_COLOUR + + else: # Wenn nichts passt Pad-Beleuchtung ausschalten + chan = 0 + vel = 0 + + except Exception as e: # Bei Fehler Pad-beleuchtung ausschalten + chan = 0 + vel = 0 + + # set pad color with velocity value + lib_zyncore.dev_send_note_on(self.idev_out, chan, note, vel) + + + + + def pad_off(self, col, row): + # note = 96 + row * 16 + col # statt 96 -> 91 für Push + note = ABL_PAD_END +1 -(row+1) * 8 + col + logging.error(f"BRUMBY: row={row}; col={col} pad-note={note}") + lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0) + + def pads_off(self): + dbg = False + logging.error("BRUMBY: pas_off") + for row in range(self.rows): + for col in range(self.cols): + self.pad_off(col, row) + + def midi_event(self, ev): + logging.error(f"midi_event Ableton Push 1 - BRUMBY {ev}") + evtype = (ev[0] >> 4) & 0x0F + + # evtype= EV_NOTE_ON + if evtype == 0x9: + + note = ev[1] & 0x7F # das ist überflüssig, weil note immer < 127 ist + + # Alle Noteevents ausfiltern, die nicht von den Pads kommen + if note < ABL_PAD_START: + return True + if note > ABL_PAD_END: + return True # ignore every note_on not from pads + + logging.error(f"BRUMBY: note={note}") + + # Entered session mode so set pad LEDs + # QUESTION: What kind of message is this? Only SysEx messages can be bigger than 3 bytes. + # if ev == b'\x90\x90\x0C\x7F': + # self.update_seq_bank() + + # Toggle pad + # Hier wird der midi-Notenwert in einen x,y Wert umgewandelt, um die Sequencer-Bank entsprechend zu toggeln. + + if self.pad_mode_active == self.PAD_MODE_SCALES: + # hier muss er Translator für scales hin! + logging.error(f"midi_event Ableton Push 1 - BRUMBY: PAD in SCALES mode - not implemented yet") + return False + + + + + + if self.pad_mode_active == self.PAD_MODE_SEQ: + try: + # BRUMBY_NEU + + col = (note - ABL_PAD_START) // 8 # statt 96 -> 91 + row = (note - ABL_PAD_START) % 8 # Statt 96 -> 91 + # row = 7 - row; + col = 7 - col; + pad = row * self.zynseq.col_in_bank + col + + logging.error(f"midi_event 1 MEINER Ableton Push 1 - BRUMBY: row={row}; col={col}; pad={pad}") + + if pad < self.zynseq.seq_in_bank: + self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) + return True + except: + pass + + return False + + # GUI Control Changes + # evtype = EV_CC + elif evtype == 0xB: + ccnum = ev[1] & 0x7F + ccval = ev[2] & 0x7F + + # Der Status der Schift taste CC49 wird abgefragt. CCVall <> 0 heisst gedruückt. Andernfalls losgelassen + if ccnum == 49: + # SHIFT + self.shift = ccval != 0 + if self.shift: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL_KEY_LED_LIT_BLINK) + else: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL_KEY_LED_DIM) + return True + + # Jetzt wird alles ausgefiltert, das den Wert 0 hat, damit das loslassen der Taste als CC ausgefiltert wird + elif ccnum == 0 or ccval == 0: + return True + + # Ab hier kann mann die Tastendrücke auswerten + + elif (self.shift and 20 < ccnum < 29) or (20 < ccnum < 25): + chain = self.chain_manager.get_chain_by_position( + ccnum - 21, midi=False) + if chain and chain.mixer_chan is not None and chain.mixer_chan < 17: + self.zynmixer.set_level(chain.mixer_chan, ccval / 127.0) + + # Zynpoties Werte an GUI + # Potis Oben 72 - 75 die ersten 4 + elif 70 < ccnum < 80: + # self.state_manager.send_cuia("ZYNPOT_ABS", [ccnum - 72, ccval/127]) + val = ccval + if val > 68: + val = (val - 128) + # falsch geraten, nicht ZYNPT_REL. Vielleicht ZYNPOT? + self.state_manager.send_cuia("ZYNPOT", [ccnum - 71, val]) + logging.error(f"BRUMBY: Poti={ccnum-71} val={val}") + return True + + + elif (ccnum == ABL_OK) or (ccnum == 23): + logging.error("ABL_OK BRUMBY") + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) + return True + + elif ccnum == 21: + logging.error("ZYNPUT_BUT 1 ESC BRUMBY") + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) + return True + + + elif ccnum == ABL_ESC: + logging.error("ABL_ESC BRUMBY") + self.state_manager.send_cuia("BACK") + return True + + + + elif ccnum == 45: # BRUMBY + # elif ccnum == 0x66: + # TRACK RIGHT + self.state_manager.send_cuia("ARROW_RIGHT") + return False + + elif ccnum == 44: + # elif ccnum == 0x67: + # TRACK LEFT + self.state_manager.send_cuia("ARROW_LEFT") + return False + + elif ccnum == 46: + # elif ccnum == 0x68: + # UP + self.state_manager.send_cuia("ARROW_UP") + return True + + elif ccnum == 47: + # elif ccnum == 0x69: + # DOWN + self.state_manager.send_cuia("ARROW_DOWN") + return True + + elif ccnum == ABL_PLAY: + # PLAY + if self.shift: + self.state_manager.send_cuia("TOGGLE_MIDI_PLAY") + else: + self.state_manager.send_cuia("TOGGLE_PLAY") + return True + elif ccnum == ABL_REC: + # RECORD + if self.shift: + self.state_manager.send_cuia("TOGGLE_MIDI_RECORD") + else: + self.state_manager.send_cuia("TOGGLE_RECORD") + return True + elif (ccnum > 35) and (ccnum < 44): + self.zynseq.select_bank (8- (ccnum - 36)) + # Leuchstatus ändern + for t in [ 36,37,38,39,40,41,42,43]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL_KEY_LED_DIM) # 2! + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL_KEY_LED_LIT_BLINK_FAST) # 2! + return True + + + # evtype = MIDI_PC ?? + elif evtype == 0xC: + val1 = ev[1] & 0x7F + self.zynseq.select_bank(val1 + 1) ## wahrscheinlich wird hier update_seq_state aufgerufen + return True + + return False + + +# def send_sysex(self, data): + return + # Send SysEx universal inquiry. + # It's answered by some devices with a SysEx message. + # def send_sysex_universal_inquiry(self): + if self.idev_out > 0: + #msg = bytes(SYSEX_DATA_SET_USER_MODE) + #logging.error(f"BRUMBY: set user mode SYSEX={msg};") + #lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + #sleep (0.05) + + # "240 71 127 21 24 0 69 0 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247") + s = "240 71 127 21 25 0 69 0 32 32 32 72 101 108 108 111 32 87 111 114 108 100 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247" + s2 = "240 71 127 21 26 0 69 0 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247" + # "240 71 127 21 27 0 69 0 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247" + + # s = "240 71 127 21 25 0 69 0 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247" + + # String S + integers = [int(x) for x in s.split()] + msg = bytes(integers) + logging.error(f"BRUMBY: DISPLAY LINE2 SYSEX={msg};") + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + + # String S2 + integers = [int(x) for x in s2.split()] + msg = bytes(integers) + logging.error(f"BRUMBY: DISPLAY LINE3 SYSEX={msg};") + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + + + #logging.error(f"BRUMBY: MIDDLE OF send_sysex;") + + #logging.error(f"BRUMBY: SYSEX={data};") + #lib_zyncore.dev_send_midi_event(self.idev_out, data, len(data)) + + #sleep(0.05) + logging.error(f"BRUMBY: END OF send_sysex;") + + +# ------------------------------------------------------------------------------ + +#// Special Dispay Characters +##define UP_ARROW 0 +##define DOWN_ARROW 1 +##define THREE_STACKED_HORIZONTAL_LINES 2 +##define VERTICAL_LINE_AND_HORIZONTAL_LINE 3 +##define HORIZONTAL_LINE_AND_VERTICAL_LINE 4 +##define TWO_VERTICAL_LINES 5 +##define TWO_SIDE_BY_SIDE_HORIZONTAL_LINES 6 +##define FOLDER_SYMBOL 7 +##define SPLIT_VERTICAL_LINES 8 +##define FLAT_SYMBOLS 27 +##define THREE_SIDE_BY_SIDE_DOTS 28 +##define FULL_BLOCK 29 +##define RIGHT_ARROW 30 +##define LEFT_ARROW 31 + +class Display: + + display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten + + def __init__ (self, idev_out): + self.dbg = True + self.idev_out = idev_out + # if self.dbg: + logging.error(f"BRUMBY: Class Display instantiiert") + + + + def clear (self): + # clear out display_memory with blanks. + self.display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten + + + """Overwrites whole display with ascii 32""" + #if self.idve_out == 0: + # pass + #logging.error(f"BRUMBY: Display.clear: idev_out={self.idev_out}") + + # SYSEX_ZEILE_LÖSCHEN = 240,71,127,21,<28+line(0-3)>,0,0,247 + s0 = bytes([240,71,127,21,28,0,0,247]) # Zeile 0 + s1 = bytes([240,71,127,21,29,0,0,247]) # Zeile 1 + s2 = bytes([240,71,127,21,30,0,0,247]) # Zeile 2 + s3 = bytes([240,71,127,21,31,0,0,247]) # Zeile 3 + for x in [s0, s1, s2, s3]: + lib_zyncore.dev_send_midi_event(self.idev_out, x, len(x)) + sleep(0.05) + + # logging.error(f"BRUMBY: Display.clear: end of func idev_out={self.idev_out}") + + def refresh (self): + # display memory to display + for row in range(4): + #msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) + text = bytes(self.display_mem[row]) + text_len = len(text) + col = 0 + # here the magic happens and sysex is cunstructed + # 240, 71, 127, 21, <24+line(0-3)>,0, ,,, 247 + msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) + # logging.error(f"BRUMBY: Display.refresh SYSEX={msg}") + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + sleep(0.05) + return + # not implemented yet + + + + def write_xy (self, text, col_in, row_in): + # schreibt Text an Position col_in, row_in in display memory und auf Display + # mit refresh + + # Koordinaten prüfen + if(row_in > 3): row_in = 3 + if(row_in < 0): row_in = 0 + if(col_in > 63): col_in = 63 + if(col_in < 0): col_in = 0 + + # Textlänge prüfen, ob im erlaubten Bereich. + text_len = len(text) + if text_len + col_in > 68 : text = text[:68-col_in] # Rest abschneiden + text_len = len(text) + + self.display_mem[row_in][col_in:col_in+text_len] = list(text) + self.refresh() + return + + + """ #dbg = False + #if dbg: logging.error(f"BRUMBY: Display.write_xy text={text}x={col_in} y={row_in}") + row=row_in; + col=col_in + if not type(text) is bytes: + text = 'TypError b\'text\' erwartet' + logging.error(f"BRUMBY: TypeError b'text' erwartet und nicht {text}->{type(text)}") + + if (row < 0) : row = 0 + if (row > 3) : row = 3 + if (col < 0) : col = 0 + if (col > 63): col = 63 # nur der erste Char von text wäre druckbar + + #if dbg: logging.error(f"BRUMBY: Display.write_xy 2 text={text}x={col_in} y={row_in}") + + # Textlänge prüfen, ob im erlaubten Bereich. + text_len = len(text) + if text_len + col > 68 : text = text[:68-col] # Rest abschneiden + text_len = len(text) + + # here the magic happens and sysex is cunstructed + # 240, 71, 127, 21, <24+line(0-3)>,0, ,,, 247 + msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) + + #if dbg: logging.error(f"BRUMBY: Display.write_xy 4 SYSEX={msg}") + + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + # time.sleep(0.5) + #if dbg: logging.error(f"BRUMBY: END_OF_Display.write_xy 2 ") + """ + +# This might belong to the display setup +# Contrast request 240,71,127,21,122,0,0,247 +# Contrast set 240,71,127,21,122,0,1,, 247 +# Brightness request 240,71,127,21,124,0,0,247 +# Brightness set 240,71,127,21,124,0,1,,247 + + def contrast (self, i=None) -> int: + """ + Setzt oder liest den Kontrast des Geräts via SysEx. + + Args: + i (int, optional): Der gewünschte Kontrastwert (typischerweise 0-127). + Wenn None, wird der aktuelle Kontrast gelesen. + + Returns: + int: Der aktuelle Kontrastwert (nach Setzung oder Abfrage). + + Raises: + ValueError: Wenn der Kontrastwert außerhalb des gültigen Bereichs liegt. + """ + + # Überprüfen, ob ein Wert zum Setzen übergeben wurde + + if i is not None: + + if i < 0 : i = 0 + if i > 63: + i = 63 + logging.error(f"Constrast values more than 63, seem to do nothing. values set to 63") + + + # set contrast + # 240,71,127,21,122,0,1,, 247 + msg = bytes([240,71,127,21,122,0,1,i , 247]) + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + return i + + # send sysexrequest + # Contrast request 240,71,127,21,122,0,0,247 + msg = bytes([240,71,127,21,122,0,0,247]) + + # lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + + # Return Contrast. Not implemented + return None + + + + def brightnes (self, i=None) -> int: + logging.error(f"BRUMBY") + + if i is not None: + + if i < 9: i = 0 + if i > 63: + i = 63 + logging.error(f"Brightnes values more than 63, seem to do nothing. values set to 63") + + logging.error(f"BRUMBY brightnes={i}") + + # set brightnes + # 240,71,127,21,124,0,1,,247 + + msg = bytes([240,71,127,21,124,0,1,i , 247]) + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + logging.error(f"BRUMBY zu brightnes={i} geändert") + + + + + # send sysexrequest + # Brightness request 240,71,127,21,124,0,0,247 + # commented out; getting return value not implemented yet. don't know how to + # msg = bytes([240,71,127,21,124,0,0,247]) + # lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + + # Return Brightnes. Not implemented + return None + + + + + + + + + + + + +# Zynthian specific modules +# from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad # , zynthian_ctrldev_zynmixer +# from zyncoder.zyncore import lib_zyncore +# from zynlibs.zynseq import zynseq From ec903b7ce54246464e24fca1f082bc1879df29ee Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 9 Sep 2025 22:51:12 +0200 Subject: [PATCH 09/57] This is for scales on pads like push 1 or launchpad etc as basedriver --- .../ctrldev/zynthian_ctrldev_base_scale.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100755 zyngine/ctrldev/zynthian_ctrldev_base_scale.py diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py new file mode 100755 index 000000000..3e2d0b25d --- /dev/null +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -0,0 +1,104 @@ +#!/zynthian/venv/bin/python + + +# Zynthian specific modules +# from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad # , zynthian_ctrldev_zynmixer +# from zyncoder.zyncore import lib_zyncore +# from zynlibs.zynseq import zynseq + +# import zynthian_ctrldev_base + +# Following from: https://github.com/Carlborg/hardpush/blob/master/hardpush.ino +# All scales seem to work with 12-halftones. (otherwise they would need the octave-distance at the end)4 +SCALES = { # define scales on the form 'semitones added to tonic' + 'Chromatic': [0,1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + "Major": [0, 2, 4, 5, 7, 9, 11], + "Minor": [0, 2, 3, 5, 7, 8, 10], + "Dorian": [0, 2, 3, 5, 7, 9, 10], + "Mixolydian": [0, 2, 4, 5, 7, 9, 10], + "Lydian": [0, 2, 4, 6, 7, 9, 11], + "Phrygian": [0, 1, 3, 5, 7, 8, 10], + "Locrian": [0, 1, 3, 4, 7, 8, 10], + "Diminished": [0, 1, 3, 4, 6, 7, 9, 10], + "Whole-Half": [0, 2, 3, 5, 6, 8, 9, 11], + "Whole Tone": [0, 2, 4, 6, 8, 10], + "Minor Blues": [0, 3, 5, 6, 7, 10], + "Minor Pentatonic": [0, 3, 5, 7, 10], + "Major Pentatonic": [0, 2, 4, 7, 9], + "Harmonic Minor": [0, 2, 3, 5, 7, 8, 11], + "Melodig Minor": [0, 2, 3, 5, 7, 9, 11], + "Super Locrian": [0, 1, 3, 4, 6, 8, 10], + "Bhairav": [0, 1, 4, 5, 7, 8, 11], + "Hungarian Minor": [0, 2, 3, 6, 7, 8, 11], + "Minor Gipsy": [0, 1, 4, 5, 7, 8, 10], + "Hirojoshi": [0, 2, 3, 7, 8], + "In-Sen": [0, 1, 5, 7, 10], + "Iwato": [0, 1, 5, 6, 10], + "Kumoi": [0, 2, 3, 7, 9], + "Pelog": [0, 1, 3, 4, 7, 8], + "Spanish": [0, 1, 3, 4, 5, 6, 8, 10] +} + + +### How to get the values +## print(list(scales)) # ['Chromatic', 'Major', 'Minor', 'Dorian', 'Mixolydian', 'Lydian', 'Phrygian', 'Locrian', 'Diminished', 'Whole-Half', 'Whole Tone', 'Minor Blues', 'Minor Pentatonic', 'Major Pentatonic', 'Harmonic Minor', 'Melodig Minor', 'Super Locrian', 'Bhairav', 'Hungarian Minor', 'Minor Gipsy', 'Hirojoshi', 'In-Sen', 'Iwato', 'Kumoi', 'Pelog', 'Spanish'] +## print(list(scales)[0]) # "Chromatic" +## print(scales['Chromatic']) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + + +class Harmony: + + scales = SCALES + + def __init__ (self): + pass + + + + + def get_midi_note(self, scale,start) -> int: + """params + + scale: is string with name of scale + + start: is integer representing the starting point in the scale. if start is bigger than + the length of the specified scale it adds start % leng(scale) * 12 to the result. n + So you ca cycle through the number of keyboard keys to get their midi notes + """ + try: + print(scales[scale]) + l = scales[scale] + pos = start%len(l) + octave = start // len(l) + print(f"[Debug note]: scale={scale}, start={start} pos={pos}, erg={l[pos]} von {l}") + return l[pos] + (octave * 12 ) + except: + print("Error: get_midi_note: Tonart nicht definiert!") + + + def get_scale_len(self, scale) -> int: + try: + print(f"[Debug len]: scale={scale}, len={len(scales[scale])}") + return len(scales[scale]) + except: print("Error: get_scale_len: Tonart nicht definiert!") + + def get_scale_names(self): + return list(scales) + + + +h = Harmony() + +print(h.get_scale_names()) +print(h.get_midi_note("Kumoi", 4)) +print(h.get_scale_len("Kumoi")) + + +""" +class zynthian_ctrldev_scale(zynthian_ctrldev_base): + + + + def __init(self): + pass +""" \ No newline at end of file From 514bb73b241a846db2f7ba51a939254a8b22e836 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 10 Sep 2025 01:00:39 +0200 Subject: [PATCH 10/57] Consts for ableton push 1 driver --- zyngine/ctrldev/ableton/push1_consts.py | 277 ++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 zyngine/ctrldev/ableton/push1_consts.py diff --git a/zyngine/ctrldev/ableton/push1_consts.py b/zyngine/ctrldev/ableton/push1_consts.py new file mode 100644 index 000000000..677d4df14 --- /dev/null +++ b/zyngine/ctrldev/ableton/push1_consts.py @@ -0,0 +1,277 @@ +# Ableteton Push 1 + +### Definition of all buttons, pads and knobs +#knobs and Buttons are CC-Events +#knobs also have touch function with midi note event +# +#ribbon is type modwheel !!! Just one byte ev[0] +#ribbon has also touch function with midi note even +# +# pad has note event + + +##### +# Buttons are defined with ther action Message. Noteon, Control Change + + +### SYSEX +#SYSEX_PREAMBLE = [] +#SYSEX_END = [] + +# Display +# Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 +# Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 +#SYSEX_INST_WRITE_LINE_0 = bytes([24]) +#SYSEX_INST_WRITE_LINE_1 = bytes([25]) +#SYSEX_INST_WRITE_LINE_2 = bytes([26]) +#SYSEX_INST_WRITE_LINE_3 = bytes([27]) + + +# Knobs 1-9 +KNOB_1 = [0xB0, 71] # CC71 +KNOB_2 = [0xB0, 72] # CC72 +KNOB_3 = [0xB0, 73] +KNOB_4 = [0xB0, 74] +KNOB_5 = [0xB0, 75] # CC75 +KNOB_6 = [0xB0, 76] # CC76 +KNOB_7 = [0xB0, 77] # CC77 +KNOB_8 = [0xB0, 78] # CC78 +KNOB_9 = [0xB0, 79] # CC79 + +# Touch +KNOB_1_T = [0x90,0] # "C-1" sic! +KNOB_2_T = [0x90,1] # "C#-1" +KNOB_3_T = [0x90,2] # "D-1" +KNOB_4_T = [0x90,3] # "D#-1" +KNOB_5_T = [0x90,4] # Note 4 +KNOB_6_T = [0x90,5] # Note 5 +KNOB_7_T = [0x90,6] # note 6 +KNOB_8_T = [0x90,7] # note 7 +KNOB_9_T = [0x90,8] # Note 8 + +RIBBON_TOUCH_T = [0x90,12] # "C0" note 12 +RIBBON_PITCH = [0xE0] # Mod-wheel ?? ### Achtung einziger Identifier, der nur 1 byte hat!!! ### + +# Monochromatic Buttons +# Alle Button sind CC / Alle PAD sind Noteon +BTN_TAP_TEMPO = [0xB0, 3] +BTN_METRONOME = [0xB0, 9] + + +BTN_FIXED_LENGTH = [0xB0, 90] +BTN_AUTOMATION = [0xB0, 89] +BTN_DUPLICATE = [0xB0, 88] + +BTN_NEW = [0xB0, 87] +BTN_REC = [0xB0, 86] +BTN_START = [0xB0, 85] + +######### RECHTS ############ +BTN_PAN = [0xB0, 115] # CC115 +BTN_VOLUME = [0xB0, 114] # CC114 + +BTN_CLIP = [0xB0, 113] +BTN_TRACK = [0xB0, 112] + +BTN_BROWSE = [0xB0, 111] +BTN_DEVICE = [0xB0, 110] + + +BTN_ESC = [0xB0, 63] +BTN_OK = [0xB0, 62] +BTN_SOLO = [0xB0, 61] +BTN_MUTE = [0xB0, 60] +BTN_USER = [0xB0, 59] +BTN_SCALES = [0xB0, 58] +BTN_ACCENT = [0xB0, 57] +BTN_REPEAT = [0xB0, 56] +BTN_OCTAVE_UP = [0xB0, 55] +BTN_OCTAVE_DOWN = [0xB0, 54] + +BTN_ADD_TRACK = [0xB0, 53] +BTN_ADD_EFFECT = [0xB0, 52] +BTN_SESSION = [0xB0, 51] +BTN_NOTE = [0xB0, 50] +BTN_SHIFT = [0xB0, 49] +BTN_SELECT = [0xB0, 48] + +BTN_UP = [0xB0, 46] +BTN_DOWN = [0xB0, 47] +BTN_LEFT = [0xB0, 44] +BTN_RIGHT = [0xB0, 45] + +BTN_QUATER = [0xB0, 36] +BTN_QUATER_T = [0xB0, 37] +BTN_EIGHTH = [0xB0, 38] +BTN_EIGHTH_T = [0xB0, 39] +BTN_SIXTEENTH = [0xB0, 40] +BTN_SIXTEENTH_T = [0xB0, 41] +BTN_THIRTYSECOND = [0xB0, 42] +BTN_THIRTYSECOND_T = [0xB0, 43] + +BTN_MASTER = [0xB0, 28] +BTN_STOP = [0xB0, 29] + +# Bicolor Buttons in the middle, below the display +# They have two colors, red and green. +BTN_R1_C1 = [0xB0, 20] +BTN_R1_C2 = [0xB0, 21] +BTN_R1_C3 = [0xB0, 22] +BTN_R1_C4 = [0xB0, 23] +BTN_R1_C5 = [0xB0, 24] +BTN_R1_C6 = [0xB0, 25] +BTN_R1_C7 = [0xB0, 26] +BTN_R1_C8 = [0xB0, 27] + +BTN_R2_C1 = [0xB0, 102] +BTN_R2_C2 = [0xB0, 103] +BTN_R2_C3 = [0xB0, 104] +BTN_R2_C4 = [0xB0, 105] +BTN_R2_C5 = [0xB0, 106] +BTN_R2_C6 = [0xB0, 107] +BTN_R2_C7 = [0xB0, 108] +BTN_R2_C8 = [0xB0, 109] + +# Have RGB-LED +PAD_36 = [0x90, 36] # note +PAD_37 = [0x90, 37] # note +PAD_38 = [0x90, 38] # note +PAD_39 = [0x90, 39] # note +PAD_40 = [0x90, 40] # note +PAD_41 = [0x90, 41] # note +PAD_42 = [0x90, 42] # note +PAD_43 = [0x90, 43] # note + +PAD_44 = [0x90, 44] # note +PAD_45 = [0x90, 44] # note +PAD_46 = [0x90, 45] # note +PAD_47 = [0x90, 46] # note +PAD_48 = [0x90, 47] # note +PAD_49 = [0x90, 48] # note +PAD_50 = [0x90, 50] # note +PAD_51 = [0x90, 51] # note + +PAD_52 = [0x90, 52] # note +PAD_53 = [0x90, 53] # note +PAD_54 = [0x90, 54] # note +PAD_55 = [0x90, 55] # note +PAD_56 = [0x90, 56] # note +PAD_57 = [0x90, 57] # note +PAD_58 = [0x90, 58] # note +PAD_59 = [0x90, 59] # note + +PAD_60 = [0x90, 60] # note +PAD_61 = [0x90, 61] # note +PAD_62 = [0x90, 62] # note +PAD_63 = [0x90, 63] # note +PAD_64 = [0x90, 64] # note +PAD_65 = [0x90, 65] # note +PAD_66 = [0x90, 66] # note +PAD_67 = [0x90, 67] # note + +PAD_68 = [0x90, 68] # note +PAD_69 = [0x90, 69] # note +PAD_70 = [0x90, 70] # note +PAD_71 = [0x90, 71] # note +PAD_72 = [0x90, 72] # note +PAD_73 = [0x90, 73] # note +PAD_74 = [0x90, 74] # note +PAD_75 = [0x90, 75] # not + +PAD_76 = [0x90, 76] # note +PAD_77 = [0x90, 77] # note +PAD_78 = [0x90, 78] # note +PAD_79 = [0x90, 79] # note +PAD_80 = [0x90, 80] # note +PAD_81 = [0x90, 81] # note +PAD_82 = [0x90, 82] # note +PAD_83 = [0x90, 83] # note + +PAD_84 = [0x90, 84] # note +PAD_85 = [0x90, 85] # note +PAD_86 = [0x90, 86] # note +PAD_87 = [0x90, 87] # note +PAD_88 = [0x90, 88] # note +PAD_89 = [0x90, 89] # note +PAD_90 = [0x90, 90] # note +PAD_91 = [0x90, 91] # note + +PAD_92 = [0x90, 92] # note +PAD_93 = [0x90, 96] # note +PAD_94 = [0x90, 94] # note +PAD_95 = [0x90, 95] # note +PAD_96 = [0x90, 96] # note +PAD_97 = [0x90, 97] # note +PAD_98 = [0x90, 98] # note +PAD_99 = [0x90, 99] # note + + +## from pushmod.blospot.com +#### PUSH 1 SYSEX ####################################### + +# 71 is the manufacturer ID (Akai Electric Co. Ltd.) +# 127 is the device ID (default it 127 - All Devices) +# 21 is the product ID (Push) +# The Device ID can be sent as 0 as well. + +# Identity request 240,126,0,6,1,247 +# Set pad color (RGB) 240,71,127,21,4,0,8,,0,,,,,,,247 +# Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 +# Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 +# Set key aftertouch 240,71,127,21,92,0,1,0,247 +# Set channel aftertouch 240,71,127,21,92,0,1,1,247 +# Set Live version 240,71,127,21,96,0,4,65,,,,247 +# Set Live mode 240,71,127,21,98,0,1,0,247 +SYSEX_DATA_SET_LIVE_MODE= [240,71,127,21,98,0,1,0,247] + +# Set User mode 240,71,127,21,98,0,1,1,247 +SYSEX_DATA_SET_USER_MODE= [240,71,127,21,98,0,1,1,247] + +# Set touch strip mode 240,71,127,21,99,0,1,,247 +# Request white calibration information 240,71,127,21,107,0,0,247 +# Contrast request 240,71,127,21,122,0,0,247 +# Contrast set 240,71,127,21,122,0,1,, 247 +# Brightness request 240,71,127,21,124,0,0,247 +# Brightness set 240,71,127,21,124,0,1,,247 +######### END PUSH 1 SYSEX ############################ + +### Monochromatic Keys/Pads ####################### +MONO_LED_OFF = 0 # 0 - Off +MONO_LED_DIM = 1 # 1 - Dim +MONO_LED_DIM_BLINK = 2 # 2 - Dim Blink +MONO_LED_DIM_BLINK_FAST = 3 # 3 - Dim Blink Fast +MONO_LED_LIT = 4 # 4 - Lit +MONO_LED_LIT_BLINK = 5 # 5 - Lit Blink +MONO_LED_LIT_BLINK_FAST = 6 # 6 - Lit Blink Fast +# 7 -> 127 - Lit +######### END MONOCHROMATIC LED ################## + +#Bi-color LED table +#These are the colors which will be set on the bi-color (red/green) buttons below display + +BI_LED_OFF = 0 # 0 - Off (Black) +BI_RED_DIM = 1 # 1 - Red Dim +BI_RED_DIM_BLINK = 2 # 2 - Red Dim Blink +BI_RED_DIM_BLINK_FAST = 3 # 3 - Red Dim Blink Fast +BI_RED = 4 # 4 - Red +BI_RED_BLINK = 5 # 5 - Red Blink +BI_RED_BLINK_FAST = 6 # 6 - Red Blink Fast +BI_ORANGE_DIM = 7 # 7 - Orange Dim +BI_ORANGE_DIM_BLINK = 8 # 8 - Orange Dim Blink +BI_ORANGE_DIM_BLINK_FAST = 9 # 9 - Orange Dim Blink Fast +BI_ORANGE = 10 # 10 - Orange +BI_ORANGE_BLINK = 11 # 11 - Orange Blink +BI_ORANGE_BLINK_FAST = 12 # 12 - Orange Blink Fast +BI_YELLOW_DIM = 13 # 13 - Yellow (Lime) Dim +BI_YELLOW_DIM_BLINK = 14 # 14 - Yellow Dim Blink +BI_YELLOW_DIM_BLINK_FAST = 15 # 15 - Yellow Dim Blink Fast +BI_YELLOW = 16 # 16 - Yellow (Lime) +BI_YELLOW_BLINK = 17 # 17 - Yellow Blink +BI_YELLOW_BLINK_FAST = 18 # 18 - Yellow Blink Fast +BI_GREEN_DIM = 19 # 19 - Green Dim +BI_GREEN_DIM_BLINK = 20 # 20 - Green Dim Blink +BI_GREEN_DIM_BLINK_FAST = 21 # 21 - Green Dim Blink Fast +BI_GREEN = 22 # 22 - Green +BI_GREEN_BLINK = 23 # 23 - Green Blink +BI_GREEN_BLINK_FAST = 24 # 24 - Green Blink Fast +#25 -> 127 - Green \ No newline at end of file From 03310978d20666dccdb1f58c0a1b8d8f8ca539f6 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 10 Sep 2025 17:04:03 +0200 Subject: [PATCH 11/57] Mixer states to the display. first tries --- zyngine/ctrldev/ableton/push1_consts.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zyngine/ctrldev/ableton/push1_consts.py b/zyngine/ctrldev/ableton/push1_consts.py index 677d4df14..97d235ccf 100644 --- a/zyngine/ctrldev/ableton/push1_consts.py +++ b/zyngine/ctrldev/ableton/push1_consts.py @@ -206,7 +206,7 @@ PAD_99 = [0x90, 99] # note -## from pushmod.blospot.com +## from pushmod.blosgpot.com #### PUSH 1 SYSEX ####################################### # 71 is the manufacturer ID (Akai Electric Co. Ltd.) @@ -274,4 +274,6 @@ BI_GREEN = 22 # 22 - Green BI_GREEN_BLINK = 23 # 23 - Green Blink BI_GREEN_BLINK_FAST = 24 # 24 - Green Blink Fast -#25 -> 127 - Green \ No newline at end of file +#25 -> 127 - Green + + From 57fc7520b3da811324f1f7f3dbd0da13790df281 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 10 Sep 2025 17:06:49 +0200 Subject: [PATCH 12/57] all vars to the class zynthian_ctrldev_base_scale --- zyngine/ctrldev/zynthian_ctrldev_base_scale.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index 3e2d0b25d..1fdfa951c 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -66,8 +66,8 @@ def get_midi_note(self, scale,start) -> int: So you ca cycle through the number of keyboard keys to get their midi notes """ try: - print(scales[scale]) - l = scales[scale] + print(self.scales[scale]) + l = self.scales[scale] pos = start%len(l) octave = start // len(l) print(f"[Debug note]: scale={scale}, start={start} pos={pos}, erg={l[pos]} von {l}") @@ -78,12 +78,12 @@ def get_midi_note(self, scale,start) -> int: def get_scale_len(self, scale) -> int: try: - print(f"[Debug len]: scale={scale}, len={len(scales[scale])}") - return len(scales[scale]) + print(f"[Debug len]: scale={scale}, len={len(self.scales[scale])}") + return len(self.scales[scale]) except: print("Error: get_scale_len: Tonart nicht definiert!") def get_scale_names(self): - return list(scales) + return list(self.scales) From 9bcd9be0224578b98beb250bf77089ee8664efe7 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 10 Sep 2025 17:08:25 +0200 Subject: [PATCH 13/57] cleaned up src, Consts now in external library --- .../zynthian_ctrldev_ableton_push_1.py | 482 +++++++++--------- 1 file changed, 237 insertions(+), 245 deletions(-) mode change 100644 => 100755 zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py old mode 100644 new mode 100755 index e447c6078..c40cd4996 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -1,181 +1,45 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -# lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, 0) -# lib_zyncore.dev_send_midi_event(self.idev_out, sysex_data, len(sysex_data)) -# lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) - -# 20250821: Brumby INITIAL Version copied from Launchpad Mini Mk3. -# Ergolge: -# 1. Device wird erkannt und die einzelnen Functionen werden aufgerufen -# 2. Es werden Session angezeigt, Sogar mit den 4 verschiedenen Farben. Allerdings an falscher Stelle! -# 3. Die Shifttaste leuchtet, wenn man sie drückt! -### -# 20250828 -# 4. Pad-Anzeige funktioniert. Es kann noch nicht geschaltet werden. Farben sind wohl auch nicht ganz richtig -# 5. Start und Stop geht, aber die oben und Unten sind vertauscht -### -# 250829 -# 6 Zustand von 4 wieder erreicht. -# 5 geht nicht mehr! -# 7 Zustand von 5 ist wieder erreicht. Toggle row ist unten statt oben. -### -# 8 Ergolg Pads funktionieren!. Allerdings lassen sich unbelegte Platze einschalten. -# 9 Die Gui Potis funktionieren -# Erinnerung: Die Potis CC71-CC79 haben auch ein Touchfunktion. Allerdings mit anfassen des Knopfes und Loslassen. -# Das können wir deshalb nicht als keypress verwenden. -# Die Tasten unter dem Display aber schon! -# 10 poti 0-3 werden als Zypod 1-4 erkannt -# todo Tasten unterhalb des Displays als ZYNPOD-Buttons -# 11 ABL_OK und ABL_ESC funktionieren nicht. -# -- ABL_OK hat nicht den richtigen GUISTEUERWERT -# -- ABL_ESC wird nicht mit der Taste angesteuert -# 12 ABL_OK und ABL_ESC funktionieren. -# -# Bemerkung: Alle Tasten, die eine Funktion haben, leuchten. Es wird aber KEIN Leuchstatus gelöscht, wenn er schon gesetzt war! -# -# todo Display mit Sysex ansteuern +#! /zynthian/venv/bin/python -# 250830 0310 -# 13 Ich kann die Statusmeldung überschreiben und aus irgendeinem Grund wird Hallo Welt ausgegeben. - -# 20250831-0057 -# Das Display funktioniert. Ich kann Texte beliebig positionieren und den Bildschirm löschen. -#20250901-0015 -#Sicherung funktionsfähig +# -*- coding: utf-8 -*- +# 20250910-0044 +# Alle Konstanten ausgelagert. #### #### #### -## from pushmod.blospot.com -#### PUSH 1 SYSEX ####################################### - -# 71 is the manufacturer ID (Akai Electric Co. Ltd.) -# 127 is the device ID (default it 127 - All Devices) -# 21 is the product ID (Push) -# The Device ID can be sent as 0 as well. - -# Identity request 240,126,0,6,1,247 -# Set pad color (RGB) 240,71,127,21,4,0,8,,0,,,,,,,247 -# Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 -# Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 -# Set key aftertouch 240,71,127,21,92,0,1,0,247 -# Set channel aftertouch 240,71,127,21,92,0,1,1,247 -# Set Live version 240,71,127,21,96,0,4,65,,,,247 -# Set Live mode 240,71,127,21,98,0,1,0,247 -SYSEX_DATA_SET_LIVE_MODE= [240,71,127,21,98,0,1,0,247] - -# Set User mode 240,71,127,21,98,0,1,1,247 -SYSEX_DATA_SET_USER_MODE= [240,71,127,21,98,0,1,1,247] - -# Set touch strip mode 240,71,127,21,99,0,1,,247 -# Request white calibration information 240,71,127,21,107,0,0,247 -# Contrast request 240,71,127,21,122,0,0,247 -# Contrast set 240,71,127,21,122,0,1,, 247 -# Brightness request 240,71,127,21,124,0,0,247 -# Brightness set 240,71,127,21,124,0,1,,247 -######### END PUSH 1 SYSEX ############################ - - -### Monochromatic Keys/Pads ####################### -# 0 - Off -ABL_KEY_LED_OFF = 0 -# 1 - Dim -ABL_KEY_LED_DIM = 1 -# 2 - Dim Blink -ABL_KEY_LED_DIM_BLINK = 2 -# 3 - Dim Blink Fast -ABL_KEY_LED_DIM_BLINK_FAST = 3 -# 4 - Lit -ABL_KEY_LED_LIT = 4 -# 5 - Lit Blink -ABL_KEY_LED_LIT_BLINK = 5 -# 6 - Lit Blink Fast -ABL_KEY_LED_LIT_BLINK_FAST = 6 -# 7 -> 127 - Lit -######### END MONOCHROMATIC LED ################## - -#Bi-color LED table -#These are the colors which will be set on the bi-color (red/green) buttons: - -ABL_BI_LED_OFF = 0 # 0 - Off (Black) -ABL_BI_RED_DIM = 1 # 1 - Red Dim -ABL_BI_RED_DIM_BLINK = 2 # 2 - Red Dim Blink -ABL_BI_RED_DIM_BLINK_FAST = 3 # 3 - Red Dim Blink Fast -ABL_BI_RED = 4 # 4 - Red -# 5 - Red Blink -ABL_BI_RED_BLINK = 5 -# 6 - Red Blink Fast -ABL_BI_RED_BLINK_FAST = 6 -# 7 - Orange Dim -ABL_BI_ORANGE_DIM = 7 -# 8 - Orange Dim Blink -ABL_BI_ORANGE_DIM_BLINK = 8 -# 9 - Orange Dim Blink Fast -ABL_BI_ORANGE_DIM_BLINK_FAST = 9 -#10 - Orange -ABL_BI_ORANGE = 10 -#11 - Orange Blink -ABL_BI_ORANGE_BLINK = 11 -#12 - Orange Blink Fast -ABL_BI_ORANGE_BLINK_FAST = 12 -#13 - Yellow (Lime) Dim -ABL_BI_YELLOW_DIM = 13 -#14 - Yellow Dim Blink -ABL_BI_YELLOW_DIM_BLINK = 14 -#15 - Yellow Dim Blink Fast -ABL_BI_YELLOW_DIM_BLINK_FAST = 15 -#16 - Yellow (Lime) -ABL_BI_YELLOW = 16 -#17 - Yellow Blink -ABL_BI_YELLOW_BLINK = 17 -#18 - Yellow Blink Fast -ABL_BI_YELLOW_BLINK_FAST = 18 -#19 - Green Dim -ABL_BI_GREEN_DIM = 19 -#20 - Green Dim Blink -ABL_BI_GREEN_DIM_BLINK = 20 -#21 - Green Dim Blink Fast -ABL_BI_GREEN_DIM_BLINK_FAST = 21 -#22 - Green -ABL_BI_GREEN = 22 -#23 - Green Blink -ABL_BI_GREEN_BLINK = 23 -#24 - Green Blink Fast -ABL_BI_GREEN_BLINK_FAST = 24 -#25 -> 127 - Green # note werte Push 1 Midimapping -ABL_PAD_START = 36 -ABL_PAD_END = 99 +# don't delete. +ABL_PAD_START = 36 # 1. Pad = pad_36 +ABL_PAD_END = 99 # letztes Paad = pad_99 -# CC Werte der Tasten -ABL_REC = 86 -ABL_PLAY = 85 +# # CC Werte der Tasten +# ABL_REC = 86 +# # ABL_PLAY = 85 -ABL_OK = 62 -ABL_ESC = 63 # Abbruch, Zurück +# ABL_OK = 62 +# ABL_ESC = 63 # Abbruch, Zurück -ABL_TRACK = 112 +# ABL_TRACK = 112 -ABL_ARROW_LEFT = 44 -ABL_ARROW_RIGHT = 45 -ABL_ARROW_UP = 46 -ABL_ARROR_DOWN = 47 -ABL_SHIFT = 49 +# ABL_ARROW_LEFT = 44 +# ABL_ARROW_RIGHT = 45 +# ABL_ARROW_UP = 46 +# ABL_ARROR_DOWN = 47 +# ABL_SHIFT = 49 -# DISPLAY Buttons Row 1 -ABL_BUTTON_DISPL_R1_0 = 20 -# ... -ABL_BUTTON_DISPL_R1_8 = 27 -# Display Buttons Row 2 -ABL_BUTTON_DISPL_R2_0 = 102 -# ... -ABL_BUTTON_DISPL_R2_8 = 109 +# # DISPLAY Buttons Row 1 +# ABL_BUTTON_DISPL_R1_0 = 20 +# # ... +# ABL_BUTTON_DISPL_R1_8 = 27 +# # Display Buttons Row 2 +# ABL_BUTTON_DISPL_R2_0 = 102 +# # ... +# ABL_BUTTON_DISPL_R2_8 = 109 @@ -206,12 +70,18 @@ import logging -# Brumbs imports -#from ableton_push1_display import Push1Display -from time import sleep +# Brumbys imports +from time import sleep # pause between sysex events. +import sys # for button detection +# vor editor use following. +# import ableton.push1_consts as ABL + +# for running driver this way: +import zyngine.ctrldev.ableton.push1_consts as ABL + # Zynthian specific modules -from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad # , zynthian_ctrldev_zynmixer +from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer from zyncoder.zyncore import lib_zyncore from zynlibs.zynseq import zynseq @@ -219,8 +89,10 @@ # Ableton Push 1 # ------------------------------------------------------------------------------------------------------------------ +# zynthian_ctrldev_zynpad is class for Controlling the sequencer with pads +#zynthian_ctrldev_zynmixer cpntrolls the main mixer -class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad): # , zynthian_ctrldev_zynmixer): +class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer): logging.error("Klassenaufruf - Ableton Push 1 - BRUMBY") @@ -247,7 +119,7 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad): # , zynthia # Function to initialise class def __init__(self, state_manager, idev_in, idev_out=None): - logging.info("__init__ Ableton Push 1 - BRUMBY") + logging.info("Created Instance from Ableton Push 1 driver - BRUMBY") self.shift = False super().__init__(state_manager, idev_in, idev_out) @@ -267,8 +139,8 @@ def __init__(self, state_manager, idev_in, idev_out=None): sleep(0.1) # Positionierungshilfe -# self.display.write_xy(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) -# sleep(0.1) + self.display.write_xy(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) + sleep(0.1) self.display.write_xy(b'** Zynthian Push1Driver 0.1 **', 17,2) sleep(0.1) @@ -276,26 +148,29 @@ def __init__(self, state_manager, idev_in, idev_out=None): self.display.write_xy(b'++ Make MusicNot War ++', 20,3) def init(self): - logging.error("init Ableton Push 1 - BRUMBY") + logging.info("called init. Setting up Ableton Push 1 - BRUMBY") # Hier muss die Trackstaste zum Leuchten gebracht werden am Push 1 # Enable session mode on launchkey # Track-Taste CC112 # ABL_TRACK - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 112, ABL_KEY_LED_LIT) # 2! + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 112, ABL.MONO_LED_LIT) # 2! # CC62 = OK; CC63 = Back (ABL_OK, ABL_ESC - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 62, ABL_KEY_LED_LIT_BLINK) # 2! + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 62, ABL.MONO_LED_LIT_BLINK) # 2! # Monochrome Tasten die hell leuchten sollen - for t in [ 36,37,38,39,40,41,42,43, ABL_PLAY, ABL_OK, ABL_ESC, ABL_ARROW_LEFT, ABL_ARROW_RIGHT, ABL_ARROW_UP, ABL_ARROR_DOWN]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL_KEY_LED_LIT) # 2! - - # monochrome Tasten die dim leuchten sollen - for t in [ABL_REC, ABL_SHIFT]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL_KEY_LED_DIM) # 2! + for t in [ 36,37,38,39,40,41,42,43, + ABL.BTN_START[1], ABL.BTN_OK[1], ABL.BTN_ESC[1], ABL.BTN_LEFT[1], + ABL.BTN_RIGHT[1], ABL.BTN_UP[1], ABL.BTN_DOWN[1], ABL.BTN_SCALES[1] ]: + + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_LIT) - # Bicolortasten die dim leuchten sollen CC20-27 + 102-109 + # monochrome Buttons than should be dim state + for t in [ ABL.BTN_REC[1], ABL.BTN_SHIFT[1] ]: # ,ABL_REC, ABL_SHIFT]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) + + # Bicolor LEDs dim ## CC20-27 + 102-109 for t in [21, 23]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, 13) # 2! + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_ORANGE_DIM) ### lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) self.cols = 8 @@ -310,7 +185,30 @@ def end(self): ### Disable session mode on launchkey ## lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 0) # device, channel, note, velocity +### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. +### just copy the derived functions in the this driver and implement them accordingly + def update_mixer_active_chain(self, active_chain): + """Update hardware indicators for active_chain""" + logging.error(f"not implemented active_chain: {active_chain}") + + def update_mixer_strip(self, chan, symbol, value): + """Update hardware indicators for a mixer strip: mute, solo, level, balance, etc. # oh my goodness, what means etc. ? + *SHOULD* be implemented by child class + + chan - Mixer strip index + symbol - Control name + value - Control value + + Idiea for display + ||||||||||| = lefel indicator + M S L B = M=Mute; S=Solo L=changing the Lefel; B=changing balance; But what else ??? + """ + logging.debug( + f"Update mixer strip for {type(self).__name__}: NOT IMPLEMENTED! chan: {chan}; symbol: {symbol} value: {value}") +### END of Mixer functions. + +### Start of SEQUENCER FUNCTIONS # this function is called by zynseq when a sequencer state is changed # we will update pad LED to show state def update_seq_state(self, bank, seq, state, mode, group): @@ -363,19 +261,25 @@ def update_seq_state(self, bank, seq, state, mode, group): def pad_off(self, col, row): # note = 96 + row * 16 + col # statt 96 -> 91 für Push - note = ABL_PAD_END +1 -(row+1) * 8 + col - logging.error(f"BRUMBY: row={row}; col={col} pad-note={note}") + note = ABL_PAD_END +1 -(row+1) * 8 + col # recalculate midi note from col and row + # logging.info(f"BRUMBY: row={row}; col={col} pad-note={note}") lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0) +### End of derived Sequencer Functions. + + # Just for me a helper function to set all pads off def pads_off(self): dbg = False - logging.error("BRUMBY: pas_off") + logging.debug("BRUMBY: pads_off") for row in range(self.rows): for col in range(self.cols): self.pad_off(col, row) def midi_event(self, ev): - logging.error(f"midi_event Ableton Push 1 - BRUMBY {ev}") + logging.debug(f"midi_event Ableton Push 1 - BRUMBY {ev}") + btn_name = self.button_name_from_midi_event(ev) + + evtype = (ev[0] >> 4) & 0x0F # evtype= EV_NOTE_ON @@ -383,18 +287,13 @@ def midi_event(self, ev): note = ev[1] & 0x7F # das ist überflüssig, weil note immer < 127 ist - # Alle Noteevents ausfiltern, die nicht von den Pads kommen + # filter every note on not from pads. knobs have, when touched also note_on messages if note < ABL_PAD_START: return True if note > ABL_PAD_END: return True # ignore every note_on not from pads - logging.error(f"BRUMBY: note={note}") - - # Entered session mode so set pad LEDs - # QUESTION: What kind of message is this? Only SysEx messages can be bigger than 3 bytes. - # if ev == b'\x90\x90\x0C\x7F': - # self.update_seq_bank() + logging.debug(f"BRUMBY: note on event with note={note}") # Toggle pad # Hier wird der midi-Notenwert in einen x,y Wert umgewandelt, um die Sequencer-Bank entsprechend zu toggeln. @@ -402,20 +301,20 @@ def midi_event(self, ev): if self.pad_mode_active == self.PAD_MODE_SCALES: # hier muss er Translator für scales hin! logging.error(f"midi_event Ableton Push 1 - BRUMBY: PAD in SCALES mode - not implemented yet") - return False + # for note_on events following. + #zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_ON, + # izmip=izmip, chan=chan, note=ev[1] & 0x7f, vel=ev[2] & 0x7f) + self.set_pad_rgb(note-36, 205,25,255) + return False # let caller process event - - - + if self.pad_mode_active == self.PAD_MODE_SEQ: try: - # BRUMBY_NEU - col = (note - ABL_PAD_START) // 8 # statt 96 -> 91 row = (note - ABL_PAD_START) % 8 # Statt 96 -> 91 # row = 7 - row; - col = 7 - col; + col = 7 - col; # midi notes start from bottom, so recalculate row pad = row * self.zynseq.col_in_bank + col logging.error(f"midi_event 1 MEINER Ableton Push 1 - BRUMBY: row={row}; col={col}; pad={pad}") @@ -434,122 +333,215 @@ def midi_event(self, ev): ccnum = ev[1] & 0x7F ccval = ev[2] & 0x7F - # Der Status der Schift taste CC49 wird abgefragt. CCVall <> 0 heisst gedruückt. Andernfalls losgelassen - if ccnum == 49: + # Sate of shoft button CC49 wird abgefragt. CCVall > 0 means pressed + if ccnum == ABL.BTN_SHIFT[1]: + # if ccnum == 49: # SHIFT - self.shift = ccval != 0 + self.shift = ccval != 0 # set shift variable + # visual feedback with button LED if self.shift: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL_KEY_LED_LIT_BLINK) + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_LIT_BLINK) else: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL_KEY_LED_DIM) - return True - - # Jetzt wird alles ausgefiltert, das den Wert 0 hat, damit das loslassen der Taste als CC ausgefiltert wird + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_DIM) + return True # event processed. No further action required + + # From here filter any event with velocyty=0 We just need pressed vaues to come through elif ccnum == 0 or ccval == 0: - return True + return False # Warning: With "return True" no further processing in zynthian. + # So no Controlchange with data=0 gets through to zynthian. + # Is that, what we want ??? + # Also bank changes are msb or isit LSB are filtered waay. + # I assume, it has to return False, so Zynthian can do bank chanages !!! - # Ab hier kann mann die Tastendrücke auswerten + # From here only positive Values are processed! + # Displays bi-color Buttons elif (self.shift and 20 < ccnum < 29) or (20 < ccnum < 25): - chain = self.chain_manager.get_chain_by_position( - ccnum - 21, midi=False) + chain = self.chain_manager.get_chain_by_position(ccnum - 21, midi=False) if chain and chain.mixer_chan is not None and chain.mixer_chan < 17: - self.zynmixer.set_level(chain.mixer_chan, ccval / 127.0) + self.zynmixer.set_level(chain.mixer_chan, ccval / 127.0) # "/127.0" creates a float val from 0.0 .. 1.0 + + # This swtches between this drivers pad states: Sequencer and Scales + elif (ccnum == ABL.BTN_SCALES[1]): + logging.info("BRUMBY: BTN_SCALES") + if self.pad_mode_active != self.PAD_MODE_SCALES: + self.pad_mode_active = self.PAD_MODE_SCALES + # visual feedback, let Scales Button blink + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) + self.pads_off() # akk pad leds off + else: + self.pad_mode_active = self.PAD_MODE_SEQ + # visual feedback, set LED to solid on + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) + self.pads_off() # clean up visible state. all pad leds off + self.refresh() # refreshe LEDs for Sequencer mode of this driver. + + return True + + # Gui events ausgelagert zu: + if self.process_gui_events(ev): return True + + + # evtype = MIDI_Program Change + elif evtype == 0xC: + val1 = ev[1] & 0x7F + self.zynseq.select_bank(val1 + 1) + return True + + # default return, when no match + return False # When nothing matches, False shows that midi event has to be processed further + + # to clean up the code GUI events are processed here + def process_gui_events(self,ev) -> bool: + + if ev[0] & 0xF0 == 0xB0: # event is midi CC ? + ccnum = ev[1] & 0x7F + ccval = ev[2] & 0x7F + + # Zynpoties Werte an GUI # Potis Oben 72 - 75 die ersten 4 - elif 70 < ccnum < 80: + # if 70 < ccnum < 80: + if ABL.KNOB_1[1] <= ccnum <= ABL.KNOB_4[1]: # self.state_manager.send_cuia("ZYNPOT_ABS", [ccnum - 72, ccval/127]) val = ccval if val > 68: val = (val - 128) # falsch geraten, nicht ZYNPT_REL. Vielleicht ZYNPOT? self.state_manager.send_cuia("ZYNPOT", [ccnum - 71, val]) - logging.error(f"BRUMBY: Poti={ccnum-71} val={val}") + logging.debug(f"BRUMBY: Poti={ccnum-71} val={val}") return True - elif (ccnum == ABL_OK) or (ccnum == 23): - logging.error("ABL_OK BRUMBY") + elif (ccnum == ABL.BTN_OK[1]) or (ccnum == 23): + logging.debug("ABL_OK BRUMBY") self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) return True - elif ccnum == 21: - logging.error("ZYNPUT_BUT 1 ESC BRUMBY") + + # elif ccnum == 21: Does that work? + elif ccnum == ABL.BTN_R1_C2[1]: # Zweiter Button unter dem Display + logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) return True - elif ccnum == ABL_ESC: - logging.error("ABL_ESC BRUMBY") + elif ccnum == ABL.BTN_ESC[1]: + # logging.debug("BTN_ESC BRUMBY") self.state_manager.send_cuia("BACK") return True - - elif ccnum == 45: # BRUMBY + # elif ccnum == 45: + elif ccnum == ABL.BTN_RIGHT[1]: # elif ccnum == 0x66: # TRACK RIGHT self.state_manager.send_cuia("ARROW_RIGHT") return False - elif ccnum == 44: + + # elif ccnum == 44: + elif ccnum == ABL.BTN_LEFT[1]: # elif ccnum == 0x67: # TRACK LEFT self.state_manager.send_cuia("ARROW_LEFT") return False - elif ccnum == 46: + + elif ccnum == ABL.BTN_UP[1]: # CC46 # elif ccnum == 0x68: # UP self.state_manager.send_cuia("ARROW_UP") return True - elif ccnum == 47: - # elif ccnum == 0x69: + + elif ccnum == ABL.BTN_DOWN[1]: + # elif ccnum == 47: # DOWN self.state_manager.send_cuia("ARROW_DOWN") return True - elif ccnum == ABL_PLAY: + + elif ccnum == ABL.BTN_START[1]: # ehemals ABL_PLAY: # PLAY if self.shift: self.state_manager.send_cuia("TOGGLE_MIDI_PLAY") else: self.state_manager.send_cuia("TOGGLE_PLAY") return True - elif ccnum == ABL_REC: + + + elif ccnum == ABL.BTN_REC[1]: # ABL_REC: # RECORD if self.shift: self.state_manager.send_cuia("TOGGLE_MIDI_RECORD") else: self.state_manager.send_cuia("TOGGLE_RECORD") return True + + + # These are the note_length Buttons right of pads in Sequencer mode to start and stop a whole row of sequences elif (ccnum > 35) and (ccnum < 44): self.zynseq.select_bank (8- (ccnum - 36)) # Leuchstatus ändern for t in [ 36,37,38,39,40,41,42,43]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL_KEY_LED_DIM) # 2! - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL_KEY_LED_LIT_BLINK_FAST) # 2! + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) # 2! + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL.MONO_LED_LIT_BLINK_FAST) # 2! return True - - # evtype = MIDI_PC ?? - elif evtype == 0xC: - val1 = ev[1] & 0x7F - self.zynseq.select_bank(val1 + 1) ## wahrscheinlich wird hier update_seq_state aufgerufen - return True - return False - + return False # event is not processed + + def button_name_from_midi_event(self, ev): ###, button_event): # button_event is a Constant from import abl. + # create key_data from midi event + # if too slow, we have to revert the array to a named array with buttonevent as name + if len(ev) < 2 : return None # event is too short to get two byte data. Time event or so? + if len(ev) > 2: data = ev[2] + search_key = [ ev[0] & 0xF0, ev[1] ] + for name in dir(ABL): # all vars as textstring + if not name.startswith('__'): # no attributes with '__' + attr = getattr(ABL, name) + if attr == search_key and name.isupper(): + logging.debug(f"midi_event {ev} from Button with name: {name} and value: {data}") + return name + logging.debug(f"midi_event {ev} from Button not defined with value: {data}") + return None + + + def set_pad_rgb(self, pad_nr, r,g,b): + # Sysex : 240,71,127,21,4,0,8,,0,,,,,,,247 + # pad = 0-71 NICHT PAD_36 - PAD_99 + # blogspot.com + # To set a pad color to a RGB(0-255) value, the RGB values need to be set into "Push" format, for example: + # r1 = r /(integer division) 16 + # r2 = r %(modulo) 16 + # So a value of R132 would become: r1=8 r2=4. + # The pad index if from 0 to 71, zero being the bottom left pad, all the way up to the second row button to the right (second row of buttons is RGB). + if r > 255: r = 255; + if r < 0: r = 0 + if g > 255: g = 255; + if g < 0: g = 0 + if b > 255: b = 255; + if b < 0: b = 0 + if not 0 <= pad_nr <= 64: + logging.error(f"Padnr wrong. not in 1..64 pad_nr = {pad_nr}") + return False + + r1= r // 16 ; r2= r % 16 + g1= g // 16 ; g2= g % 16 + b1= b // 16 ; b2= b % 16 + sysex = bytes ([240,71,127,21,4,0,8,pad_nr,0,r1,r2,g1,g2,b1,b2,247] ) + lib_zyncore.dev_send_midi_event(self.idev_out, sysex, len(sysex)) -# def send_sysex(self, data): + def send_sysex(self, data): return # Send SysEx universal inquiry. # It's answered by some devices with a SysEx message. # def send_sysex_universal_inquiry(self): if self.idev_out > 0: - #msg = bytes(SYSEX_DATA_SET_USER_MODE) + + #msg = bytes(ABL.SYSEX_DATA_SET_USER_MODE) #logging.error(f"BRUMBY: set user mode SYSEX={msg};") #lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) #sleep (0.05) @@ -634,8 +626,8 @@ def clear (self): # logging.error(f"BRUMBY: Display.clear: end of func idev_out={self.idev_out}") - def refresh (self): - # display memory to display + def update (self): + # move display memory to display with sysex for row in range(4): #msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) text = bytes(self.display_mem[row]) @@ -644,7 +636,7 @@ def refresh (self): # here the magic happens and sysex is cunstructed # 240, 71, 127, 21, <24+line(0-3)>,0, ,,, 247 msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) - # logging.error(f"BRUMBY: Display.refresh SYSEX={msg}") + # logging.error(f"BRUMBY: Display.update SYSEX={msg}") lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) sleep(0.05) return @@ -654,7 +646,7 @@ def refresh (self): def write_xy (self, text, col_in, row_in): # schreibt Text an Position col_in, row_in in display memory und auf Display - # mit refresh + # mit update # Koordinaten prüfen if(row_in > 3): row_in = 3 @@ -668,7 +660,7 @@ def write_xy (self, text, col_in, row_in): text_len = len(text) self.display_mem[row_in][col_in:col_in+text_len] = list(text) - self.refresh() + self.update() return From 0d18828ce1a607a379263b4ecf1dee35308e9999 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Thu, 11 Sep 2025 11:51:50 +0200 Subject: [PATCH 14/57] clean up of committed --- .../ctrldev/zynthian_ctrldev_base_scale.py | 179 ++++++++++++++---- 1 file changed, 138 insertions(+), 41 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index 1fdfa951c..6161ea1ff 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -1,4 +1,8 @@ #!/zynthian/venv/bin/python +import logging + +# do not change. just if this file is started directly from console +console_debug = False # Zynthian specific modules @@ -10,7 +14,7 @@ # Following from: https://github.com/Carlborg/hardpush/blob/master/hardpush.ino # All scales seem to work with 12-halftones. (otherwise they would need the octave-distance at the end)4 -SCALES = { # define scales on the form 'semitones added to tonic' +_SCALES = { # define scales on the form 'semitones added to tonic' 'Chromatic': [0,1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "Major": [0, 2, 4, 5, 7, 9, 11], "Minor": [0, 2, 3, 5, 7, 8, 10], @@ -39,66 +43,159 @@ "Spanish": [0, 1, 3, 4, 5, 6, 8, 10] } - -### How to get the values -## print(list(scales)) # ['Chromatic', 'Major', 'Minor', 'Dorian', 'Mixolydian', 'Lydian', 'Phrygian', 'Locrian', 'Diminished', 'Whole-Half', 'Whole Tone', 'Minor Blues', 'Minor Pentatonic', 'Major Pentatonic', 'Harmonic Minor', 'Melodig Minor', 'Super Locrian', 'Bhairav', 'Hungarian Minor', 'Minor Gipsy', 'Hirojoshi', 'In-Sen', 'Iwato', 'Kumoi', 'Pelog', 'Spanish'] -## print(list(scales)[0]) # "Chromatic" -## print(scales['Chromatic']) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] +### How to get names and values from a named array: +## list(scales) # ['Chromatic', 'Major', 'Minor', 'Dorian', 'Mixolydian', 'Lydian', 'Phrygian', 'Locrian', 'Diminished', 'Whole-Half', 'Whole Tone', 'Minor Blues', 'Minor Pentatonic', 'Major Pentatonic', 'Harmonic Minor', 'Melodig Minor', 'Super Locrian', 'Bhairav', 'Hungarian Minor', 'Minor Gipsy', 'Hirojoshi', 'In-Sen', 'Iwato', 'Kumoi', 'Pelog', 'Spanish'] +## list(scales)[0] # "Chromatic" +## scales['Chromatic'] # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] +### Begin class definition Harmony ############################################## class Harmony: - - scales = SCALES - - def __init__ (self): - pass - - - - def get_midi_note(self, scale,start) -> int: + # in class-vars, Hardware of device and Sacles dont change in insctances + scales = _SCALES + + ### next the instance vars, setup by init()-function, are deverse for each instance + ### cols = 8 + ### row = 8 + ### target_notes = [] + ### target_notes_reverse = {} # to get pads with same midinote to light them uo, when pressed + ### col_versatz = -5 + ### active_scale = "Major" + + def __init__ (self, pad_cols, pad_rows): + self.cols = pad_cols + self.rows = pad_rows + self.target_notes = [] # instance variable + self.target_notes_reverse = {} + self.active_scale = None + self.col_versatz = 0 # 0 means linear, no recess + + def init_scale(self, scale_name, note_start, col_versatz): + """scalename: name of scale in self._scales + note_start: number of the tone in scale with octaves 12 would be second octaves tonica + col_versatz. each row can start with a different reces, so -5 means in C-Major-Scale an "F" above the C in row-1 line + """ + self.col_versatz = col_versatz + self.active_scale = scale_name + self.target_notes = [] # reset for new scale + self.target_notes_reverse = {} # reset for new scale + + for i in range(self.cols * self.rows): + + in_row = i // self.cols + h = i + note_start + (in_row * col_versatz) + if console_debug: print(f"{h} ", end="") + note_new = self._harmony_calculate_midi_note(h) + + self.target_notes.append(note_new) + # reverse mapping + self.target_notes_reverse.setdefault(note_new, []).append(i) + + if console_debug: + print(f"({note_new}), ", end="\t") + if i % self.cols == 0: print() # newline + #print() + #print(self.target_notes_reverse) + + + + def _harmony_calculate_midi_note(self, note) -> int: """params scale: is string with name of scale - start: is integer representing the starting point in the scale. if start is bigger than + note: is integer representing the starting point in the scale. if start is bigger than the length of the specified scale it adds start % leng(scale) * 12 to the result. n So you ca cycle through the number of keyboard keys to get their midi notes """ try: - print(self.scales[scale]) - l = self.scales[scale] - pos = start%len(l) - octave = start // len(l) - print(f"[Debug note]: scale={scale}, start={start} pos={pos}, erg={l[pos]} von {l}") - return l[pos] + (octave * 12 ) - except: - print("Error: get_midi_note: Tonart nicht definiert!") - - - def get_scale_len(self, scale) -> int: + #logging.debug(self.cales, "Scale. ",self.scales[self.active_scale]) + scale = self.scales[self.active_scale] + pos_in_scale = note % len(scale) # + octave = note // len(scale) + #logging.debug(f"[Debug note]: scale={self.scale}, note={note} pos={pos_in_scale}, erg={l[pos_in_scale]} von {l}") + return scale[pos_in_scale] + (octave * 12 ) + except KeyError: + logging.error(f"Error: Scale '{self.active_scale}' not found!") + return -1 # -1 is error, there is no midinote -1 + except Exception as e: + logging.error(f"Error calculating midi note: {e}") + return -1 + + + def harmony_get_scale_len(self, scale) -> int: + """return count tones in scale""" try: - print(f"[Debug len]: scale={scale}, len={len(self.scales[scale])}") + logging.debug(f"[Debug len]: scale={scale}, len={len(self.scales[scale])}") return len(self.scales[scale]) - except: print("Error: get_scale_len: Tonart nicht definiert!") + except: logging.error(f"Error: get_scale_len: scale: {scale} not defined") - def get_scale_names(self): + def harmony_get_scale_names(self): return list(self.scales) + + def harmony_get_target_note(self, pad_nr: int) -> int: + if not 0 <= pad_nr < len(self.target_notes): + logging.error ("Program error, pad_nr out of range for len(target_notes)") + return None + return self.target_notes[pad_nr] + + def harmony_get_padnrs_with_same_note(self, midi_note:int): + return self.target_notes_reverse.get(midi_note, [midi_note]) # if nothing is in map, midinote itself is givan back. None wär +### End of class definition Harmony ############################################## +# class Pad_array: + +# cols = 8 +# rows = 8 + +# pads = [] # array of Pad + +# def __init__(self, pad_cols, pad_rows): +# self.cols = pad_cols +# self.rows = pad_rows + +# pad_count = pad_cols * pad_rows +# print (pad_count) + +# for i in range (pad_count): +# a_pad = Pad() +# self.pads.append(a_pad) + + + +### SART of class definition Pads ################################################ +# class Pad: + +# color_state = None # an integer, that is used as index to an array of colors +# send_midi_event = None # translated event to send +# detect_midi_event = None # detect pad from received midi_event +# name = '' + +# def __init__(self): +# pass + +# def set_color_state(self, color): +# pass + +# def translate_midi(self, ev): +# pass + + + +### END of class definition Pad ################################################# -h = Harmony() - -print(h.get_scale_names()) -print(h.get_midi_note("Kumoi", 4)) -print(h.get_scale_len("Kumoi")) +### for test purposes +if __name__=="__main__": + + console_debug = False -""" -class zynthian_ctrldev_scale(zynthian_ctrldev_base): - + h = Harmony(8, 8) + h.init_scale("Major", 0, -5) +# print(h.harmony_get_scale_names()) +# print(h.harmony_get_midi_note("Kumoi", 4)) +# print(h.harmony_get_scale_len("Kumoi")) - def __init(self): - pass -""" \ No newline at end of file From c15e1a1285c873edd6c02458f85f8cdead66e7c5 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Thu, 11 Sep 2025 12:02:10 +0200 Subject: [PATCH 15/57] another clean up and spelling correction --- zyngine/ctrldev/zynthian_ctrldev_base_scale.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index 6161ea1ff..c19e9fc82 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -4,16 +4,8 @@ # do not change. just if this file is started directly from console console_debug = False - -# Zynthian specific modules -# from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad # , zynthian_ctrldev_zynmixer -# from zyncoder.zyncore import lib_zyncore -# from zynlibs.zynseq import zynseq - -# import zynthian_ctrldev_base - # Following from: https://github.com/Carlborg/hardpush/blob/master/hardpush.ino -# All scales seem to work with 12-halftones. (otherwise they would need the octave-distance at the end)4 +# All scales seem to work as 12-halftone-scales. (otherwise they would need the octave-distance at the end)4 _SCALES = { # define scales on the form 'semitones added to tonic' 'Chromatic': [0,1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "Major": [0, 2, 4, 5, 7, 9, 11], @@ -43,7 +35,7 @@ "Spanish": [0, 1, 3, 4, 5, 6, 8, 10] } -### How to get names and values from a named array: +### How to get names and values from a map: ## list(scales) # ['Chromatic', 'Major', 'Minor', 'Dorian', 'Mixolydian', 'Lydian', 'Phrygian', 'Locrian', 'Diminished', 'Whole-Half', 'Whole Tone', 'Minor Blues', 'Minor Pentatonic', 'Major Pentatonic', 'Harmonic Minor', 'Melodig Minor', 'Super Locrian', 'Bhairav', 'Hungarian Minor', 'Minor Gipsy', 'Hirojoshi', 'In-Sen', 'Iwato', 'Kumoi', 'Pelog', 'Spanish'] ## list(scales)[0] # "Chromatic" ## scales['Chromatic'] # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] @@ -78,7 +70,7 @@ def init_scale(self, scale_name, note_start, col_versatz): """ self.col_versatz = col_versatz self.active_scale = scale_name - self.target_notes = [] # reset for new scale + self.target_notes = [] # reset for new scale self.target_notes_reverse = {} # reset for new scale for i in range(self.cols * self.rows): From 81e3c83fd3bbe2c8564e9a0d281a2ef37f0d978e Mon Sep 17 00:00:00 2001 From: JBrumby Date: Thu, 11 Sep 2025 12:06:10 +0200 Subject: [PATCH 16/57] last commit went wrong --- .../ctrldev/zynthian_ctrldev_base_scale.py | 130 ++++++------------ 1 file changed, 41 insertions(+), 89 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index c19e9fc82..c9deb8507 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -1,13 +1,13 @@ #!/zynthian/venv/bin/python import logging -# do not change. just if this file is started directly from console +# Do not change. Only if this file is started directly from console console_debug = False # Following from: https://github.com/Carlborg/hardpush/blob/master/hardpush.ino -# All scales seem to work as 12-halftone-scales. (otherwise they would need the octave-distance at the end)4 -_SCALES = { # define scales on the form 'semitones added to tonic' - 'Chromatic': [0,1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], +# All scales seem to work as 12-semitone scales. (otherwise they would need the octave-distance at the end) +_SCALES = { # Define scales in the form 'semitones added to tonic' + 'Chromatic': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "Major": [0, 2, 4, 5, 7, 9, 11], "Minor": [0, 2, 3, 5, 7, 8, 10], "Dorian": [0, 2, 3, 5, 7, 9, 10], @@ -22,11 +22,11 @@ "Minor Pentatonic": [0, 3, 5, 7, 10], "Major Pentatonic": [0, 2, 4, 7, 9], "Harmonic Minor": [0, 2, 3, 5, 7, 8, 11], - "Melodig Minor": [0, 2, 3, 5, 7, 9, 11], + "Melodic Minor": [0, 2, 3, 5, 7, 9, 11], # Fixed spelling: "Melodic" instead of "Melodig" "Super Locrian": [0, 1, 3, 4, 6, 8, 10], "Bhairav": [0, 1, 4, 5, 7, 8, 11], "Hungarian Minor": [0, 2, 3, 6, 7, 8, 11], - "Minor Gipsy": [0, 1, 4, 5, 7, 8, 10], + "Minor Gypsy": [0, 1, 4, 5, 7, 8, 10], # Fixed spelling: "Gypsy" instead of "Gipsy" "Hirojoshi": [0, 2, 3, 7, 8], "In-Sen": [0, 1, 5, 7, 10], "Iwato": [0, 1, 5, 6, 10], @@ -35,8 +35,8 @@ "Spanish": [0, 1, 3, 4, 5, 6, 8, 10] } -### How to get names and values from a map: -## list(scales) # ['Chromatic', 'Major', 'Minor', 'Dorian', 'Mixolydian', 'Lydian', 'Phrygian', 'Locrian', 'Diminished', 'Whole-Half', 'Whole Tone', 'Minor Blues', 'Minor Pentatonic', 'Major Pentatonic', 'Harmonic Minor', 'Melodig Minor', 'Super Locrian', 'Bhairav', 'Hungarian Minor', 'Minor Gipsy', 'Hirojoshi', 'In-Sen', 'Iwato', 'Kumoi', 'Pelog', 'Spanish'] +### How to get names and values from a dictionary: +## list(scales) # ['Chromatic', 'Major', 'Minor', 'Dorian', 'Mixolydian', 'Lydian', 'Phrygian', 'Locrian', 'Diminished', 'Whole-Half', 'Whole Tone', 'Minor Blues', 'Minor Pentatonic', 'Major Pentatonic', 'Harmonic Minor', 'Melodic Minor', 'Super Locrian', 'Bhairav', 'Hungarian Minor', 'Minor Gypsy', 'Hirojoshi', 'In-Sen', 'Iwato', 'Kumoi', 'Pelog', 'Spanish'] ## list(scales)[0] # "Chromatic" ## scales['Chromatic'] # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] @@ -44,150 +44,102 @@ ### Begin class definition Harmony ############################################## class Harmony: - # in class-vars, Hardware of device and Sacles dont change in insctances + # Class variables: Hardware of device and Scales don't change in instances scales = _SCALES - ### next the instance vars, setup by init()-function, are deverse for each instance + ### Next the instance variables, set up by init() function, are different for each instance ### cols = 8 - ### row = 8 + ### rows = 8 ### target_notes = [] - ### target_notes_reverse = {} # to get pads with same midinote to light them uo, when pressed + ### target_notes_reverse = {} # To get pads with same MIDI note to light them up when pressed ### col_versatz = -5 ### active_scale = "Major" - def __init__ (self, pad_cols, pad_rows): + def __init__(self, pad_cols, pad_rows): self.cols = pad_cols self.rows = pad_rows - self.target_notes = [] # instance variable + self.target_notes = [] # Instance variable self.target_notes_reverse = {} self.active_scale = None - self.col_versatz = 0 # 0 means linear, no recess + self.col_versatz = 0 # 0 means linear, no offset def init_scale(self, scale_name, note_start, col_versatz): - """scalename: name of scale in self._scales - note_start: number of the tone in scale with octaves 12 would be second octaves tonica - col_versatz. each row can start with a different reces, so -5 means in C-Major-Scale an "F" above the C in row-1 line + """scale_name: name of scale in self._scales + note_start: number of the tone in scale with octaves (12 would be second octave tonic) + col_versatz: each row can start with a different offset, so -5 means in C-Major-Scale an "F" above the C in row-1 line """ self.col_versatz = col_versatz self.active_scale = scale_name - self.target_notes = [] # reset for new scale - self.target_notes_reverse = {} # reset for new scale + self.target_notes = [] # Reset for new scale + self.target_notes_reverse = {} # Reset for new scale for i in range(self.cols * self.rows): - in_row = i // self.cols h = i + note_start + (in_row * col_versatz) if console_debug: print(f"{h} ", end="") note_new = self._harmony_calculate_midi_note(h) self.target_notes.append(note_new) - # reverse mapping + # Reverse mapping self.target_notes_reverse.setdefault(note_new, []).append(i) if console_debug: print(f"({note_new}), ", end="\t") - if i % self.cols == 0: print() # newline - #print() - #print(self.target_notes_reverse) - + if i % self.cols == 0: print() # Newline def _harmony_calculate_midi_note(self, note) -> int: - """params - - scale: is string with name of scale - - note: is integer representing the starting point in the scale. if start is bigger than - the length of the specified scale it adds start % leng(scale) * 12 to the result. n - So you ca cycle through the number of keyboard keys to get their midi notes + """Parameters: + scale: string with name of scale + note: integer representing the starting point in the scale. If start is bigger than + the length of the specified scale it adds start % len(scale) * 12 to the result. + So you can cycle through the number of keyboard keys to get their MIDI notes """ try: - #logging.debug(self.cales, "Scale. ",self.scales[self.active_scale]) scale = self.scales[self.active_scale] pos_in_scale = note % len(scale) # octave = note // len(scale) - #logging.debug(f"[Debug note]: scale={self.scale}, note={note} pos={pos_in_scale}, erg={l[pos_in_scale]} von {l}") - return scale[pos_in_scale] + (octave * 12 ) + return scale[pos_in_scale] + (octave * 12) except KeyError: logging.error(f"Error: Scale '{self.active_scale}' not found!") - return -1 # -1 is error, there is no midinote -1 + return -1 # -1 is error, there is no MIDI note -1 except Exception as e: - logging.error(f"Error calculating midi note: {e}") + logging.error(f"Error calculating MIDI note: {e}") return -1 def harmony_get_scale_len(self, scale) -> int: - """return count tones in scale""" + """Return count of tones in scale""" try: logging.debug(f"[Debug len]: scale={scale}, len={len(self.scales[scale])}") return len(self.scales[scale]) - except: logging.error(f"Error: get_scale_len: scale: {scale} not defined") + except KeyError: + logging.error(f"Error: get_scale_len: scale '{scale}' not defined") + return 0 def harmony_get_scale_names(self): - return list(self.scales) + return list(self.scales) def harmony_get_target_note(self, pad_nr: int) -> int: if not 0 <= pad_nr < len(self.target_notes): - logging.error ("Program error, pad_nr out of range for len(target_notes)") + logging.error("Program error, pad_nr out of range for len(target_notes)") return None return self.target_notes[pad_nr] - def harmony_get_padnrs_with_same_note(self, midi_note:int): - return self.target_notes_reverse.get(midi_note, [midi_note]) # if nothing is in map, midinote itself is givan back. None wär + def harmony_get_padnrs_with_same_note(self, midi_note: int): + return self.target_notes_reverse.get(midi_note, [midi_note]) # If nothing is in map, midi_note itself is given back ### End of class definition Harmony ############################################## -# class Pad_array: - -# cols = 8 -# rows = 8 - -# pads = [] # array of Pad - -# def __init__(self, pad_cols, pad_rows): -# self.cols = pad_cols -# self.rows = pad_rows - -# pad_count = pad_cols * pad_rows -# print (pad_count) - -# for i in range (pad_count): -# a_pad = Pad() -# self.pads.append(a_pad) - - -### SART of class definition Pads ################################################ -# class Pad: - -# color_state = None # an integer, that is used as index to an array of colors -# send_midi_event = None # translated event to send -# detect_midi_event = None # detect pad from received midi_event -# name = '' - -# def __init__(self): -# pass - -# def set_color_state(self, color): -# pass - -# def translate_midi(self, ev): -# pass - - - -### END of class definition Pad ################################################# -### for test purposes -if __name__=="__main__": - +### For test purposes from command line +if __name__ == "__main__": console_debug = False - h = Harmony(8, 8) h.init_scale("Major", 0, -5) # print(h.harmony_get_scale_names()) # print(h.harmony_get_midi_note("Kumoi", 4)) -# print(h.harmony_get_scale_len("Kumoi")) - +# print(h.harmony_get_scale_len("Kumoi")) \ No newline at end of file From b09a98d82a4ae7f39c7ad45e982cd8a7945858d5 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Fri, 12 Sep 2025 18:45:38 +0200 Subject: [PATCH 17/57] Now working as GUI-Controller and as 3-dim-keyboard --- .../zynthian_ctrldev_ableton_push_1.py | 287 +++++++++++------- 1 file changed, 181 insertions(+), 106 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index c40cd4996..841e46b0b 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -72,9 +72,13 @@ # Brumbys imports from time import sleep # pause between sysex events. -import sys # for button detection +#mport sys # for button detection # vor editor use following. # import ableton.push1_consts as ABL +from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony +from zyngine.zynthian_signal_manager import zynsigman +from zyngine.zynthian_engine import zynthian_engine # to send directly to soundengine... + # for running driver this way: import zyngine.ctrldev.ableton.push1_consts as ABL @@ -98,8 +102,8 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ logging.error("Klassenaufruf - Ableton Push 1 - BRUMBY") # Im Weblog wird angezeigt, dass der Treiber geladen wurde - dev_ids = ["Ableton Push IN 2"] - driver_name = "Ableton Push v1" + dev_ids = ["Ableton Push IN 2", "Ableton Push IN 1"] # get by stepping through zynthian_ctrldev_manager.load_driver() + driver_name = "Ableton Push v1" # not essential. class name would be used otherwise driver_description = "Interface Ableton Push v1 with zynpad and zynmixer" # Folgende Farben sind wohl die Sequencer Farben?? @@ -109,30 +113,51 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ STARTING_COLOUR = 123 STOPPING_COLOUR = 120 - # pad_modes - PAD_MODE_SEQ = 0 - PAD_MODE_DRUMS = 1 - PAD_MODE_SCALES = 2 + # dev_modes + DEV_MODE_NONE = None + DEV_MODE_PAD = 1 + DEV_MODE_DRUMS = 2 + DEV_MODE_SCALES = 3 + + EV_NOTE_ON = 0X9 + EV_NOTE_OFF = 0x8 + EV_CC = 0XB # pad_mode_active = PAD_MODE_SEQ - pad_mode_active = PAD_MODE_SCALES + device_mode_active = DEV_MODE_SCALES + + scales = Harmony(8,8) + scales.init_scale("Major", 36, -5) # -3 = new start per row + # Function to initialise class def __init__(self, state_manager, idev_in, idev_out=None): logging.info("Created Instance from Ableton Push 1 driver - BRUMBY") - self.shift = False + + # super.__init__ saves state_manger, chainmanger, idev_in and idev_out + # nothing more. super().__init__(state_manager, idev_in, idev_out) - # self.pad_mode_active = self.PAD_MODE_SCALES - + + self.shift = False + self.device_mode_active = self.DEV_MODE_SCALES + self.set_dev_scale_color() + + # self.zynmixer = state_manager.zynmixer # Mixer object + # set push to live mode + # seems not to do anything + #sysex = bytes(ABL.SYSEX_DATA_SET_LIVE_MODE) + #lib_zyncore.dev_send_midi_event(self.idev_out, sysex, len(sysex)) + #sleep(0.01) + # Initialize display self.display = Display(idev_out) self.display.clear() sleep(0.1) # necessary delays, otherwise the next command is ignored - self.display.brightnes(63) + self.display.brightnes(36) sleep(0.1) self.display.write_xy(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) @@ -146,37 +171,45 @@ def __init__(self, state_manager, idev_in, idev_out=None): sleep(0.1) self.display.write_xy(b'++ Make MusicNot War ++', 20,3) + self.unroute_from_chains = True + return def init(self): - logging.info("called init. Setting up Ableton Push 1 - BRUMBY") - - # Hier muss die Trackstaste zum Leuchten gebracht werden am Push 1 - # Enable session mode on launchkey - # Track-Taste CC112 # ABL_TRACK - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 112, ABL.MONO_LED_LIT) # 2! - # CC62 = OK; CC63 = Back (ABL_OK, ABL_ESC - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 62, ABL.MONO_LED_LIT_BLINK) # 2! - - # Monochrome Tasten die hell leuchten sollen - for t in [ 36,37,38,39,40,41,42,43, - ABL.BTN_START[1], ABL.BTN_OK[1], ABL.BTN_ESC[1], ABL.BTN_LEFT[1], - ABL.BTN_RIGHT[1], ABL.BTN_UP[1], ABL.BTN_DOWN[1], ABL.BTN_SCALES[1] ]: + try: + logging.info("called init. Setting up Ableton Push 1 - BRUMBY") + + # Hier muss die Trackstaste zum Leuchten gebracht werden am Push 1 + # Enable session mode on launchkey + # Track-Taste CC112 # ABL_TRACK + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 112, ABL.MONO_LED_LIT) # 2! + # CC62 = OK; CC63 = Back (ABL_OK, ABL_ESC + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 62, ABL.MONO_LED_LIT_BLINK) # 2! - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_LIT) - - # monochrome Buttons than should be dim state - for t in [ ABL.BTN_REC[1], ABL.BTN_SHIFT[1] ]: # ,ABL_REC, ABL_SHIFT]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) - - # Bicolor LEDs dim ## CC20-27 + 102-109 - for t in [21, 23]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_ORANGE_DIM) + # Monochrome Tasten die hell leuchten sollen + for t in [ 36,37,38,39,40,41,42,43, + ABL.BTN_START[1], ABL.BTN_OK[1], ABL.BTN_ESC[1], ABL.BTN_LEFT[1], + ABL.BTN_RIGHT[1], ABL.BTN_UP[1], ABL.BTN_DOWN[1], ABL.BTN_SCALES[1] ]: + + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_LIT) - ### lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) - self.cols = 8 - self.rows = 8 # war 2 20250829-2134 - super().init() # aktiviert. Muss aktiviert sein! - self.pads_off() + # monochrome Buttons than should be dim state + for t in [ ABL.BTN_REC[1], ABL.BTN_SHIFT[1] ]: # ,ABL_REC, ABL_SHIFT]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) + + # Bicolor LEDs dim ## CC20-27 + 102-109 + for t in [21, 23]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_ORANGE_DIM) + + ### lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) + self.cols = 8 + self.rows = 8 # war 2 20250829-2134 + super().init() # aktiviert. Muss aktiviert sein! + # self.pads_off() + if self.device_mode_active == self.DEV_MODE_SCALES: + self.set_dev_scale_color() + + except ValueError as e: + print(f"Fehler aufgetreten: {e}") def end(self): @@ -212,49 +245,53 @@ def update_mixer_strip(self, chan, symbol, value): # this function is called by zynseq when a sequencer state is changed # we will update pad LED to show state def update_seq_state(self, bank, seq, state, mode, group): - - # Onlyreturn if Push1 driver is not in sequencer_mode_view - if not self.pad_mode_active == self.PAD_MODE_SEQ: return - - # logging.info(f"BRUMBY bank={bank}; seq={seq}; state={state}; mode={mode}; group:{group}") - if self.idev_out is None or bank != self.zynseq.bank: - return - - col, row = self.zynseq.get_xy_from_pad(seq) - note = ABL_PAD_END +1 -(row+1) * 8 + col - # logging.info(f"BRUMBY-P col={col}; row={row} ergibt note:{note}") - - # Alles abfangen, was ausserhalb des Pad-Bereichs ist BRUMBY_NEU. - #if (note > ABL_PAD_END) or (note < ABL_PAD_START): - # return - try: - if mode == 0 or group > 16: - chan = 0 - vel = 0 - elif state == zynseq.SEQ_STOPPED: - chan = 0 - vel = self.PAD_COLOURS[group] - elif state == zynseq.SEQ_PLAYING: - chan = 2 - vel = self.PAD_COLOURS[group] - elif state in [zynseq.SEQ_STOPPING, zynseq.SEQ_STOPPINGSYNC]: - chan = 1 - vel = self.STOPPING_COLOUR - elif state == zynseq.SEQ_STARTING: - chan = 1 - vel = self.STARTING_COLOUR - - else: # Wenn nichts passt Pad-Beleuchtung ausschalten + # return + # Onlyreturn if Push1 driver is not in sequencer_mode_view + if not self.device_mode_active == self.DEV_MODE_PAD: + return + + # logging.info(f"BRUMBY bank={bank}; seq={seq}; state={state}; mode={mode}; group:{group}") + if self.idev_out is None or bank != self.zynseq.bank: + return + + col, row = self.zynseq.get_xy_from_pad(seq) + note = ABL_PAD_END +1 -(row+1) * 8 + col + # logging.info(f"BRUMBY-P col={col}; row={row} ergibt note:{note}") + + # Alles abfangen, was ausserhalb des Pad-Bereichs ist BRUMBY_NEU. + #if (note > ABL_PAD_END) or (note < ABL_PAD_START): + # return + + try: + if mode == 0 or group > 16: + chan = 0 + vel = 0 + elif state == zynseq.SEQ_STOPPED: + chan = 0 + vel = self.PAD_COLOURS[group] + elif state == zynseq.SEQ_PLAYING: + chan = 2 + vel = self.PAD_COLOURS[group] + elif state in [zynseq.SEQ_STOPPING, zynseq.SEQ_STOPPINGSYNC]: + chan = 1 + vel = self.STOPPING_COLOUR + elif state == zynseq.SEQ_STARTING: + chan = 1 + vel = self.STARTING_COLOUR + + else: # Wenn nichts passt Pad-Beleuchtung ausschalten + chan = 0 + vel = 0 + + except Exception as e: # Bei Fehler Pad-beleuchtung ausschalten chan = 0 vel = 0 - - except Exception as e: # Bei Fehler Pad-beleuchtung ausschalten - chan = 0 - vel = 0 - # set pad color with velocity value - lib_zyncore.dev_send_note_on(self.idev_out, chan, note, vel) + # set pad color with velocity value + lib_zyncore.dev_send_note_on(self.idev_out, chan, note, vel) + except ValueError as e: + print(f"Fehler aufgetreten: {e}") @@ -275,41 +312,69 @@ def pads_off(self): for col in range(self.cols): self.pad_off(col, row) - def midi_event(self, ev): - logging.debug(f"midi_event Ableton Push 1 - BRUMBY {ev}") - btn_name = self.button_name_from_midi_event(ev) + + # https://discourse.zynthian.org/t/driver-for-ableton-push-1-first-steps/12166/8 + def _forward_like_niels_did(self, ev): + # Direct keybed to chains + #if (channel == 1): + chain = self.chain_manager.get_active_chain() + # print(chain.midi_chan) + # @todo: find out how to get 'last' active chain, for now: just back out. + + if chain.midi_chan is None: + return False + + status = (ev[0] & 0xF0) | chain.midi_chan + self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) + return True + # if not processed you call + # return super()._on_midi_event(ev)` + + def midi_event(self, ev): + try: # if midievent is too short it fails... + btn_name = self.button_name_from_midi_event(ev) + logging.debug(f"Button: {btn_name} gives midi_event: {ev} = {ev[0]}, {ev[1]} {ev[2]}") + except: + pass + #return False # prüfen ob sofortige Umleitung eine Note spield???!!!! + evtype = (ev[0] >> 4) & 0x0F - # evtype= EV_NOTE_ON - if evtype == 0x9: + - note = ev[1] & 0x7F # das ist überflüssig, weil note immer < 127 ist + note = ev[1] & 0x7F # das ist überflüssig, weil note immer < 127 ist - # filter every note on not from pads. knobs have, when touched also note_on messages - if note < ABL_PAD_START: - return True - if note > ABL_PAD_END: - return True # ignore every note_on not from pads + # filter every note on not from pads. knobs have, when touched also note_on messages + if note < ABL_PAD_START: + return True + if note > ABL_PAD_END: + return True # ignore every note_on not from pads - logging.debug(f"BRUMBY: note on event with note={note}") + logging.debug(f"BRUMBY: note on event with note={note}") - # Toggle pad - # Hier wird der midi-Notenwert in einen x,y Wert umgewandelt, um die Sequencer-Bank entsprechend zu toggeln. - - if self.pad_mode_active == self.PAD_MODE_SCALES: + # Toggle pad + # Hier wird der midi-Notenwert in einen x,y Wert umgewandelt, um die Sequencer-Bank entsprechend zu toggeln. + + + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF] and self.device_mode_active == self.DEV_MODE_SCALES: # hier muss er Translator für scales hin! - logging.error(f"midi_event Ableton Push 1 - BRUMBY: PAD in SCALES mode - not implemented yet") + logging.debug(f"Scales mode -BRUMBY") + pad_nr = note -35# get pad_nr from note + note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes arf based 0 pad_nr based 1 + new_ev = bytes([ev[0], note_translated, ev[2]*2]) + #if note_translated % 12 == 0: # Oktave detected + # pass # for note_on events following. - #zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_ON, - # izmip=izmip, chan=chan, note=ev[1] & 0x7f, vel=ev[2] & 0x7f) - self.set_pad_rgb(note-36, 205,25,255) - return False # let caller process event + + self._forward_like_niels_did(new_ev) + return True # not processed, has to be processed further - + # evtype= EV_NOTE_ON + if evtype == 0x9: - if self.pad_mode_active == self.PAD_MODE_SEQ: + if self.device_mode_active == self.DEV_MODE_PAD: try: col = (note - ABL_PAD_START) // 8 # statt 96 -> 91 row = (note - ABL_PAD_START) % 8 # Statt 96 -> 91 @@ -364,13 +429,14 @@ def midi_event(self, ev): # This swtches between this drivers pad states: Sequencer and Scales elif (ccnum == ABL.BTN_SCALES[1]): logging.info("BRUMBY: BTN_SCALES") - if self.pad_mode_active != self.PAD_MODE_SCALES: - self.pad_mode_active = self.PAD_MODE_SCALES + if not self.device_mode_active == self.DEV_MODE_SCALES: + self.device_mode_active = self.DEV_MODE_SCALES # visual feedback, let Scales Button blink lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) self.pads_off() # akk pad leds off + self.set_dev_scale_color() else: - self.pad_mode_active = self.PAD_MODE_SEQ + self.device_mode_active = self.DEV_MODE_PAD # visual feedback, set LED to solid on lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) self.pads_off() # clean up visible state. all pad leds off @@ -503,13 +569,22 @@ def button_name_from_midi_event(self, ev): ###, button_event): # button_event is if not name.startswith('__'): # no attributes with '__' attr = getattr(ABL, name) if attr == search_key and name.isupper(): - logging.debug(f"midi_event {ev} from Button with name: {name} and value: {data}") + logging.debug(f"midi_event {ev} {ev[0]}, {ev[1]}, from Button with name: {name} and value: {data}") return name logging.debug(f"midi_event {ev} from Button not defined with value: {data}") return None + def set_dev_scale_color(self): + for pad_nr in range(64): + new_note = self.scales.harmony_get_target_note(pad_nr) + if new_note % 12 == 0: + r = 0; g = 0; b = 255 + else: + r = 200; g = 200; b = 200 + self.set_pad_rgb(pad_nr, r, g, b) + pass - def set_pad_rgb(self, pad_nr, r,g,b): + def set_pad_rgb(self, pad_nr: int, r:int ,g:int ,b:int): # Sysex : 240,71,127,21,4,0,8,,0,,,,,,,247 # pad = 0-71 NICHT PAD_36 - PAD_99 # blogspot.com From a0b4d2f8a7f0ee66239ff3aff48455337fb82776 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 13 Sep 2025 11:33:00 +0200 Subject: [PATCH 18/57] Clean up. possibly detected a inconsistency in driver processing return value --- .../zynthian_ctrldev_keystation_pro_88_mk1.py | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py index 883a6afb9..e9941e542 100644 --- a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py @@ -17,8 +17,13 @@ # everything that is not essential is commented out # +# Strange, when this driver throws an exception, the midi event is processed, as if the midi_event() sends back "False" +# I realized this, when I had a mistake in my send_midi function (library zynseq was not importet) +# need further exploration + import logging from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_base +from zynlibs.zynseq import zynseq # to send midi directly from this driver logger = logging.getLogger('zynthian') @@ -31,7 +36,8 @@ class zynthian_ctrldev_keystation_pro_88_mk1(zynthian_ctrldev_base): # They are different in Zynthian to that what I found in linux console # # with debugging it is easy to find the correct dev_ids. Found no other way. You could try # device name with " IN 1" and "IN 2" at the end - dev_ids = ["Keystation Pro 88 IN 1", "Keystation Pro 88 IN 2"] # these values are ESSENTIAL for the driver to connect it to the device + # dev_ids = ["Keystation Pro 88 IN 1", "Keystation Pro 88 IN 2"] # these values are ESSENTIAL for the driver to connect it to the device + dev_ids = ["Keystation Pro 88 IN 1"] # these values are ESSENTIAL for the driver to connect it to the device. Data is just at Port 1 # driver_name = "Keystation Pro 88 Minimal" # not essential, just for information in logs @@ -46,13 +52,33 @@ class zynthian_ctrldev_keystation_pro_88_mk1(zynthian_ctrldev_base): zynpot_2 = 0 zynpot_3 = 0 + # evtype = (ev[0] >> 4) & 0x0F -> + EV_NOTE_OFF = 0x8 # 3 Bytes + EV_NOTE_ON = 0X9 # 3 Bytes + EV_AFTERTOUSCH = 0xA # 3 Bytes (polyphonic = per note) + EV_CC = 0xB # 3 Bytes + EV_PC = 0xC # 2 Bytes + EV_CHAN_PRESS = 0xD # 2 Bytes + EV_PITCHBEND = 0xE # 3 bytes ev[1] = LSB 0-127; ev[2] = MSB 0-127 + EV_SYSTEM = 0xF # Systemtype = ev[0] & 0x0F + + def midi_event(self, ev): """Easy MIDI event handler for Keystation Pro 88""" - # logger.debug(f"MIDI Event empfangen: {ev}") + # self.unroute_from_chains = False # otherwise no Keyboard anymore, just a controller + # self.enabled = True + evtype = (ev[0] >> 4) & 0x0F + + if len(ev) == 3: logger.debug(f"MIDI Event empfangen: {ev} {ev[0]} {ev[1]} {ev[2]}") + if len(ev) > 0: status= ev[0] & 0xF0 # Which midi message type (note on, note off, control change, etc. ) # channel = ev[0] & 0x0F # not usesd, + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUSCH, self.EV_PITCHBEND]: + return self.send_midi(ev) + + if len(ev) == 3: # most times 3 bytes and we need 3 bytes # status = ev[0] & 0xF0 # Which midi message type (note on, note off, control change, etc. ) # channel = ev[0] & 0x0F # not usesd, just for information @@ -80,7 +106,7 @@ def midi_event(self, ev): # data2 is the value of the knob (0-127) - if status == 0xb0 and data1 == 42: # 42 is knob 18 in "keystations Preset 10. (Press Recall and choose 10)" + if status == 0xb0 and data1 == 104: # 42 is knob 18 in "keystations Preset 10. (Press Recall and choose 10)" # if controller sends relative values, that is: negative values for left turn and positive values for right turn, # you can use: # pot = data2 @@ -92,19 +118,19 @@ def midi_event(self, ev): self.state_manager.send_cuia("ZYNPOT", [0, pot]) return True # Event processed. restarts event loop - if status == 0xb0 and data1 == 34: # 34 is knob 19 in "Preset-Recall 10" + if status == 0xb0 and data1 == 105: # 34 is knob 19 in "Preset-Recall 10" pot = data2-self.zynpot_1 self.zynpot_1 = data2 self.state_manager.send_cuia("ZYNPOT", [1, pot]) return True # Event processed. restarts event loop - if ev[0] == 0xb0 and data1 == 10: # 10 is knob 10 in "Preset-Recall 10" + if ev[0] == 0xb0 and data1 == 85: # 10 is knob 10 in "Preset-Recall 10" pot = data2-self.zynpot_2 self.zynpot_2 = data2 self.state_manager.send_cuia("ZYNPOT", [2, pot]) return True # Event processed. restarts event loop - if ev[0] == 0xb0 and data1 == 2: # 2 is knob 11 in "Preset-Recall 10" + if ev[0] == 0xb0 and data1 == 86: # 2 is knob 11 in "Preset-Recall 10" pot = data2-self.zynpot_3 self.zynpot_3 = data2 self.state_manager.send_cuia("ZYNPOT", [3, pot]) @@ -131,7 +157,20 @@ def midi_event(self, ev): self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) # self.state_manager.send_cuia("SELECT") return True # Event processed. restarts event loop """ + return False # event not processed by this driver. Zynthian queue has to process it further down the row + + def send_midi (self, ev): + chain = self.chain_manager.get_active_chain() + # print(chain.midi_chan) + # @todo: find out how to get 'last' active chain, for now: just back out. + + if chain.midi_chan is None: + return False + + status = (ev[0] & 0xF0) | chain.midi_chan + self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) + return True From d1d8a271b1d38de180c9c5f43859d91b75dc636d Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 13 Sep 2025 13:12:17 +0200 Subject: [PATCH 19/57] fixed errors in midi event processing --- .../zynthian_ctrldev_ableton_push_1.py | 516 ++++++++++-------- 1 file changed, 277 insertions(+), 239 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 841e46b0b..4e2b6ad4b 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -1,46 +1,6 @@ #! /zynthian/venv/bin/python - - # -*- coding: utf-8 -*- -# 20250910-0044 -# Alle Konstanten ausgelagert. - -#### -#### -#### - - - -# note werte Push 1 Midimapping -# don't delete. -ABL_PAD_START = 36 # 1. Pad = pad_36 -ABL_PAD_END = 99 # letztes Paad = pad_99 - -# # CC Werte der Tasten -# ABL_REC = 86 -# # ABL_PLAY = 85 - -# ABL_OK = 62 -# ABL_ESC = 63 # Abbruch, Zurück - -# ABL_TRACK = 112 - -# ABL_ARROW_LEFT = 44 -# ABL_ARROW_RIGHT = 45 -# ABL_ARROW_UP = 46 -# ABL_ARROR_DOWN = 47 -# ABL_SHIFT = 49 - -# # DISPLAY Buttons Row 1 -# ABL_BUTTON_DISPL_R1_0 = 20 -# # ... -# ABL_BUTTON_DISPL_R1_8 = 27 -# # Display Buttons Row 2 -# ABL_BUTTON_DISPL_R2_0 = 102 -# # ... -# ABL_BUTTON_DISPL_R2_8 = 109 - # ****************************************************************************** @@ -69,6 +29,7 @@ import logging +import traceback # Brumbys imports from time import sleep # pause between sysex events. @@ -78,6 +39,7 @@ from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony from zyngine.zynthian_signal_manager import zynsigman from zyngine.zynthian_engine import zynthian_engine # to send directly to soundengine... +from zyngine.ctrldev.zynthian_ctrldev_base_extended import RunTimer, KnobSpeedControl, ButtonTimer, CONST # for running driver this way: @@ -96,13 +58,22 @@ # zynthian_ctrldev_zynpad is class for Controlling the sequencer with pads #zynthian_ctrldev_zynmixer cpntrolls the main mixer + +# note werte Push 1 Midimapping +# don't delete. +ABL_PAD_START = 36 # 1. Pad = pad_36 +ABL_PAD_END = 99 # letztes Paad = pad_99 + + class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer): - logging.error("Klassenaufruf - Ableton Push 1 - BRUMBY") + logging.info("Klassenaufruf - Ableton Push 1") # Im Weblog wird angezeigt, dass der Treiber geladen wurde - dev_ids = ["Ableton Push IN 2", "Ableton Push IN 1"] # get by stepping through zynthian_ctrldev_manager.load_driver() + # dev_ids = ["Ableton Push IN 2", "Ableton Push IN 1"] # get by stepping through zynthian_ctrldev_manager.load_driver() + dev_ids = ["Ableton Push IN 2"] # get by stepping through zynthian_ctrldev_manager.load_driver(). Data just at Port 2 + driver_name = "Ableton Push v1" # not essential. class name would be used otherwise driver_description = "Interface Ableton Push v1 with zynpad and zynmixer" @@ -116,18 +87,25 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ # dev_modes DEV_MODE_NONE = None DEV_MODE_PAD = 1 - DEV_MODE_DRUMS = 2 - DEV_MODE_SCALES = 3 + # DEV_MODE_DRUMS = 2 + DEV_MODE_SCALES = 3 # keyboard modes + + # evtype = (ev[0] >> 4) & 0x0F -> + EV_NOTE_OFF = 0x8 # 3 Bytes + EV_NOTE_ON = 0X9 # 3 Bytes + EV_AFTERTOUSCH = 0xA # 3 Bytes (polyphonic = per note) + EV_CC = 0xB # 3 Bytes + EV_PC = 0xC # 2 Bytes + EV_CHAN_PRESS = 0xD # 2 Bytes + EV_PITCHBEND = 0xE # 3 bytes ev[1] = LSB 0-127; ev[2] = MSB 0-127 + EV_SYSTEM = 0xF # Systemtype = ev[0] & 0x0F - EV_NOTE_ON = 0X9 - EV_NOTE_OFF = 0x8 - EV_CC = 0XB # pad_mode_active = PAD_MODE_SEQ device_mode_active = DEV_MODE_SCALES scales = Harmony(8,8) - scales.init_scale("Major", 36, -5) # -3 = new start per row + scales.init_scale("Minor", 36-1, -5) # -3 = new start per row # Function to initialise class @@ -136,48 +114,28 @@ def __init__(self, state_manager, idev_in, idev_out=None): # super.__init__ saves state_manger, chainmanger, idev_in and idev_out # nothing more. - super().__init__(state_manager, idev_in, idev_out) - - - self.shift = False - self.device_mode_active = self.DEV_MODE_SCALES - self.set_dev_scale_color() + # Indecators of the device LEDs and Text + self._leds_mono = Feedback_Mono_LEDs(idev_out) # control buttons right and left from pads + self._leds_bi = Feedback_Bi_LEDs(idev_out) # display buttons below display, above pads + self._leds_rgb = Feedback_RGB_LEDs(idev_out) # pads in rgb + self._display = Feedback_Display(idev_out) # Text display - # self.zynmixer = state_manager.zynmixer # Mixer object + super().__init__(state_manager, idev_in, idev_out) - # set push to live mode - # seems not to do anything - #sysex = bytes(ABL.SYSEX_DATA_SET_LIVE_MODE) - #lib_zyncore.dev_send_midi_event(self.idev_out, sysex, len(sysex)) - #sleep(0.01) - - # Initialize display - self.display = Display(idev_out) - self.display.clear() - sleep(0.1) # necessary delays, otherwise the next command is ignored - - self.display.brightnes(36) - sleep(0.1) - - self.display.write_xy(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) - sleep(0.1) - -# Positionierungshilfe - self.display.write_xy(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) - sleep(0.1) - - self.display.write_xy(b'** Zynthian Push1Driver 0.1 **', 17,2) - sleep(0.1) - - self.display.write_xy(b'++ Make MusicNot War ++', 20,3) + # seems to be necessary, because we send translates midi_events. self.unroute_from_chains = True return def init(self): try: logging.info("called init. Setting up Ableton Push 1 - BRUMBY") + self.shift = False + self.device_mode_active = self.DEV_MODE_SCALES + self.set_dev_scale_color() + self._display.first_screen() + # Hier muss die Trackstaste zum Leuchten gebracht werden am Push 1 # Enable session mode on launchkey # Track-Taste CC112 # ABL_TRACK @@ -208,8 +166,14 @@ def init(self): if self.device_mode_active == self.DEV_MODE_SCALES: self.set_dev_scale_color() - except ValueError as e: - print(f"Fehler aufgetreten: {e}") + #except: + # print("Fehler aufgetreten: {e}") + except Exception as e: + print("Exception aufgetreten:") + # Gibt den vollständigen Traceback aus + traceback.print_exc() + # logging.error("Exception aufgetreten: %s", e) + # logging.error("Traceback: %s", traceback.format_exc()) def end(self): @@ -332,69 +296,85 @@ def _forward_like_niels_did(self, ev): # return super()._on_midi_event(ev)` def midi_event(self, ev): - try: # if midievent is too short it fails... - btn_name = self.button_name_from_midi_event(ev) - logging.debug(f"Button: {btn_name} gives midi_event: {ev} = {ev[0]}, {ev[1]} {ev[2]}") - except: - pass - - #return False # prüfen ob sofortige Umleitung eine Note spield???!!!! - - evtype = (ev[0] >> 4) & 0x0F - + ### For debugging purposes block can be commented out ! + evtype = None + chan_or_instruction = None + note_or_register = None + val_or_vel = None + + if len(ev) > 0: + evtype = (ev[0] >> 4) & 0x0F + chan_or_instruction = ev[0] & 0xF + if len(ev) > 1: + note_or_register = ev[1] & 0x7F + if len(ev) > 2: + val_or_vel = ev[2] & 0x7F + + if note_or_register: # len > 1 -> Button / Pad detection is possible + btn_name = self.button_name_from_midi_event(ev) # ev[0] and ev[1] fields are proved. so any status can be a button + + logging.debug(f"Button: {btn_name} on chan. {chan_or_instruction} gives midi_event: {hex(ev[0])} {hex(ev[1])} {hex(val_or_vel)} = {evtype}, {note_or_register} {val_or_vel}") - note = ev[1] & 0x7F # das ist überflüssig, weil note immer < 127 ist - - # filter every note on not from pads. knobs have, when touched also note_on messages - if note < ABL_PAD_START: - return True - if note > ABL_PAD_END: - return True # ignore every note_on not from pads - - logging.debug(f"BRUMBY: note on event with note={note}") - - # Toggle pad - # Hier wird der midi-Notenwert in einen x,y Wert umgewandelt, um die Sequencer-Bank entsprechend zu toggeln. + ### End of debugging purposes. + # don't process 1-byte events. + if len(ev)<2: + return False - if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF] and self.device_mode_active == self.DEV_MODE_SCALES: - # hier muss er Translator für scales hin! - logging.debug(f"Scales mode -BRUMBY") - pad_nr = note -35# get pad_nr from note - note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes arf based 0 pad_nr based 1 - new_ev = bytes([ev[0], note_translated, ev[2]*2]) - #if note_translated % 12 == 0: # Oktave detected - # pass - # for note_on events following. - - self._forward_like_niels_did(new_ev) - return True # not processed, has to be processed further + # processing starts here + evtype = (ev[0] >> 4) & 0x0F + note = ev[1] & 0x7F # is that need? any event field is from 0-127 except the status field + + ### keyboard mode + if self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected + # Filter out note events created by push 1 when touching Knobs and Ribbon + if ABL_PAD_START <= note <= ABL_PAD_END: # just note events, which should sound. + # filter for getting any vent that is sound event + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUSCH, self.EV_PITCHBEND]: + + # logging.debug(f"Scales mode -BRUMBY") + pad_nr = note -35# translat note from event to pad_nr + + # here magic for different sccale layouts happens. + # it translates midi_note events to the translated note_events + note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes are based 0 pad_nr based 1 + new_ev = bytes([ev[0], note_translated, ev[2]*2]) + #if note_translated % 12 == 0: # Oktave detected + # pass + # for note_on events following. + + self._forward_like_niels_did(new_ev) # + return True # return to caller and mark event as processed - # evtype= EV_NOTE_ON - if evtype == 0x9: - - if self.device_mode_active == self.DEV_MODE_PAD: + # pad mode to control sequencer + elif self.device_mode_active == self.DEV_MODE_PAD: + if evtype == 0x9: # fitler just for note_on events try: - col = (note - ABL_PAD_START) // 8 # statt 96 -> 91 - row = (note - ABL_PAD_START) % 8 # Statt 96 -> 91 - # row = 7 - row; + col = (note - ABL_PAD_START) // 8 # + row = (note - ABL_PAD_START) % 8 # col = 7 - col; # midi notes start from bottom, so recalculate row pad = row * self.zynseq.col_in_bank + col - - logging.error(f"midi_event 1 MEINER Ableton Push 1 - BRUMBY: row={row}; col={col}; pad={pad}") - + # logging.error(f"BRUMBY: row={row}; col={col}; pad={pad}") + if pad < self.zynseq.seq_in_bank: self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) - return True + return True # mark processed except: pass - return False + # I think we dont need any note events... so filter out + # NO note events anymore after this two lines. Comment out what you need after this lines in "Pad Mode" = DEV_MODE_PAD + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUSCH, self.EV_PITCHBEND]: + return True + + # no return call. I don't know if I need note_on_events further down. + + # if prceessd before there are just note_events lower PAD_START and higher PAD_END if processed before # GUI Control Changes # evtype = EV_CC - elif evtype == 0xB: + if evtype == 0xB: ccnum = ev[1] & 0x7F ccval = ev[2] & 0x7F @@ -410,8 +390,8 @@ def midi_event(self, ev): lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_DIM) return True # event processed. No further action required - # From here filter any event with velocyty=0 We just need pressed vaues to come through - elif ccnum == 0 or ccval == 0: + # From here filter any event with velocyty=0 We just need notepressed values to come through + elif ccnum == 0 or ccval == 0: # is that midi bank change? return False # Warning: With "return True" no further processing in zynthian. # So no Controlchange with data=0 gets through to zynthian. # Is that, what we want ??? @@ -426,9 +406,9 @@ def midi_event(self, ev): if chain and chain.mixer_chan is not None and chain.mixer_chan < 17: self.zynmixer.set_level(chain.mixer_chan, ccval / 127.0) # "/127.0" creates a float val from 0.0 .. 1.0 - # This swtches between this drivers pad states: Sequencer and Scales + # This swtches between this drivers pad states: Pad (Sequencer) and Scales elif (ccnum == ABL.BTN_SCALES[1]): - logging.info("BRUMBY: BTN_SCALES") + logging.info("BRUMBY: BTN_SCALES processing") if not self.device_mode_active == self.DEV_MODE_SCALES: self.device_mode_active = self.DEV_MODE_SCALES # visual feedback, let Scales Button blink @@ -444,7 +424,7 @@ def midi_event(self, ev): return True - # Gui events ausgelagert zu: + # Gui events moved to: if self.process_gui_events(ev): return True @@ -479,26 +459,22 @@ def process_gui_events(self,ev) -> bool: logging.debug(f"BRUMBY: Poti={ccnum-71} val={val}") return True - elif (ccnum == ABL.BTN_OK[1]) or (ccnum == 23): logging.debug("ABL_OK BRUMBY") self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) return True - # elif ccnum == 21: Does that work? elif ccnum == ABL.BTN_R1_C2[1]: # Zweiter Button unter dem Display logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) return True - elif ccnum == ABL.BTN_ESC[1]: # logging.debug("BTN_ESC BRUMBY") self.state_manager.send_cuia("BACK") return True - # elif ccnum == 45: elif ccnum == ABL.BTN_RIGHT[1]: # elif ccnum == 0x66: @@ -569,45 +545,49 @@ def button_name_from_midi_event(self, ev): ###, button_event): # button_event is if not name.startswith('__'): # no attributes with '__' attr = getattr(ABL, name) if attr == search_key and name.isupper(): - logging.debug(f"midi_event {ev} {ev[0]}, {ev[1]}, from Button with name: {name} and value: {data}") + # logging.debug(f"midi_event {ev} {ev[0]}, {ev[1]}, from Button with name: {name} and value: {data}") return name - logging.debug(f"midi_event {ev} from Button not defined with value: {data}") - return None + # logging.debug(f"midi_event {ev} from Button not defined with value: {data}") + return "" + def set_dev_scale_color(self): + self._leds_rgb.all_off(True) # led_states must not be deleted. is done in next lines for pad_nr in range(64): new_note = self.scales.harmony_get_target_note(pad_nr) if new_note % 12 == 0: r = 0; g = 0; b = 255 else: r = 200; g = 200; b = 200 - self.set_pad_rgb(pad_nr, r, g, b) + # self.set_pad_rgb(pad_nr, r, g, b) ## OLD FUnction + self._leds_rgb.set_rgb(pad_nr, r, g, b, overlay=False) pass def set_pad_rgb(self, pad_nr: int, r:int ,g:int ,b:int): - # Sysex : 240,71,127,21,4,0,8,,0,,,,,,,247 - # pad = 0-71 NICHT PAD_36 - PAD_99 - # blogspot.com - # To set a pad color to a RGB(0-255) value, the RGB values need to be set into "Push" format, for example: - # r1 = r /(integer division) 16 - # r2 = r %(modulo) 16 - # So a value of R132 would become: r1=8 r2=4. - # The pad index if from 0 to 71, zero being the bottom left pad, all the way up to the second row button to the right (second row of buttons is RGB). - if r > 255: r = 255; - if r < 0: r = 0 - if g > 255: g = 255; - if g < 0: g = 0 - if b > 255: b = 255; - if b < 0: b = 0 - if not 0 <= pad_nr <= 64: - logging.error(f"Padnr wrong. not in 1..64 pad_nr = {pad_nr}") - return False - - r1= r // 16 ; r2= r % 16 - g1= g // 16 ; g2= g % 16 - b1= b // 16 ; b2= b % 16 - sysex = bytes ([240,71,127,21,4,0,8,pad_nr,0,r1,r2,g1,g2,b1,b2,247] ) - lib_zyncore.dev_send_midi_event(self.idev_out, sysex, len(sysex)) + logging.error(" set_pad_rb aufgerufen.") + # # Sysex : 240,71,127,21,4,0,8,,0,,,,,,,247 + # # pad = 0-71 NICHT PAD_36 - PAD_99 + # # blogspot.com + # # To set a pad color to a RGB(0-255) value, the RGB values need to be set into "Push" format, for example: + # # r1 = r /(integer division) 16 + # # r2 = r %(modulo) 16 + # # So a value of R132 would become: r1=8 r2=4. + # # The pad index if from 0 to 71, zero being the bottom left pad, all the way up to the second row button to the right (second row of buttons is RGB). + # if r > 255: r = 255; + # if r < 0: r = 0 + # if g > 255: g = 255; + # if g < 0: g = 0 + # if b > 255: b = 255; + # if b < 0: b = 0 + # if not 0 <= pad_nr <= 64: + # logging.error(f"Padnr wrong. not in 1..64 pad_nr = {pad_nr}") + # return False + # + # r1= r // 16 ; r2= r % 16 + # g1= g // 16 ; g2= g % 16 + # b1= b // 16 ; b2= b % 16 + # sysex = bytes ([240,71,127,21,4,0,8,pad_nr,0,r1,r2,g1,g2,b1,b2,247] ) + # lib_zyncore.dev_send_midi_event(self.idev_out, sysex, len(sysex)) def send_sysex(self, data): return @@ -668,12 +648,12 @@ def send_sysex(self, data): ##define RIGHT_ARROW 30 ##define LEFT_ARROW 31 -class Display: +class Feedback_Display: display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten def __init__ (self, idev_out): - self.dbg = True + # self.dbg = True self.idev_out = idev_out # if self.dbg: logging.error(f"BRUMBY: Class Display instantiiert") @@ -681,15 +661,12 @@ def __init__ (self, idev_out): def clear (self): + """Overwrites whole display with ascii 32""" + # logging.info(f"Display.clear: end of func idev_out={self.idev_out}") + # clear out display_memory with blanks. self.display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten - - - """Overwrites whole display with ascii 32""" - #if self.idve_out == 0: - # pass - #logging.error(f"BRUMBY: Display.clear: idev_out={self.idev_out}") - + # SYSEX_ZEILE_LÖSCHEN = 240,71,127,21,<28+line(0-3)>,0,0,247 s0 = bytes([240,71,127,21,28,0,0,247]) # Zeile 0 s1 = bytes([240,71,127,21,29,0,0,247]) # Zeile 1 @@ -699,7 +676,6 @@ def clear (self): lib_zyncore.dev_send_midi_event(self.idev_out, x, len(x)) sleep(0.05) - # logging.error(f"BRUMBY: Display.clear: end of func idev_out={self.idev_out}") def update (self): # move display memory to display with sysex @@ -715,12 +691,10 @@ def update (self): lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) sleep(0.05) return - # not implemented yet - def write_xy (self, text, col_in, row_in): - # schreibt Text an Position col_in, row_in in display memory und auf Display + # writes to display memory at Position col_in, row_in in display memory und auf Display # mit update # Koordinaten prüfen @@ -738,44 +712,6 @@ def write_xy (self, text, col_in, row_in): self.update() return - - """ #dbg = False - #if dbg: logging.error(f"BRUMBY: Display.write_xy text={text}x={col_in} y={row_in}") - row=row_in; - col=col_in - if not type(text) is bytes: - text = 'TypError b\'text\' erwartet' - logging.error(f"BRUMBY: TypeError b'text' erwartet und nicht {text}->{type(text)}") - - if (row < 0) : row = 0 - if (row > 3) : row = 3 - if (col < 0) : col = 0 - if (col > 63): col = 63 # nur der erste Char von text wäre druckbar - - #if dbg: logging.error(f"BRUMBY: Display.write_xy 2 text={text}x={col_in} y={row_in}") - - # Textlänge prüfen, ob im erlaubten Bereich. - text_len = len(text) - if text_len + col > 68 : text = text[:68-col] # Rest abschneiden - text_len = len(text) - - # here the magic happens and sysex is cunstructed - # 240, 71, 127, 21, <24+line(0-3)>,0, ,,, 247 - msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) - - #if dbg: logging.error(f"BRUMBY: Display.write_xy 4 SYSEX={msg}") - - lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - # time.sleep(0.5) - #if dbg: logging.error(f"BRUMBY: END_OF_Display.write_xy 2 ") - """ - -# This might belong to the display setup -# Contrast request 240,71,127,21,122,0,0,247 -# Contrast set 240,71,127,21,122,0,1,, 247 -# Brightness request 240,71,127,21,124,0,0,247 -# Brightness set 240,71,127,21,124,0,1,,247 - def contrast (self, i=None) -> int: """ Setzt oder liest den Kontrast des Geräts via SysEx. @@ -783,16 +719,12 @@ def contrast (self, i=None) -> int: Args: i (int, optional): Der gewünschte Kontrastwert (typischerweise 0-127). Wenn None, wird der aktuelle Kontrast gelesen. - Returns: int: Der aktuelle Kontrastwert (nach Setzung oder Abfrage). - Raises: ValueError: Wenn der Kontrastwert außerhalb des gültigen Bereichs liegt. """ - # Überprüfen, ob ein Wert zum Setzen übergeben wurde - if i is not None: if i < 0 : i = 0 @@ -807,13 +739,11 @@ def contrast (self, i=None) -> int: lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) return i - # send sysexrequest - # Contrast request 240,71,127,21,122,0,0,247 - msg = bytes([240,71,127,21,122,0,0,247]) - + ### send sysexrequest + ### Contrast request 240,71,127,21,122,0,0,247 + # msg = bytes([240,71,127,21,122,0,0,247]) # lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - - # Return Contrast. Not implemented + ### Return Contrast. Not implemented. Must be in event chain ais sysex anwser return None @@ -838,29 +768,137 @@ def brightnes (self, i=None) -> int: logging.error(f"BRUMBY zu brightnes={i} geändert") - - # send sysexrequest # Brightness request 240,71,127,21,124,0,0,247 # commented out; getting return value not implemented yet. don't know how to # msg = bytes([240,71,127,21,124,0,0,247]) # lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - # Return Brightnes. Not implemented + # Return Brightnes. Not implemented. Answer comes as sysex in event chain return None + + def first_screen(self): + self.clear() + sleep(0.01) # necessary delays, otherwise sysex hick ups + self.brightnes(36) + sleep(0.01) + self.write_xy(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) + sleep(0.01) + # Positionierungshilfe + self.write_xy(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) + sleep(0.01) + self.write_xy(b'** Zynthian Push1Driver 0.1 **', 17,2) + sleep(0.1) + self.write_xy(b'++ Make MusicNot War ++', 20,3) + return +# -------------------------------------------------------------------------- +# Feedback LEDs controller +# -------------------------------------------------------------------------- +class Feedback_Mono_LEDs: + #Takt Notenlängen Pfeile Track-Modifier Copy/Del/undo + _all_mono = [3,9, 28,29, 36,37,38,39,40,41,42,43, 44,45,46,47, 48,49,50,51,52,53,54,55,56,57,58,59,61,62,63, 85,86,87,88,89,90, 110,111,11,113,114,115, 116,117,118,119, ] + + def __init__(self, idev): + self._idev = idev + self._state = {} + self._timer = RunTimer() + + def all_off(self, overlay = False): + for note in self._all_mono: + lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, 0) + if not overlay: + self._led_state[note] = 0 + return + + + def set_mono(self, note:int, grey_val:int, overlay=False): + lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, grey_val) # grey_val something of ABL.MONO_LED_DIM) + if not overlay: + self._led_state[note] = 0 + return + + def refresh_one(self, note): + if self._led_state[note]: # is one saved? + lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, self._led_state[note]) + + def refresh(self): + for note in self._all_mono: + if self._led_state[note]: + lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, self._led_state[note]) + + +class Feedback_Bi_LEDs: + def __init__(self, idev): + self._idev = idev + self._state = {} + self._timer = RunTimer() + def all_off(self): + pass + +# RGB LED Class for the pads rgb-LEDs +class Feedback_RGB_LEDs: + + # _led_states = {} + + def __init__(self, idev): + self._idev = idev + self._state = {} + self._timer = RunTimer() + self._led_state = {} + + def all_off(self, overlay): + for pad_nr in range(ABL_PAD_END+1-ABL_PAD_START): + self.set_rgb(pad_nr,0,0,0, overlay) + if not overlay: + self._led_state[pad_nr] = [0,0,0] + return + + def refresh(self): # whole array of pads + for pad_nr in range(ABL_PAD_END+1-ABL_PAD_START): + self.refresh_one(pad_nr) + + + def refresh_one(self, pad_nr): # get back to saved value + self.set_rgb(pad_nr, self._led_state[pad_nr][0], self._led_state[pad_nr][1], self._led_state[pad_nr][2]) + + + def off_col_row(self, col, row): + # note = 96 + row * 16 + col # statt 96 -> 91 für Push + note = ABL_PAD_END +1 -(row+1) * 8 + col # recalculate midi note from col and row + # logging.info(f"BRUMBY: row={row}; col={col} pad-note={note}") + lib_zyncore.dev_send_note_on(self._idev, 0, note, 0) # this is palette mode. + def set_rgb(self, pad_nr: int, r:int ,g:int ,b:int, overlay=False): + # Sysex : 240,71,127,21,4,0,8,,0,,,,,,,247 + # pad = 0-71 NICHT PAD_36 - PAD_99 + # blogspot.com + # To set a pad color to a RGB(0-255) value, the RGB values need to be set into "Push" format, for example: + # r1 = r /(integer division) 16 + # r2 = r %(modulo) 16 + # So a value of R132 would become: r1=8 r2=4. + # The pad index if from 0 to 71, zero being the bottom left pad, all the way up to the second row button to the right (second row of buttons is RGB). + if r > 255: r = 255; + if r < 0: r = 0 + if g > 255: g = 255; + if g < 0: g = 0 + if b > 255: b = 255; + if b < 0: b = 0 + if not 0 <= pad_nr <= 64: + logging.error(f"Padnr wrong. not in 1..64 pad_nr = {pad_nr}") + return False + + # self._led_state.setdefault(pad_nr, []).append(i) + if not overlay: + self._led_state[pad_nr] = [r,g,b] + + r1= r // 16 ; r2= r % 16 + g1= g // 16 ; g2= g % 16 + b1= b // 16 ; b2= b % 16 + sysex = bytes ([240,71,127,21,4,0,8,pad_nr,0,r1,r2,g1,g2,b1,b2,247] ) + # lib_zyncore.dev + lib_zyncore.dev_send_midi_event(self._idev, sysex, len(sysex)) - - - - - - -# Zynthian specific modules -# from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad # , zynthian_ctrldev_zynmixer -# from zyncoder.zyncore import lib_zyncore -# from zynlibs.zynseq import zynseq From 4d23078ed9d33e33a545a04f3ab5d9cd9cd6f7b6 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 14 Sep 2025 01:03:53 +0200 Subject: [PATCH 20/57] Some fixes. but is broken. Harmony is on the way. --- zyngine/ctrldev/ableton/push1_consts.py | 5 + .../zynthian_ctrldev_ableton_push_1.py | 132 +++++++++---- .../ctrldev/zynthian_ctrldev_base_scale.py | 186 +++++++++++++----- 3 files changed, 238 insertions(+), 85 deletions(-) diff --git a/zyngine/ctrldev/ableton/push1_consts.py b/zyngine/ctrldev/ableton/push1_consts.py index 97d235ccf..45fb21d0e 100644 --- a/zyngine/ctrldev/ableton/push1_consts.py +++ b/zyngine/ctrldev/ableton/push1_consts.py @@ -37,6 +37,9 @@ KNOB_7 = [0xB0, 77] # CC77 KNOB_8 = [0xB0, 78] # CC78 KNOB_9 = [0xB0, 79] # CC79 +KNOB_10 = [0xB0, 14] # CC79 +KNOB_11 = [0xB0, 15] # CC79 + # Touch KNOB_1_T = [0x90,0] # "C-1" sic! @@ -48,6 +51,8 @@ KNOB_7_T = [0x90,6] # note 6 KNOB_8_T = [0x90,7] # note 7 KNOB_9_T = [0x90,8] # Note 8 +KNOB_10_T = [0x90,9] +KNOB_11_T = [0x90,10] RIBBON_TOUCH_T = [0x90,12] # "C0" note 12 RIBBON_PITCH = [0xE0] # Mod-wheel ?? ### Achtung einziger Identifier, der nur 1 byte hat!!! ### diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 4e2b6ad4b..56ea47f11 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -105,10 +105,11 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ device_mode_active = DEV_MODE_SCALES scales = Harmony(8,8) - scales.init_scale("Minor", 36-1, -5) # -3 = new start per row + scales.init_scale(tonic=0, middle_c=48) # (0, "Major", 36-1, -5) # -3 = new start per row # Function to initialise class + # called from parent (instance) def __init__(self, state_manager, idev_in, idev_out=None): logging.info("Created Instance from Ableton Push 1 driver - BRUMBY") @@ -123,26 +124,24 @@ def __init__(self, state_manager, idev_in, idev_out=None): super().__init__(state_manager, idev_in, idev_out) - # seems to be necessary, because we send translates midi_events. + # seems to be necessary, because we send translated midi_events. o self.unroute_from_chains = True return + # called from parent def init(self): try: logging.info("called init. Setting up Ableton Push 1 - BRUMBY") self.shift = False + + # set initial device mode self.device_mode_active = self.DEV_MODE_SCALES - self.set_dev_scale_color() - + + # setup device screen self._display.first_screen() - # Hier muss die Trackstaste zum Leuchten gebracht werden am Push 1 - # Enable session mode on launchkey - # Track-Taste CC112 # ABL_TRACK - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 112, ABL.MONO_LED_LIT) # 2! - # CC62 = OK; CC63 = Back (ABL_OK, ABL_ESC - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 62, ABL.MONO_LED_LIT_BLINK) # 2! + # setup LEDS in Ctrl-Buttons # Monochrome Tasten die hell leuchten sollen for t in [ 36,37,38,39,40,41,42,43, ABL.BTN_START[1], ABL.BTN_OK[1], ABL.BTN_ESC[1], ABL.BTN_LEFT[1], @@ -159,12 +158,13 @@ def init(self): lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_ORANGE_DIM) ### lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) + # setup device pad arry size self.cols = 8 self.rows = 8 # war 2 20250829-2134 super().init() # aktiviert. Muss aktiviert sein! # self.pads_off() if self.device_mode_active == self.DEV_MODE_SCALES: - self.set_dev_scale_color() + self.set_dev_to_scales_mode() #except: # print("Fehler aufgetreten: {e}") @@ -175,13 +175,57 @@ def init(self): # logging.error("Exception aufgetreten: %s", e) # logging.error("Traceback: %s", traceback.format_exc()) - + # called from parent def end(self): # logging.error("end Ableton Push 1 - BRUMBY") super().end() ### Disable session mode on launchkey ## lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 0) # device, channel, note, velocity + # new in this class, to setup scales_mode = keyboard mode + def set_dev_to_scales_mode(self): + self.device_mode_active = self.DEV_MODE_SCALES + # visual feedback, let Scales Button blink + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) + self.pads_off() # akk pad leds off + self.set_dev_scale_color() # set LEDs for scale mode + self._display.clear() + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + self._display.write_xy_mem(scale_n_mode, 0, 3) + self._display.update_screen() + + + def set_tonic(self, step): + if step > 63: step -=128 + self.scales.tonic = self.scales.tonic + step + if self.scales.tonic < 0: self.scales.tonic = 11 # target: B + if self.scales.tonic > 11: self.scales.tonic = 0 # target: C + self.set_dev_to_scales_mode(); + self.scales.init_scale(self.scales.tonic, self.scales.active_mode) + + def set_mode(self, step): + modenames = self.scales.harmony_get_mode_names() + nr_of_modes = len(modenames) + result = None + if not self.scales.active_mode: self.scales.active_mode = modenames[0] + if step > 63: step -=128 + for i in range(nr_of_modes): + if modenames[i] == self.scales.active_mode: + result = i + break + if not result == None: + result += step + if result >= nr_of_modes: result = 0 + elif result < 0 : result = nr_of_modes-1 + + new_mode = modenames[result] + self.scales.active_mode = new_mode + else: + logging.error("Bug in set_mode") + # do the magic + self.set_dev_to_scales_mode(); + self.scales.init_scale(self.scales.tonic, self.scales.active_mode) # self.scales.tonic, self.scales.active_mode, 36-1, -5) + ### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. ### just copy the derived functions in the this driver and implement them accordingly def update_mixer_active_chain(self, active_chain): @@ -207,7 +251,7 @@ def update_mixer_strip(self, chan, symbol, value): ### Start of SEQUENCER FUNCTIONS # this function is called by zynseq when a sequencer state is changed - # we will update pad LED to show state + # we have update pad LED to show state def update_seq_state(self, bank, seq, state, mode, group): try: # return @@ -257,8 +301,11 @@ def update_seq_state(self, bank, seq, state, mode, group): except ValueError as e: print(f"Fehler aufgetreten: {e}") - - + # for LED feedback bei pad mode (Sequencer) + def refresh(self): # form zynseq classe + # if not filtered, the pad loop kills any other LED setup + if self.device_mode_active == self.DEV_MODE_PAD: + return super().refresh() def pad_off(self, col, row): # note = 96 + row * 16 + col # statt 96 -> 91 für Push @@ -410,11 +457,12 @@ def midi_event(self, ev): elif (ccnum == ABL.BTN_SCALES[1]): logging.info("BRUMBY: BTN_SCALES processing") if not self.device_mode_active == self.DEV_MODE_SCALES: - self.device_mode_active = self.DEV_MODE_SCALES + # self.device_mode_active = self.DEV_MODE_SCALES # visual feedback, let Scales Button blink - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) - self.pads_off() # akk pad leds off - self.set_dev_scale_color() + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) + # self.pads_off() # akk pad leds off + # self.set_dev_scale_color() + self.set_dev_to_scales_mode() else: self.device_mode_active = self.DEV_MODE_PAD # visual feedback, set LED to solid on @@ -445,11 +493,16 @@ def process_gui_events(self,ev) -> bool: ccnum = ev[1] & 0x7F ccval = ev[2] & 0x7F - + if ABL.KNOB_7[1] == ccnum: # scale + self.set_tonic(ccval) + + elif ABL.KNOB_8[1] == ccnum: # mode + self.set_mode(ccval) + # Zynpoties Werte an GUI # Potis Oben 72 - 75 die ersten 4 # if 70 < ccnum < 80: - if ABL.KNOB_1[1] <= ccnum <= ABL.KNOB_4[1]: + elif ABL.KNOB_1[1] <= ccnum <= ABL.KNOB_4[1]: # self.state_manager.send_cuia("ZYNPOT_ABS", [ccnum - 72, ccval/127]) val = ccval if val > 68: @@ -555,7 +608,8 @@ def set_dev_scale_color(self): self._leds_rgb.all_off(True) # led_states must not be deleted. is done in next lines for pad_nr in range(64): new_note = self.scales.harmony_get_target_note(pad_nr) - if new_note % 12 == 0: + if self.scales.is_tonic_by_midnote(new_note): + print (f"found: Tonic {new_note}") r = 0; g = 0; b = 255 else: r = 200; g = 200; b = 200 @@ -677,7 +731,7 @@ def clear (self): sleep(0.05) - def update (self): + def update_screen (self): # move display memory to display with sysex for row in range(4): #msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) @@ -693,10 +747,21 @@ def update (self): return - def write_xy (self, text, col_in, row_in): + def write_xy_mem (self, text, col_in:int, row_in:int): # writes to display memory at Position col_in, row_in in display memory und auf Display # mit update + #convert text to bytes + if isinstance(text, str): + # print("Die Variable ist ein String (Text)") + text = text.encode() + elif isinstance(text, bytes): + # print("Die Variable ist Bytes") + pass # is fine + else: + # print("Die Variable ist weder String noch Bytes") + text = "Typeerror in textconversion".encode() + # Koordinaten prüfen if(row_in > 3): row_in = 3 if(row_in < 0): row_in = 0 @@ -709,7 +774,7 @@ def write_xy (self, text, col_in, row_in): text_len = len(text) self.display_mem[row_in][col_in:col_in+text_len] = list(text) - self.update() + # self.update() return def contrast (self, i=None) -> int: @@ -779,17 +844,14 @@ def brightnes (self, i=None) -> int: def first_screen(self): self.clear() - sleep(0.01) # necessary delays, otherwise sysex hick ups - self.brightnes(36) - sleep(0.01) - self.write_xy(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) - sleep(0.01) - # Positionierungshilfe - self.write_xy(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) - sleep(0.01) - self.write_xy(b'** Zynthian Push1Driver 0.1 **', 17,2) + # self.brightnes(36) sleep(0.1) - self.write_xy(b'++ Make MusicNot War ++', 20,3) + self.write_xy_mem(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) + # Positionierungshilfe + self.write_xy_mem(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) + self.write_xy_mem(b'** Zynthian Push1Driver 0.1 **', 17,2) + self.write_xy_mem(b'++ Make MusicNot War ++', 20,3) + self.update_screen() return diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index c9deb8507..ef0382150 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -6,7 +6,7 @@ # Following from: https://github.com/Carlborg/hardpush/blob/master/hardpush.ino # All scales seem to work as 12-semitone scales. (otherwise they would need the octave-distance at the end) -_SCALES = { # Define scales in the form 'semitones added to tonic' +_MODES = { # Define scales in the form 'semitones added to tonic' 'Chromatic': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "Major": [0, 2, 4, 5, 7, 9, 11], "Minor": [0, 2, 3, 5, 7, 8, 10], @@ -35,6 +35,8 @@ "Spanish": [0, 1, 3, 4, 5, 6, 8, 10] } +_SCALES = ["C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B" ] # same as midi-notes 0-11 + ### How to get names and values from a dictionary: ## list(scales) # ['Chromatic', 'Major', 'Minor', 'Dorian', 'Mixolydian', 'Lydian', 'Phrygian', 'Locrian', 'Diminished', 'Whole-Half', 'Whole Tone', 'Minor Blues', 'Minor Pentatonic', 'Major Pentatonic', 'Harmonic Minor', 'Melodic Minor', 'Super Locrian', 'Bhairav', 'Hungarian Minor', 'Minor Gypsy', 'Hirojoshi', 'In-Sen', 'Iwato', 'Kumoi', 'Pelog', 'Spanish'] ## list(scales)[0] # "Chromatic" @@ -45,6 +47,7 @@ class Harmony: # Class variables: Hardware of device and Scales don't change in instances + modes = _MODES scales = _SCALES ### Next the instance variables, set up by init() function, are different for each instance @@ -53,81 +56,163 @@ class Harmony: ### target_notes = [] ### target_notes_reverse = {} # To get pads with same MIDI note to light them up when pressed ### col_versatz = -5 - ### active_scale = "Major" + ### active_mode = "Major" + ### active_scale = 0 # means "C" def __init__(self, pad_cols, pad_rows): self.cols = pad_cols self.rows = pad_rows self.target_notes = [] # Instance variable self.target_notes_reverse = {} - self.active_scale = None + self.active_mode = None self.col_versatz = 0 # 0 means linear, no offset - def init_scale(self, scale_name, note_start, col_versatz): - """scale_name: name of scale in self._scales - note_start: number of the tone in scale with octaves (12 would be second octave tonic) + + # def init_scale(self, tonic: int, mode_name: str, note_start: int, col_versatz: int): + # """tonic : tonic of scale as midinote 0-11 (semitones) + # mode_name: name of mode from self._modes + # note_start: number of the tone in scale with octaves (12 would be second octave tonic) + # col_versatz: each row can start with a different offset, so -5 means in C-Major-Scale an "F" above the C in row-1 line + # """ + # self.tonic = tonic + # self.col_versatz = col_versatz + # self.active_mode = mode_name + # self.pad1_midi_note = note_start # midi_note for pad1 + + # self.target_notes = [] # Reset for new scale + # self.target_notes_reverse = {} # Reset for new scale + + # for i in range(self.cols * self.rows): + # in_row = i // self.cols + # h = i + (in_row * col_versatz) + # if console_debug: print(f"{h} ", end="", flush=True) + # note_new = self._harmony_calculate_midi_note(h) + + # self.target_notes.append(note_new) # always as "C scale" + # # Reverse mapping + # self.target_notes_reverse.setdefault(note_new, []).append(i) # always as "C-scale" + + # if console_debug: + # print(f"({note_new}), ", end="\t", flush=True) + # if (i+1) % 8 == 0: # self.cols == 0: + # print("*", end="\n", flush=True) # Newline + # return + + + + def init_scale(self, + tonic: int = 0, # (C) semitone distance counted from from C = 0 + mode_name: str = "Major", # mode as str from Array + col_versatz: int = -5, # per row recess + middle_c: int = 36, # must be middle_c % 12 = 60 + middle_pad_nr: int = 3): # padnr of middle tonic + + """tonic : tonic of scale as midinote 0-11 (semitones) + mode_name: name of mode from self._modes + middle_C: number of the tone in scale with octaves (12 would be second octave tonic) col_versatz: each row can start with a different offset, so -5 means in C-Major-Scale an "F" above the C in row-1 line """ + + + self.tonic = tonic self.col_versatz = col_versatz - self.active_scale = scale_name + self.middle_c = middle_c + self.active_mode = mode_name + self.target_notes = [] # Reset for new scale self.target_notes_reverse = {} # Reset for new scale + + if middle_pad_nr < 0 : middle_pad_nr = 0 + if middle_pad_nr>63 : middle_pad_nr = 63 + + mode = self.modes[self.active_mode] + pad_counter = -1 - for i in range(self.cols * self.rows): - in_row = i // self.cols - h = i + note_start + (in_row * col_versatz) - if console_debug: print(f"{h} ", end="") - note_new = self._harmony_calculate_midi_note(h) + for i in range (-middle_pad_nr, (self.cols*self.rows) - middle_pad_nr): + pad_counter += 1 + + row_nr = (pad_counter +1) // self.cols + + note_nr_in_scale = i + (row_nr * col_versatz) - self.target_notes.append(note_new) + + octave = note_nr_in_scale // len(mode) + if console_debug: print (f"{octave}:{pad_counter}=", end="") + + note_in_mode = note_nr_in_scale % len(mode) + note = mode[note_in_mode] + note += octave * 12 + note += self.middle_c + + self.target_notes.append(note) # always as "C scale" # Reverse mapping - self.target_notes_reverse.setdefault(note_new, []).append(i) + self.target_notes_reverse.setdefault(note, []).append(pad_counter) # always as "C-scale" if console_debug: - print(f"({note_new}), ", end="\t") - if i % self.cols == 0: print() # Newline - - - def _harmony_calculate_midi_note(self, note) -> int: - """Parameters: - scale: string with name of scale - note: integer representing the starting point in the scale. If start is bigger than - the length of the specified scale it adds start % len(scale) * 12 to the result. - So you can cycle through the number of keyboard keys to get their MIDI notes - """ - try: - scale = self.scales[self.active_scale] - pos_in_scale = note % len(scale) # - octave = note // len(scale) - return scale[pos_in_scale] + (octave * 12) - except KeyError: - logging.error(f"Error: Scale '{self.active_scale}' not found!") - return -1 # -1 is error, there is no MIDI note -1 - except Exception as e: - logging.error(f"Error calculating MIDI note: {e}") - return -1 - + print(f"({note}), ", end="\t", flush=True) + if (pad_counter+1) % 8 == 0: # self.cols == 0: + print("*", end="\n", flush=True) # Newline + return + + + + # def _harmony_calculate_midi_note(self, note_in_scale) -> int: + # """Parameters: + # scale: string with name of scale + # note: integer representing the starting point in the scale. If start is bigger than + # the length of the specified scale it adds start % len(scale) * 12 to the result. + # So you can cycle through the number of keyboard keys to get their MIDI notes + # """ + # try: + # mode = self.modes[self.active_mode] + # pos_in_mode = note_in_scale % len(mode) # + # octave = note_in_scale // len(mode) + # return mode[pos_in_mode] + (octave * 12) # is based "C-Scale" + # except KeyError: + # logging.error(f"Error: Mode '{self.active_mode}' not found!") + # return -1 # -1 is error, there is no MIDI note -1 + # except Exception as e: + # logging.error(f"Error calculating MIDI note: {e}") + # return -1 + + def is_tonic_by_padnr(self, padnr:int)-> bool: + return self.target_notes[padnr] % 12 == 0 + + + def is_tonic_by_midnote(self, midi_note:int) -> bool: + return (midi_note - self.tonic) % 12 + + def get_equi_sound_pads_with_midi_note(self, midi_note) -> list: + return self.target_notes_reverse[(midi_note - self.tonic)] + + def get_equi_sound_pads_with_pad_nr(self, pad_nr): + midi_note = self.target_notes[pad_nr] + return self.target_notes_reverse[midi_note] - def harmony_get_scale_len(self, scale) -> int: - """Return count of tones in scale""" + def harmony_get_mode_len(self, mode:str) -> int: + """Return count of tones mode""" try: - logging.debug(f"[Debug len]: scale={scale}, len={len(self.scales[scale])}") - return len(self.scales[scale]) + logging.debug(f"[Debug len]: mode={mode}, len={len(self.modes[mode])}") + return len(self.modes[mode]) except KeyError: - logging.error(f"Error: get_scale_len: scale '{scale}' not defined") + logging.error(f"Error: get_mode_len: mode '{mode}' not defined") return 0 - def harmony_get_scale_names(self): - return list(self.scales) + def harmony_get_mode_names(self): + return list(self.modes) + + def harmony_get_scale_name_with_mode (self) -> str: + result = self.scales[self.tonic] + ' ' + self.active_mode + return result def harmony_get_target_note(self, pad_nr: int) -> int: if not 0 <= pad_nr < len(self.target_notes): logging.error("Program error, pad_nr out of range for len(target_notes)") return None - return self.target_notes[pad_nr] + return self.target_notes[pad_nr] + self.tonic def harmony_get_padnrs_with_same_note(self, midi_note: int): - return self.target_notes_reverse.get(midi_note, [midi_note]) # If nothing is in map, midi_note itself is given back + return self.target_notes_reverse.get(midi_note, [midi_note- self.tonic]) # If nothing is in map, midi_note itself is given back ### End of class definition Harmony ############################################## @@ -135,11 +220,12 @@ def harmony_get_padnrs_with_same_note(self, midi_note: int): ### For test purposes from command line if __name__ == "__main__": - console_debug = False - + console_debug = True + print() h = Harmony(8, 8) - h.init_scale("Major", 0, -5) - + # h.init_scale(0, "Diminished", 0, -5) + h.init_scale_2() + print() # print(h.harmony_get_scale_names()) # print(h.harmony_get_midi_note("Kumoi", 4)) # print(h.harmony_get_scale_len("Kumoi")) \ No newline at end of file From 417db547599c9edbf2ea9c8a50b2ad790bd1406e Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 14 Sep 2025 01:47:10 +0200 Subject: [PATCH 21/57] Fixed range error in harmonies --- .../zynthian_ctrldev_ableton_push_1.py | 5 +++- .../ctrldev/zynthian_ctrldev_base_scale.py | 28 +++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 56ea47f11..3665da049 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -386,7 +386,10 @@ def midi_event(self, ev): # here magic for different sccale layouts happens. # it translates midi_note events to the translated note_events note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes are based 0 pad_nr based 1 - new_ev = bytes([ev[0], note_translated, ev[2]*2]) + vel = ev[2] + if evtype == self.EV_NOTE_ON: vel = ev[2] *2 # this creates junk with aftertouch und pitchbend.. + if vel > 255: vel = 255 + new_ev = bytes([ev[0], note_translated, vel]) #if note_translated % 12 == 0: # Oktave detected # pass # for note_on events following. diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index ef0382150..7d67c7096 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -123,7 +123,8 @@ def init_scale(self, self.target_notes_reverse = {} # Reset for new scale if middle_pad_nr < 0 : middle_pad_nr = 0 - if middle_pad_nr>63 : middle_pad_nr = 63 + if middle_pad_nr >= self.cols * self.rows: + middle_pad_nr = self.cols * self.rows -1 mode = self.modes[self.active_mode] pad_counter = -1 @@ -131,7 +132,7 @@ def init_scale(self, for i in range (-middle_pad_nr, (self.cols*self.rows) - middle_pad_nr): pad_counter += 1 - row_nr = (pad_counter +1) // self.cols + row_nr = pad_counter // self.cols note_nr_in_scale = i + (row_nr * col_versatz) @@ -144,14 +145,16 @@ def init_scale(self, note += octave * 12 note += self.middle_c + # store notes without tonic for internal represetation self.target_notes.append(note) # always as "C scale" # Reverse mapping - self.target_notes_reverse.setdefault(note, []).append(pad_counter) # always as "C-scale" + self.target_notes_reverse.setdefault(note, []).append(pad_counter) if console_debug: - print(f"({note}), ", end="\t", flush=True) - if (pad_counter+1) % 8 == 0: # self.cols == 0: - print("*", end="\n", flush=True) # Newline + actual_note = note + tonic + print(f"({actual_note}), ", end="\t", flush=True) + if (pad_counter+1) % self.cols == 0: + print("*", end="\n", flush=True) # Newline at end of row return @@ -183,16 +186,17 @@ def is_tonic_by_midnote(self, midi_note:int) -> bool: return (midi_note - self.tonic) % 12 def get_equi_sound_pads_with_midi_note(self, midi_note) -> list: - return self.target_notes_reverse[(midi_note - self.tonic)] + # Subtract tonic to get internal representation + internal_note = midi_note - self.tonic + return self.target_notes_reverse.get(internal_note, []) def get_equi_sound_pads_with_pad_nr(self, pad_nr): midi_note = self.target_notes[pad_nr] - return self.target_notes_reverse[midi_note] + return self.target_notes_reverse.get(midi_note,[]) def harmony_get_mode_len(self, mode:str) -> int: """Return count of tones mode""" try: - logging.debug(f"[Debug len]: mode={mode}, len={len(self.modes[mode])}") return len(self.modes[mode]) except KeyError: logging.error(f"Error: get_mode_len: mode '{mode}' not defined") @@ -212,8 +216,10 @@ def harmony_get_target_note(self, pad_nr: int) -> int: return self.target_notes[pad_nr] + self.tonic def harmony_get_padnrs_with_same_note(self, midi_note: int): - return self.target_notes_reverse.get(midi_note, [midi_note- self.tonic]) # If nothing is in map, midi_note itself is given back - + internal_note = midi_note - self.tonic + return self.target_notes_reverse.get(internal_note, []) + + ### End of class definition Harmony ############################################## From 013a2cabe50874d7d83ce02d7be3a796bde130e9 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 14 Sep 2025 02:27:23 +0200 Subject: [PATCH 22/57] fixed my bad english --- .../zynthian_ctrldev_keystation_pro_88_mk1.py | 278 ++++++++---------- 1 file changed, 123 insertions(+), 155 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py index e9941e542..d7933cc58 100644 --- a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py @@ -2,175 +2,143 @@ # -*- coding: UTF-8 -*- # # Minimalistic Zynthian Control Device Driver for M-Audio Keystation Pro 88 -# used for Zynthina with touch screen (but without rotary encoders) -# The device driver can controll the gui with the 4 knobs -# -# The keystation pro 88 no LED, so there is no feedback possible on the device -# also it doesn't send key on and of messages, just program change on press, -# so its not possible to detect long and short presses. -# -# Rotary Encoders are simulatet with Knobs 18, 19, 10, 11 -# -# this sample driver shows how easy it is to write a custom driver +# Designed for Zynthian with touch screen (but without rotary encoders) +# The device driver can control the GUI using the 4 knobs +# The Keystation Pro 88 has no LEDs, so no visual feedback is possible on the device +# It also doesn't send key on/off messages, only program change messages on press, +# making it impossible to detect long and short presses. +# Rotary encoders are simulated with knobs 18, 19, 10, and 11 +# This sample driver demonstrates how easy it is to write a custom driver # for a specific MIDI controller -# -# everything that is not essential is commented out -# -# Strange, when this driver throws an exception, the midi event is processed, as if the midi_event() sends back "False" -# I realized this, when I had a mistake in my send_midi function (library zynseq was not importet) -# need further exploration +# Note: When this driver throws an exception, the MIDI event is still processed +# as if midi_event() returned "False". This was noticed when there was an error +# in the send_midi function (zynseq library was not imported). +# This requires further investigation import logging from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_base -from zynlibs.zynseq import zynseq # to send midi directly from this driver +from zynlibs.zynseq import zynseq # For sending MIDI directly from this driver logger = logging.getLogger('zynthian') class zynthian_ctrldev_keystation_pro_88_mk1(zynthian_ctrldev_base): + # Device identification + # dev_id = ["Keystation Pro 88"] # Optional + + # There's no straightforward way to list device IDs on the Linux console + # The device IDs in Zynthian differ from those found in the Linux console + # Debugging is the easiest way to find the correct device IDs + # You can try using the device name with " IN 1" and " IN 2" suffixes + + dev_ids = ["Keystation Pro 88 IN 1"] # These values are essential - # Device identification - # dev_id = ["Keystation Pro 88"] # not essential - - # found no way to list the dev_ids on linux console. - # They are different in Zynthian to that what I found in linux console # - # with debugging it is easy to find the correct dev_ids. Found no other way. You could try - # device name with " IN 1" and "IN 2" at the end - # dev_ids = ["Keystation Pro 88 IN 1", "Keystation Pro 88 IN 2"] # these values are ESSENTIAL for the driver to connect it to the device - dev_ids = ["Keystation Pro 88 IN 1"] # these values are ESSENTIAL for the driver to connect it to the device. Data is just at Port 1 - - # driver_name = "Keystation Pro 88 Minimal" # not essential, just for information in logs - - # driver_description = "Minimalistic Zynthian Control Device Driver für M-Audio Keystation Pro 88" - # not essential. just for information in logs - - # driver_version = "0.1" - - # Helper variables for potentiometers. Hack, because ZYNPOT_ABS didn't work for me... - zynpot_0 = 0 - zynpot_1 = 0 - zynpot_2 = 0 - zynpot_3 = 0 - - # evtype = (ev[0] >> 4) & 0x0F -> - EV_NOTE_OFF = 0x8 # 3 Bytes - EV_NOTE_ON = 0X9 # 3 Bytes - EV_AFTERTOUSCH = 0xA # 3 Bytes (polyphonic = per note) - EV_CC = 0xB # 3 Bytes - EV_PC = 0xC # 2 Bytes - EV_CHAN_PRESS = 0xD # 2 Bytes - EV_PITCHBEND = 0xE # 3 bytes ev[1] = LSB 0-127; ev[2] = MSB 0-127 - EV_SYSTEM = 0xF # Systemtype = ev[0] & 0x0F + # driver_name = "Keystation Pro 88 Minimal" # Optional, for log information + # driver_description = "Minimalistic Zynthian Control Device Driver for M-Audio Keystation Pro 88" + # driver_version = "0.1" - - def midi_event(self, ev): - """Easy MIDI event handler for Keystation Pro 88""" - # self.unroute_from_chains = False # otherwise no Keyboard anymore, just a controller - # self.enabled = True - evtype = (ev[0] >> 4) & 0x0F - - if len(ev) == 3: logger.debug(f"MIDI Event empfangen: {ev} {ev[0]} {ev[1]} {ev[2]}") - - if len(ev) > 0: - status= ev[0] & 0xF0 # Which midi message type (note on, note off, control change, etc. ) - # channel = ev[0] & 0x0F # not usesd, - - if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUSCH, self.EV_PITCHBEND]: + # Helper variables for potentiometers. Workaround because ZYNPOT_ABS didn't work + zynpot_0 = 0 + zynpot_1 = 0 + zynpot_2 = 0 + zynpot_3 = 0 + + # MIDI event types + EV_NOTE_OFF = 0x8 # 3 bytes + EV_NOTE_ON = 0x9 # 3 bytes + EV_AFTERTOUCH = 0xA # 3 bytes (polyphonic = per note) + EV_CC = 0xB # 3 bytes + EV_PC = 0xC # 2 bytes + EV_CHAN_PRESS = 0xD # 2 bytes + EV_PITCHBEND = 0xE # 3 bytes: ev[1] = LSB 0-127; ev[2] = MSB 0-127 + EV_SYSTEM = 0xF # System type = ev[0] & 0x0F + + def midi_event(self, ev): + """MIDI event handler for Keystation Pro 88""" + evtype = (ev[0] >> 4) & 0x0F + + if len(ev) == 3: + logger.debug(f"MIDI event received: {ev} {ev[0]} {ev[1]} {ev[2]}") + + if len(ev) > 0: + status = ev[0] & 0xF0 # MIDI message type (note on, note off, control change, etc.) + # channel = ev[0] & 0x0F # Not used + + # Forward certain events directly to MIDI output + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH, self.EV_PITCHBEND]: return self.send_midi(ev) + + # Process 3-byte events (control changes) + if len(ev) == 3: + data1 = ev[1] # Note number or controller number + data2 = ev[2] # Note velocity or controller value + + # Simulate rotary encoders with knobs + # We send the difference between the last value and the new value + # to the state manager ("ZYNPOT_ABS" would be easier but didn't work) + # The state manager will handle the rest + # We have to store the last value of each knob + # We have 4 knobs for 4 virtual rotary encoders: + # Knob 18 -> ZYNPOT 0 + # Knob 19 -> ZYNPOT 1 + # Knob 10 -> ZYNPOT 2 + # Knob 11 -> ZYNPOT 3 - - if len(ev) == 3: # most times 3 bytes and we need 3 bytes - # status = ev[0] & 0xF0 # Which midi message type (note on, note off, control change, etc. ) - # channel = ev[0] & 0x0F # not usesd, just for information - data1 = ev[1] # Note number or controller number - data2 = ev[2] # Note velocity or controller value + # Note: First use of a knob will jump from 0 to the current knob value + # There's currently no way to get the initial value of the knob at startup + + # 0xB0 is Control Change on MIDI Channel 1 + if status == 0xB0: + if data1 == 104: # Knob 18 in "Preset-Recall 10" + pot = data2 - self.zynpot_0 # Calculate relative change + self.zynpot_0 = data2 # Store new value for next change + self.state_manager.send_cuia("ZYNPOT", [0, pot]) + return True # Event processed - - # We simulate a rotary encoder with the knobs - # We send the difference between the last value and the new value - # to the state manager ("ZYNPOT_ABS" would be easieer to use, but it doesn't work for me. It was never called in my tests) - # The state manager will handle the rest - # We have to store the last value of the knob - # We have 4 knobs for 4 virtual rotary encoders - # Knob 18 -> ZYNPOT 0 - # Knob 19 -> ZYNPOT 1 - # Knob 10 -> ZYNPOT 2 - # Knob 11 -> ZYNPOT 3 - - # yes I know, first time use of a knob lets jump the value from 0 to the knob value - # but I don't know how to get the current value of the knob at start up - # maybe someone can help me with that - - # 0xB0 is Control Change on MIDI Channel 1 - # 10 is the controller number for the first knob - # data2 is the value of the knob (0-127) - - - if status == 0xb0 and data1 == 104: # 42 is knob 18 in "keystations Preset 10. (Press Recall and choose 10)" - # if controller sends relative values, that is: negative values for left turn and positive values for right turn, - # you can use: - # pot = data2 - # but my keystation pro 88 MK1 sends only absolute knob values from 0 to 127 - # so I have to calculate the difference to the last value - pot = data2-self.zynpot_0 # calculates relative change of the value to use "ZYNPOT" instead of "ZYNPOT_ABS" - self.zynpot_0 = data2 # store the new value for the next change - - self.state_manager.send_cuia("ZYNPOT", [0, pot]) - return True # Event processed. restarts event loop - - if status == 0xb0 and data1 == 105: # 34 is knob 19 in "Preset-Recall 10" - pot = data2-self.zynpot_1 - self.zynpot_1 = data2 - self.state_manager.send_cuia("ZYNPOT", [1, pot]) - return True # Event processed. restarts event loop - - if ev[0] == 0xb0 and data1 == 85: # 10 is knob 10 in "Preset-Recall 10" - pot = data2-self.zynpot_2 - self.zynpot_2 = data2 - self.state_manager.send_cuia("ZYNPOT", [2, pot]) - return True # Event processed. restarts event loop - - if ev[0] == 0xb0 and data1 == 86: # 2 is knob 11 in "Preset-Recall 10" - pot = data2-self.zynpot_3 - self.zynpot_3 = data2 - self.state_manager.send_cuia("ZYNPOT", [3, pot]) - return True # Event processed. restarts event loop - - """ if len(ev) == 3: # PC has 3 bytes. - - # To use Buttons on Keystation 88 pro MK1 for "back" and "OK" is no good idea, because - # all Buttons are sending just a program change when pressed. - # You would miss them as Buttons for changing in Zynthina. But nevertheless here as example how to use it - - # status = ev[0] & 0xF0 # Which midi message type (note on, note off, control change, etc. ) - # channel = ev[0] & 0x0F # not usesd, just for information - data1 = ev[1] # Note number or controller number - - - # Buttons for "Ok" and "Back" - if status == 0xC0 and data1 == 0: # Button "Back" - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) - # self.state_manager.send_cuia("BACK") - return True # Event processed. restarts event loop - - if status == 0xC0 and data1 == 1: # Button "OK" - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) - # self.state_manager.send_cuia("SELECT") - return True # Event processed. restarts event loop """ - - - return False # event not processed by this driver. Zynthian queue has to process it further down the row - - def send_midi (self, ev): - chain = self.chain_manager.get_active_chain() - # print(chain.midi_chan) - # @todo: find out how to get 'last' active chain, for now: just back out. + elif data1 == 105: # Knob 19 in "Preset-Recall 10" + pot = data2 - self.zynpot_1 + self.zynpot_1 = data2 + self.state_manager.send_cuia("ZYNPOT", [1, pot]) + return True + + elif data1 == 85: # Knob 10 in "Preset-Recall 10" + pot = data2 - self.zynpot_2 + self.zynpot_2 = data2 + self.state_manager.send_cuia("ZYNPOT", [2, pot]) + return True + + elif data1 == 86: # Knob 11 in "Preset-Recall 10" + pot = data2 - self.zynpot_3 + self.zynpot_3 = data2 + self.state_manager.send_cuia("ZYNPOT", [3, pot]) + return True - if chain.midi_chan is None: + # Process program change events (buttons) + # Note: Using buttons on Keystation 88 Pro MK1 for "back" and "OK" is not ideal + # because all buttons send only a program change when pressed, with no way to + # detect long vs short presses + if len(ev) >= 2: + if ev[0] & 0xF0 == 0xC0: # Program Change event + data1 = ev[1] # Program number + + # Map program changes to UI actions + if data1 == 0: # Button "Back" + self.state_manager.send_control("BACK") + return True + + elif data1 == 1: # Button "OK" + self.state_manager.send_control("SELECT") + return True + + return False # Event not processed by this driver + + def send_midi(self, ev): + """Send MIDI event to active chain""" + chain = self.chain_manager.get_active_chain() + + if chain is None or chain.midi_chan is None: return False - status = (ev[0] & 0xF0) | chain.midi_chan - self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) - return True - - + status = (ev[0] & 0xF0) | chain.midi_chan + zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) + return True From 2a14b00be935437881060f485359e6649db500bc Mon Sep 17 00:00:00 2001 From: JBrumby Date: Mon, 15 Sep 2025 20:47:21 +0200 Subject: [PATCH 23/57] I don_t now --- .../zynthian_ctrldev_keystation_pro_88_mk1.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py index d7933cc58..04c9b0f86 100644 --- a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py @@ -117,18 +117,18 @@ def midi_event(self, ev): # Note: Using buttons on Keystation 88 Pro MK1 for "back" and "OK" is not ideal # because all buttons send only a program change when pressed, with no way to # detect long vs short presses - if len(ev) >= 2: - if ev[0] & 0xF0 == 0xC0: # Program Change event - data1 = ev[1] # Program number + # if len(ev) >= 2: + # if ev[0] & 0xF0 == 0xC0: # Program Change event + # data1 = ev[1] # Program number - # Map program changes to UI actions - if data1 == 0: # Button "Back" - self.state_manager.send_control("BACK") - return True + # # Map program changes to UI actions + # if data1 == 0: # Button "Back" + # self.state_manager.send_cuia("BACK") + # return True - elif data1 == 1: # Button "OK" - self.state_manager.send_control("SELECT") - return True + # elif data1 == 1: # Button "OK" + # self.state_manager.send_cuia("SELECT") + # return True return False # Event not processed by this driver From be148f042be9c0b78d1f8e1865aae321fc52a207 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Mon, 15 Sep 2025 20:53:57 +0200 Subject: [PATCH 24/57] fixe error in variable setup --- .../ctrldev/zynthian_ctrldev_base_scale.py | 191 ++++++++++-------- 1 file changed, 112 insertions(+), 79 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index 7d67c7096..ba9524320 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -1,4 +1,5 @@ #!/zynthian/venv/bin/python + import logging # Do not change. Only if this file is started directly from console @@ -49,92 +50,127 @@ class Harmony: # Class variables: Hardware of device and Scales don't change in instances modes = _MODES scales = _SCALES - - ### Next the instance variables, set up by init() function, are different for each instance - ### cols = 8 - ### rows = 8 - ### target_notes = [] - ### target_notes_reverse = {} # To get pads with same MIDI note to light them up when pressed - ### col_versatz = -5 - ### active_mode = "Major" - ### active_scale = 0 # means "C" - + def __init__(self, pad_cols, pad_rows): self.cols = pad_cols self.rows = pad_rows self.target_notes = [] # Instance variable self.target_notes_reverse = {} self.active_mode = None - self.col_versatz = 0 # 0 means linear, no offset + self.col_versatz = None # 0 means linear, no offset + self.middle_pad_nr = None + self.middle_c = None + self.must_redraw_led_colors = False + self._lock = 0 + def is_initialized(self): + if self.target_notes == []: return False + if self.target_notes_reverse == {}: return False + if type(self.active_mode) == None: return False + if type(self.col_versatz) == None: return False + if type(self.middle_pad_nr) == None: return False + if type(self.middle_c) == None: return False + return True - # def init_scale(self, tonic: int, mode_name: str, note_start: int, col_versatz: int): - # """tonic : tonic of scale as midinote 0-11 (semitones) - # mode_name: name of mode from self._modes - # note_start: number of the tone in scale with octaves (12 would be second octave tonic) - # col_versatz: each row can start with a different offset, so -5 means in C-Major-Scale an "F" above the C in row-1 line - # """ - # self.tonic = tonic - # self.col_versatz = col_versatz - # self.active_mode = mode_name - # self.pad1_midi_note = note_start # midi_note for pad1 - - # self.target_notes = [] # Reset for new scale - # self.target_notes_reverse = {} # Reset for new scale + def must_reset_led_colors(self) -> bool: + return self.must_redraw_led_colors - # for i in range(self.cols * self.rows): - # in_row = i // self.cols - # h = i + (in_row * col_versatz) - # if console_debug: print(f"{h} ", end="", flush=True) - # note_new = self._harmony_calculate_midi_note(h) - - # self.target_notes.append(note_new) # always as "C scale" - # # Reverse mapping - # self.target_notes_reverse.setdefault(note_new, []).append(i) # always as "C-scale" - - # if console_debug: - # print(f"({note_new}), ", end="\t", flush=True) - # if (i+1) % 8 == 0: # self.cols == 0: - # print("*", end="\n", flush=True) # Newline - # return - - - def init_scale(self, - tonic: int = 0, # (C) semitone distance counted from from C = 0 - mode_name: str = "Major", # mode as str from Array - col_versatz: int = -5, # per row recess - middle_c: int = 36, # must be middle_c % 12 = 60 - middle_pad_nr: int = 3): # padnr of middle tonic + tonic: int = None, # (C) semitone distance counted from from C = 0 + mode_name: str = None, # mode as str. look in _SCALES + col_versatz: int = None, # per row recess + middle_c: int = None, # must be middle_c % 12 = 60 + middle_pad_nr: int = None): # padnr of middle tonic. pad where middle_c is placed """tonic : tonic of scale as midinote 0-11 (semitones) mode_name: name of mode from self._modes middle_C: number of the tone in scale with octaves (12 would be second octave tonic) col_versatz: each row can start with a different offset, so -5 means in C-Major-Scale an "F" above the C in row-1 line """ + # for new tonic initialization is not necessary. Tonics just change returnvlaiues of notes + if not tonic is None: + if tonic > 11: tonic = 0; + if tonic < 0: tonic = len(self.scales)-1 + self.tonic = tonic + # Fallback for tonic + if self.tonic is None: + self.tonic = 0 # Set to C + logging.error("tonic not set. Fallback is 0 ='C'") - self.tonic = tonic - self.col_versatz = col_versatz - self.middle_c = middle_c - self.active_mode = mode_name + # any value afterwards will reinitialiue the class + is_dirty = False # reinitialize? + if not col_versatz is None: + if not self.col_versatz == col_versatz: + is_dirty = True + self.must_redraw_led_colors = True + self.col_versatz = col_versatz + # col_versatz not intialiued + if self.col_versatz is None: + is_dirty = True + self.must_redraw_led_colors = True + self.col_versatz = -5 # upwards 1 fourth lower the scale -> in C-Major an F above the C and so on + logging.error("row recess not set. Fallback vlaue is -5") + + + if not middle_c is None: + if not self.middle_c == middle_c: + is_dirty = True + self.must_reset_led_colors = True + middle_c = middle_c // 12 * 12 # makes middle_c % 12 == 0 + self.middle_c = middle_c + if self.middle_c is None: + self.middle_c = 48 # must be middle_c % 12 = 0 + is_dirty = True + logging.error("middle_C not set. Will be set to Midi_note=48") + + if not mode_name is None: + if not mode_name in self.modes: + logging.error(f"modename: {mode_name}") + else: + if not self.active_mode == mode_name: + # if len of new mode is different to before, LED Colors must be rewritten + self.must_redraw_led_colors = True + self.active_mode = mode_name + is_dirty = True + if self.active_mode is None: + self.active_mode = "Major" + self.must_redraw_led_colors = True + is_dirty = True + logging.error("mode not set. Falback is: Major") + + if not middle_pad_nr is None: + if middle_pad_nr < 0 : middle_pad_nr = 0 # center of scale is pad1 + if middle_pad_nr >= self.cols * self.rows: + middle_pad_nr = self.cols * self.rows -1 # center of scale is last pad + if not self.middle_pad_nr == middle_pad_nr: + self.middle_pad_nr = middle_pad_nr + self.must_redraw_led_colors = True + is_dirty = True + if self.middle_pad_nr is None: + self.middle_pad_nr = 4 + self.must_redraw_led_colors = True + is_dirty == True + + + # if not is_dirty: return # if just tonica changed go back self.target_notes = [] # Reset for new scale self.target_notes_reverse = {} # Reset for new scale - - if middle_pad_nr < 0 : middle_pad_nr = 0 - if middle_pad_nr >= self.cols * self.rows: - middle_pad_nr = self.cols * self.rows -1 - + + # if middle_pad_nr < 0 : middle_pad_nr = 0 + # if middle_pad_nr >= self.cols * self.rows: + # middle_pad_nr = self.cols * self.rows -1 + mode = self.modes[self.active_mode] pad_counter = -1 - for i in range (-middle_pad_nr, (self.cols*self.rows) - middle_pad_nr): + for i in range (- self.middle_pad_nr, (self.cols*self.rows) - self.middle_pad_nr): pad_counter += 1 row_nr = pad_counter // self.cols - note_nr_in_scale = i + (row_nr * col_versatz) + note_nr_in_scale = i + (row_nr * self.col_versatz) octave = note_nr_in_scale // len(mode) @@ -157,29 +193,25 @@ def init_scale(self, print("*", end="\n", flush=True) # Newline at end of row return + def set_new_tonic(self, new_tonic:int): + if new_tonic == self.tonic: return False + if new_tonic < 0: new_tonic = 11 # target: B + if new_tonic > 11: new_tonic = 0 # target: C + self.tonic = new_tonic + return True # yes update display. we changed it + def step_to_next_tonic(self, step): + if step > 63: step -=128 # for controller sending 127 for -1 + new_tonic = self.scales.tonic + step + if new_tonic < 0: new_tonic = 11 # target: B + if new_tonic > 11: new_tonic = 0 # target: C + self.scales.tonic = new_tonic - # def _harmony_calculate_midi_note(self, note_in_scale) -> int: - # """Parameters: - # scale: string with name of scale - # note: integer representing the starting point in the scale. If start is bigger than - # the length of the specified scale it adds start % len(scale) * 12 to the result. - # So you can cycle through the number of keyboard keys to get their MIDI notes - # """ - # try: - # mode = self.modes[self.active_mode] - # pos_in_mode = note_in_scale % len(mode) # - # octave = note_in_scale // len(mode) - # return mode[pos_in_mode] + (octave * 12) # is based "C-Scale" - # except KeyError: - # logging.error(f"Error: Mode '{self.active_mode}' not found!") - # return -1 # -1 is error, there is no MIDI note -1 - # except Exception as e: - # logging.error(f"Error calculating MIDI note: {e}") - # return -1 - - def is_tonic_by_padnr(self, padnr:int)-> bool: - return self.target_notes[padnr] % 12 == 0 + def is_tonic_by_padnr(self, pad_nr:int)-> bool: + res = self.target_notes[pad_nr] + res2 = res % 12 + return res2 == 0 + #return self.target_notes[padnr] % 12 == 0 def is_tonic_by_midnote(self, midi_note:int) -> bool: @@ -207,6 +239,7 @@ def harmony_get_mode_names(self): def harmony_get_scale_name_with_mode (self) -> str: result = self.scales[self.tonic] + ' ' + self.active_mode + result = result.ljust(20)[:20] return result def harmony_get_target_note(self, pad_nr: int) -> int: From 234c374ffd80eb9686dc6e0d04fa4f1b33b6c07f Mon Sep 17 00:00:00 2001 From: JBrumby Date: Mon, 15 Sep 2025 20:54:54 +0200 Subject: [PATCH 25/57] added display and button functions for scale modus --- .../zynthian_ctrldev_ableton_push_1.py | 267 ++++++++++++++---- 1 file changed, 206 insertions(+), 61 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 3665da049..c29c19d4c 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -93,7 +93,7 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ # evtype = (ev[0] >> 4) & 0x0F -> EV_NOTE_OFF = 0x8 # 3 Bytes EV_NOTE_ON = 0X9 # 3 Bytes - EV_AFTERTOUSCH = 0xA # 3 Bytes (polyphonic = per note) + EV_AFTERTOUCH = 0xA # 3 Bytes (polyphonic = per note) EV_CC = 0xB # 3 Bytes EV_PC = 0xC # 2 Bytes EV_CHAN_PRESS = 0xD # 2 Bytes @@ -105,7 +105,12 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ device_mode_active = DEV_MODE_SCALES scales = Harmony(8,8) - scales.init_scale(tonic=0, middle_c=48) # (0, "Major", 36-1, -5) # -3 = new start per row + scales.init_scale(tonic=0, + mode_name="Major", + col_versatz=-5, + middle_c=48, + middle_pad_nr=4) + # scales.init_scale(tonic=0, middle_c=48) # (0, "Major", 36-1, -5) # -3 = new start per row # Function to initialise class @@ -154,7 +159,7 @@ def init(self): lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) # Bicolor LEDs dim ## CC20-27 + 102-109 - for t in [21, 23]: + for t in [ABL.BTN_R1_C1[1], ABL.BTN_R1_C2[1], ABL.BTN_R1_C3[1], ABL.BTN_R1_C4[1]]: lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_ORANGE_DIM) ### lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) @@ -182,38 +187,78 @@ def end(self): ### Disable session mode on launchkey ## lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 0) # device, channel, note, velocity + def scale_update_leds(self, index_activated): # index defines blinkin LED + # Bicolor LEDs dim ## CC20-27 + 102-109 + scale_buttons = [ + ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], + ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], + ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] + ] + for t in scale_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_GREEN_DIM) + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, scale_buttons[index_activated], ABL.BI_GREEN_DIM_BLINK) + + + # new in this class, to setup scales_mode = keyboard mode def set_dev_to_scales_mode(self): self.device_mode_active = self.DEV_MODE_SCALES # visual feedback, let Scales Button blink lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) self.pads_off() # akk pad leds off - self.set_dev_scale_color() # set LEDs for scale mode + self.scales_set_pad_colors() # set LEDs for scale mode self._display.clear() scale_n_mode = self.scales.harmony_get_scale_name_with_mode() - self._display.write_xy_mem(scale_n_mode, 0, 3) + self._display.write_xy_mem(scale_n_mode, 0, 2) + # Btn_row + btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | Scale | Mode |" + btn_txt_row2 = "|modes here | | || G# | A | A# | B |" + btn_txt_row3 = "| C | C# | D | D# || E | F | F# | G |" + self._display.write_xy_mem(btn_txt_row0, 0, 0) + self._display.write_xy_mem(btn_txt_row2, 0, 2) + self._display.write_xy_mem(scale_n_mode, 0, 2) # Scale and scale over row2 + self._display.write_xy_mem(btn_txt_row3, 0, 3) self._display.update_screen() + # set Display-Button_LEDS + self.scale_update_leds(self.scales.tonic) # 0 is 'C' + - - def set_tonic(self, step): + def scales_set_tonic(self, step): if step > 63: step -=128 - self.scales.tonic = self.scales.tonic + step - if self.scales.tonic < 0: self.scales.tonic = 11 # target: B - if self.scales.tonic > 11: self.scales.tonic = 0 # target: C - self.set_dev_to_scales_mode(); - self.scales.init_scale(self.scales.tonic, self.scales.active_mode) + self.steps_tonic = getattr(self, 'steps_tonic', 0) + step + if not abs(self.steps_tonic) > 10: return # slow down. each 10th step + self.steps_tonic = 0; + new_tonic = self.scales.tonic + step + if new_tonic < 0: new_tonic = 11 # target: B + if new_tonic > 11: new_tonic = 0 # target: C + self.scales.tonic = new_tonic + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + self._display.write_xy_mem(scale_n_mode, 0, 2) + self._display.update_screen() + self.scale_update_leds(new_tonic) + return + + def scales_set_mode(self, step): + if step > 63: step -=128 # 127 = -1 - def set_mode(self, step): + # lower knob speed by 10 + self.steps_mode = getattr(self, 'steps_mode', 0) + step + if not abs(self.steps_mode) > 10: return # slow down. each 10th steop + self.steps_mode = 0; + + # all mode names modenames = self.scales.harmony_get_mode_names() nr_of_modes = len(modenames) result = None - if not self.scales.active_mode: self.scales.active_mode = modenames[0] - if step > 63: step -=128 + + # if not mode is set, get first name + if not self.scales.active_mode: self.scales.active_mode = modenames[0] # "Chromatic" + for i in range(nr_of_modes): if modenames[i] == self.scales.active_mode: result = i break - if not result == None: + if not result is None: result += step if result >= nr_of_modes: result = 0 elif result < 0 : result = nr_of_modes-1 @@ -224,7 +269,9 @@ def set_mode(self, step): logging.error("Bug in set_mode") # do the magic self.set_dev_to_scales_mode(); - self.scales.init_scale(self.scales.tonic, self.scales.active_mode) # self.scales.tonic, self.scales.active_mode, 36-1, -5) + self.scales.init_scale(self.scales.tonic, self.scales.active_mode) + self.scales_set_pad_colors() + # self.scales.tonic, self.scales.active_mode, 36-1, -5) ### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. ### just copy the derived functions in the this driver and implement them accordingly @@ -359,9 +406,15 @@ def midi_event(self, ev): val_or_vel = ev[2] & 0x7F if note_or_register: # len > 1 -> Button / Pad detection is possible - btn_name = self.button_name_from_midi_event(ev) # ev[0] and ev[1] fields are proved. so any status can be a button + button_ev = ev + aftertouch = '' + if button_ev[1] == 0xa0: # EV_AFTERTOUCH: + button_ev == [0x90, ev[1]] + aftertouch = 'aftertouch' + + btn_name = self.button_name_from_midi_event(button_ev) # ev[0] and ev[1] fields are proved. so any status can be a button - logging.debug(f"Button: {btn_name} on chan. {chan_or_instruction} gives midi_event: {hex(ev[0])} {hex(ev[1])} {hex(val_or_vel)} = {evtype}, {note_or_register} {val_or_vel}") + logging.debug(f"Button: {btn_name} on chan. {chan_or_instruction} gives midi_event: {aftertouch} {hex(ev[0])} {hex(ev[1])} {hex(val_or_vel)} = {evtype}, {note_or_register} {val_or_vel}") ### End of debugging purposes. @@ -373,29 +426,11 @@ def midi_event(self, ev): evtype = (ev[0] >> 4) & 0x0F note = ev[1] & 0x7F # is that need? any event field is from 0-127 except the status field - ### keyboard mode + ### Scale mode if self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected - # Filter out note events created by push 1 when touching Knobs and Ribbon - if ABL_PAD_START <= note <= ABL_PAD_END: # just note events, which should sound. - # filter for getting any vent that is sound event - if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUSCH, self.EV_PITCHBEND]: - - # logging.debug(f"Scales mode -BRUMBY") - pad_nr = note -35# translat note from event to pad_nr - - # here magic for different sccale layouts happens. - # it translates midi_note events to the translated note_events - note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes are based 0 pad_nr based 1 - vel = ev[2] - if evtype == self.EV_NOTE_ON: vel = ev[2] *2 # this creates junk with aftertouch und pitchbend.. - if vel > 255: vel = 255 - new_ev = bytes([ev[0], note_translated, vel]) - #if note_translated % 12 == 0: # Oktave detected - # pass - # for note_on events following. - - self._forward_like_niels_did(new_ev) # - return True # return to caller and mark event as processed + if self.process_scale_event(ev): + return True # return value cuts out follwing + # pad mode to control sequencer elif self.device_mode_active == self.DEV_MODE_PAD: @@ -415,7 +450,7 @@ def midi_event(self, ev): # I think we dont need any note events... so filter out # NO note events anymore after this two lines. Comment out what you need after this lines in "Pad Mode" = DEV_MODE_PAD - if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUSCH, self.EV_PITCHBEND]: + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH, self.EV_PITCHBEND]: return True # no return call. I don't know if I need note_on_events further down. @@ -489,6 +524,89 @@ def midi_event(self, ev): # default return, when no match return False # When nothing matches, False shows that midi event has to be processed further + def process_scale_event(self, ev) -> bool: + if not self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected + return False # we are not in scales mode + + # event part for sounds + # Filter out note events created by push 1 when touching Knobs and Ribbon + note = ev[1] + if ABL_PAD_START <= note <= ABL_PAD_END: # just note events, which should sound. + # filter for getting any vent that is sound event + + evtype = (ev[0] >> 4) & 0x0F + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH, self.EV_PITCHBEND]: + + # logging.debug(f"Scales mode -BRUMBY") + pad_nr = note -35# translates input note to hardware pad_nr + + # here magic for different sccale layouts happens. + # it translates midi_note events to the translated note_events + note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes are based 0 pad_nr based 1 + vel = ev[2] # my push1 is insensitive so I double any velocity val + if evtype == self.EV_NOTE_ON: + vel = ev[2] *2 # this creates junk with aftertouch und pitchbend.. + if vel > 255: vel = 255 + new_ev = bytes([ev[0], note_translated, vel]) + #if note_translated % 12 == 0: # Oktave detected + # pass + # for note_on events following. + + self._forward_like_niels_did(new_ev) # + return True # return to caller and mark event as processed + + # here any other ebent + # self.EV_CC, self.EV_CHAN_PRESS, self.EV_SEXSTEM, self.EV_PC + # and ALL events from Pads < PAD_START and Pads > PAD_END + + # we want to process display buttons: + ## helper for display buttons + def helper_set_new_tonic(tonic): + if self.scales.set_new_tonic(tonic): + # yes it changed. update display + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + self._display.write_xy_mem(scale_n_mode, 0, 2) + self._display.update_screen() + self.scale_update_leds(tonic) + ### + return True + + ### processing starts here + search_key = [ev[0], ev[1]] + if ev[2] > 0: # just btn down eventes + match search_key: + + case ABL.BTN_R2_C1: + helper_set_new_tonic(0); return True + case ABL.BTN_R2_C2: + helper_set_new_tonic(1); return True + case ABL.BTN_R2_C3: + helper_set_new_tonic(2); return True + case ABL.BTN_R2_C4: + helper_set_new_tonic(3); return True + case ABL.BTN_R2_C5: + helper_set_new_tonic(4); return True + case ABL.BTN_R2_C6: + helper_set_new_tonic(5); return True + case ABL.BTN_R2_C7: + helper_set_new_tonic(6); return True + case ABL.BTN_R2_C8: + helper_set_new_tonic(7); return True + case ABL.BTN_R1_C5: + helper_set_new_tonic(8); return True + case ABL.BTN_R1_C6: + helper_set_new_tonic(9); return True + case ABL.BTN_R1_C7: + helper_set_new_tonic(10); return True + case ABL.BTN_R1_C8: + helper_set_new_tonic(11); return True + + + case _: + print ("Button not defined in process_scale_event") + logging.debug (f"Button {search_key}not defined in process_scale_event") + return False + # to clean up the code GUI events are processed here def process_gui_events(self,ev) -> bool: @@ -497,10 +615,10 @@ def process_gui_events(self,ev) -> bool: ccval = ev[2] & 0x7F if ABL.KNOB_7[1] == ccnum: # scale - self.set_tonic(ccval) + self.scales_set_tonic(ccval) elif ABL.KNOB_8[1] == ccnum: # mode - self.set_mode(ccval) + self.scales_set_mode(ccval) # Zynpoties Werte an GUI # Potis Oben 72 - 75 die ersten 4 @@ -519,12 +637,30 @@ def process_gui_events(self,ev) -> bool: logging.debug("ABL_OK BRUMBY") self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) return True - + + elif ccnum == ABL.BTN_R1_C1[1]: # Zweiter Button unter dem Display + logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [0,"S"]) + return True # elif ccnum == 21: Does that work? elif ccnum == ABL.BTN_R1_C2[1]: # Zweiter Button unter dem Display logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) return True + elif ccnum == ABL.BTN_R1_C5[1]: # Zweiter Button unter dem Display + logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [2,"S"]) + return True + # elif ccnum == 21: Does that work? + elif ccnum == ABL.BTN_R1_C4[1]: # Zweiter Button unter dem Display + logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) + return True + + + + + elif ccnum == ABL.BTN_ESC[1]: # logging.debug("BTN_ESC BRUMBY") @@ -606,16 +742,17 @@ def button_name_from_midi_event(self, ev): ###, button_event): # button_event is # logging.debug(f"midi_event {ev} from Button not defined with value: {data}") return "" - - def set_dev_scale_color(self): + def scales_set_pad_colors(self): + # def set_dev_scale_color(self): self._leds_rgb.all_off(True) # led_states must not be deleted. is done in next lines for pad_nr in range(64): new_note = self.scales.harmony_get_target_note(pad_nr) - if self.scales.is_tonic_by_midnote(new_note): - print (f"found: Tonic {new_note}") + # if self.scales.is_tonic_by_midnote(new_note): ### NOT WORKING CONPLETELY + if self.scales.is_tonic_by_padnr(pad_nr): r = 0; g = 0; b = 255 else: r = 200; g = 200; b = 200 + print (f"found: Tonic {new_note}") # self.set_pad_rgb(pad_nr, r, g, b) ## OLD FUnction self._leds_rgb.set_rgb(pad_nr, r, g, b, overlay=False) pass @@ -708,12 +845,15 @@ def send_sysex(self, data): class Feedback_Display: display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten + # _disp_line_dirty =[False, False, False, False] + def __init__ (self, idev_out): # self.dbg = True self.idev_out = idev_out + self._disp_line_dirty =[False, False, False, False] # if self.dbg: - logging.error(f"BRUMBY: Class Display instantiiert") + #logging.error(f"BRUMBY: Class Display instantiiert") @@ -731,22 +871,25 @@ def clear (self): s3 = bytes([240,71,127,21,31,0,0,247]) # Zeile 3 for x in [s0, s1, s2, s3]: lib_zyncore.dev_send_midi_event(self.idev_out, x, len(x)) - sleep(0.05) + sleep(0.01) + self._disp_line_dirty =[False, False, False, False] def update_screen (self): # move display memory to display with sysex for row in range(4): - #msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) - text = bytes(self.display_mem[row]) - text_len = len(text) - col = 0 - # here the magic happens and sysex is cunstructed - # 240, 71, 127, 21, <24+line(0-3)>,0, ,,, 247 - msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) - # logging.error(f"BRUMBY: Display.update SYSEX={msg}") - lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - sleep(0.05) + if self._disp_line_dirty[row]: + #msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) + text = bytes(self.display_mem[row]) + text_len = len(text) + col = 0 + # here the magic happens and sysex is cunstructed + # 240, 71, 127, 21, <24+line(0-3)>,0, ,,, 247 + msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) + # logging.error(f"BRUMBY: Display.update SYSEX={msg}") + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + sleep(0.01) + self._disp_line_dirty[row] = False return @@ -777,6 +920,7 @@ def write_xy_mem (self, text, col_in:int, row_in:int): text_len = len(text) self.display_mem[row_in][col_in:col_in+text_len] = list(text) + self._disp_line_dirty[row_in] = True # self.update() return @@ -967,3 +1111,4 @@ def set_rgb(self, pad_nr: int, r:int ,g:int ,b:int, overlay=False): # lib_zyncore.dev lib_zyncore.dev_send_midi_event(self._idev, sysex, len(sysex)) + From 312ba07570f37635fa8d6bf1ec4e045b8e4de724 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Mon, 15 Sep 2025 22:39:28 +0200 Subject: [PATCH 26/57] scale and mode knogs moved to scale_event. Ribbon now working as pitch wheel --- .../zynthian_ctrldev_ableton_push_1.py | 257 +++++++++++------- 1 file changed, 155 insertions(+), 102 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index c29c19d4c..98630d56f 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -187,6 +187,10 @@ def end(self): ### Disable session mode on launchkey ## lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 0) # device, channel, note, velocity + +################################################################################################################# +################## START of scales fucntions ########################################################## + def scale_update_leds(self, index_activated): # index defines blinkin LED # Bicolor LEDs dim ## CC20-27 + 102-109 scale_buttons = [ @@ -198,7 +202,26 @@ def scale_update_leds(self, index_activated): # index defines blinkin LED lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_GREEN_DIM) lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, scale_buttons[index_activated], ABL.BI_GREEN_DIM_BLINK) + def scales_cleanup(self): # set of any LED and display changes + # cleadup display + btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | | |" + btn_txt_row2 = "| | | | || | | | |" + self._display.write_xy_mem(btn_txt_row0, 0, 0) + self._display.write_xy_mem(btn_txt_row2, 0, 1) + self._display.write_xy_mem(btn_txt_row2, 0, 2) + self._display.write_xy_mem(btn_txt_row2, 0, 3) + self._display.update_screen() + # cleanup scale LED + scale_buttons = [ + ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], + ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], + ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] + ] + for t in scale_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_LED_OFF) + + # def scales_setup(self) # new in this class, to setup scales_mode = keyboard mode def set_dev_to_scales_mode(self): @@ -212,9 +235,11 @@ def set_dev_to_scales_mode(self): self._display.write_xy_mem(scale_n_mode, 0, 2) # Btn_row btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | Scale | Mode |" + btn_txt_row1 = "| | | | || | | | |" btn_txt_row2 = "|modes here | | || G# | A | A# | B |" btn_txt_row3 = "| C | C# | D | D# || E | F | F# | G |" self._display.write_xy_mem(btn_txt_row0, 0, 0) + self._display.write_xy_mem(btn_txt_row1, 0, 1) self._display.write_xy_mem(btn_txt_row2, 0, 2) self._display.write_xy_mem(scale_n_mode, 0, 2) # Scale and scale over row2 self._display.write_xy_mem(btn_txt_row3, 0, 3) @@ -268,10 +293,132 @@ def scales_set_mode(self, step): else: logging.error("Bug in set_mode") # do the magic - self.set_dev_to_scales_mode(); + # self.set_dev_to_scales_mode(); ### Das ist overkill self.scales.init_scale(self.scales.tonic, self.scales.active_mode) + # colorize pad array with tonic self.scales_set_pad_colors() + # Display + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + self._display.write_xy_mem(scale_n_mode, 0, 2) + self._display.update_screen() # self.scales.tonic, self.scales.active_mode, 36-1, -5) + + def scales_set_pad_colors(self): + # def set_dev_scale_color(self): + # self._leds_rgb.all_off(True) # led_states must not be deleted. is done in next lines. just for debugging + for pad_nr in range(64): + new_note = self.scales.harmony_get_target_note(pad_nr) + # if self.scales.is_tonic_by_midnote(new_note): ### NOT WORKING CONPLETELY + if self.scales.is_tonic_by_padnr(pad_nr): + r = 0; g = 0; b = 255 + else: + r = 200; g = 200; b = 200 + print (f"found: Tonic {new_note}") + # self.set_pad_rgb(pad_nr, r, g, b) ## OLD FUnction + self._leds_rgb.set_rgb(pad_nr, r, g, b, overlay=False) + pass + + def process_scale_event(self, ev) -> bool: + if not self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected + return False # we are not in scales mode + + ##### event part for sounds + # Filter out note events created by push 1 when touching Knobs and Ribbon + note = ev[1] + if ABL_PAD_START <= note <= ABL_PAD_END: # just note events, which should sound. + # filter for getting any vent that is sound event + + + evtype = (ev[0] >> 4) & 0x0F + + if evtype == self.EV_PITCHBEND: # ribbon working as pitchwheel + self._forward_like_niels_did(ev) + + + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH]: + + # logging.debug(f"Scales mode -BRUMBY") + pad_nr = note -35# translates input note to hardware pad_nr + + # here magic for different sccale layouts happens. + # it translates midi_note events to the translated note_events + note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes are based 0 pad_nr based 1 + vel = ev[2] # my push1 is insensitive so I double any velocity val + if evtype == self.EV_NOTE_ON: + vel = ev[2] *2 # this creates junk with aftertouch und pitchbend.. + if vel > 255: vel = 255 + new_ev = bytes([ev[0], note_translated, vel]) + #if note_translated % 12 == 0: # Oktave detected + # pass + # for note_on events following. + + self._forward_like_niels_did(new_ev) # + return True # return to caller and mark event as processed + + # here any other ebent + # self.EV_CC, self.EV_CHAN_PRESS, self.EV_SEXSTEM, self.EV_PC + # and ALL events from Pads < PAD_START and Pads > PAD_END + + # we want to process display buttons: + ## helper for display buttons + def helper_set_new_tonic(tonic): + if self.scales.set_new_tonic(tonic): + # yes it changed. update display + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + self._display.write_xy_mem(scale_n_mode, 0, 2) + self._display.update_screen() + self.scale_update_leds(tonic) + ### + return True + + ### processing starts here + search_key = [ev[0], ev[1]] # build search key from event + if ev[2] > 0: # just btn down eventes + match search_key: + case ABL.BTN_R2_C1: + helper_set_new_tonic(0); return True + case ABL.BTN_R2_C2: + helper_set_new_tonic(1); return True + case ABL.BTN_R2_C3: + helper_set_new_tonic(2); return True + case ABL.BTN_R2_C4: + helper_set_new_tonic(3); return True + case ABL.BTN_R2_C5: + helper_set_new_tonic(4); return True + case ABL.BTN_R2_C6: + helper_set_new_tonic(5); return True + case ABL.BTN_R2_C7: + helper_set_new_tonic(6); return True + case ABL.BTN_R2_C8: + helper_set_new_tonic(7); return True + case ABL.BTN_R1_C5: + helper_set_new_tonic(8); return True + case ABL.BTN_R1_C6: + helper_set_new_tonic(9); return True + case ABL.BTN_R1_C7: + helper_set_new_tonic(10); return True + case ABL.BTN_R1_C8: + helper_set_new_tonic(11); return True + + # Display Knobs here + # knobs + case ABL.KNOB_7: # scale + self.scales_set_tonic(ev[2]); return True + case ABL.KNOB_8: # mode + self.scales_set_mode(ev[2]); return True + + case _: + pass + #print ("Button not defined in process_scale_event") + #logging.debug (f"Button {search_key}not defined in process_scale_event") + + + return False + + +################## END of scales fucntions ########################################################## +############################################################################################################### + ### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. ### just copy the derived functions in the this driver and implement them accordingly @@ -503,6 +650,7 @@ def midi_event(self, ev): self.set_dev_to_scales_mode() else: self.device_mode_active = self.DEV_MODE_PAD + self.scales_cleanup() # visual feedback, set LED to solid on lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) self.pads_off() # clean up visible state. all pad leds off @@ -524,88 +672,6 @@ def midi_event(self, ev): # default return, when no match return False # When nothing matches, False shows that midi event has to be processed further - def process_scale_event(self, ev) -> bool: - if not self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected - return False # we are not in scales mode - - # event part for sounds - # Filter out note events created by push 1 when touching Knobs and Ribbon - note = ev[1] - if ABL_PAD_START <= note <= ABL_PAD_END: # just note events, which should sound. - # filter for getting any vent that is sound event - - evtype = (ev[0] >> 4) & 0x0F - if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH, self.EV_PITCHBEND]: - - # logging.debug(f"Scales mode -BRUMBY") - pad_nr = note -35# translates input note to hardware pad_nr - - # here magic for different sccale layouts happens. - # it translates midi_note events to the translated note_events - note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes are based 0 pad_nr based 1 - vel = ev[2] # my push1 is insensitive so I double any velocity val - if evtype == self.EV_NOTE_ON: - vel = ev[2] *2 # this creates junk with aftertouch und pitchbend.. - if vel > 255: vel = 255 - new_ev = bytes([ev[0], note_translated, vel]) - #if note_translated % 12 == 0: # Oktave detected - # pass - # for note_on events following. - - self._forward_like_niels_did(new_ev) # - return True # return to caller and mark event as processed - - # here any other ebent - # self.EV_CC, self.EV_CHAN_PRESS, self.EV_SEXSTEM, self.EV_PC - # and ALL events from Pads < PAD_START and Pads > PAD_END - - # we want to process display buttons: - ## helper for display buttons - def helper_set_new_tonic(tonic): - if self.scales.set_new_tonic(tonic): - # yes it changed. update display - scale_n_mode = self.scales.harmony_get_scale_name_with_mode() - self._display.write_xy_mem(scale_n_mode, 0, 2) - self._display.update_screen() - self.scale_update_leds(tonic) - ### - return True - - ### processing starts here - search_key = [ev[0], ev[1]] - if ev[2] > 0: # just btn down eventes - match search_key: - - case ABL.BTN_R2_C1: - helper_set_new_tonic(0); return True - case ABL.BTN_R2_C2: - helper_set_new_tonic(1); return True - case ABL.BTN_R2_C3: - helper_set_new_tonic(2); return True - case ABL.BTN_R2_C4: - helper_set_new_tonic(3); return True - case ABL.BTN_R2_C5: - helper_set_new_tonic(4); return True - case ABL.BTN_R2_C6: - helper_set_new_tonic(5); return True - case ABL.BTN_R2_C7: - helper_set_new_tonic(6); return True - case ABL.BTN_R2_C8: - helper_set_new_tonic(7); return True - case ABL.BTN_R1_C5: - helper_set_new_tonic(8); return True - case ABL.BTN_R1_C6: - helper_set_new_tonic(9); return True - case ABL.BTN_R1_C7: - helper_set_new_tonic(10); return True - case ABL.BTN_R1_C8: - helper_set_new_tonic(11); return True - - - case _: - print ("Button not defined in process_scale_event") - logging.debug (f"Button {search_key}not defined in process_scale_event") - return False # to clean up the code GUI events are processed here def process_gui_events(self,ev) -> bool: @@ -614,16 +680,17 @@ def process_gui_events(self,ev) -> bool: ccnum = ev[1] & 0x7F ccval = ev[2] & 0x7F - if ABL.KNOB_7[1] == ccnum: # scale - self.scales_set_tonic(ccval) + # no more neede moved to scales_event... + # if ABL.KNOB_7[1] == ccnum: # scale + # self.scales_set_tonic(ccval) - elif ABL.KNOB_8[1] == ccnum: # mode - self.scales_set_mode(ccval) + # elif ABL.KNOB_8[1] == ccnum: # mode + # self.scales_set_mode(ccval) # Zynpoties Werte an GUI # Potis Oben 72 - 75 die ersten 4 # if 70 < ccnum < 80: - elif ABL.KNOB_1[1] <= ccnum <= ABL.KNOB_4[1]: + if ABL.KNOB_1[1] <= ccnum <= ABL.KNOB_4[1]: # self.state_manager.send_cuia("ZYNPOT_ABS", [ccnum - 72, ccval/127]) val = ccval if val > 68: @@ -742,20 +809,6 @@ def button_name_from_midi_event(self, ev): ###, button_event): # button_event is # logging.debug(f"midi_event {ev} from Button not defined with value: {data}") return "" - def scales_set_pad_colors(self): - # def set_dev_scale_color(self): - self._leds_rgb.all_off(True) # led_states must not be deleted. is done in next lines - for pad_nr in range(64): - new_note = self.scales.harmony_get_target_note(pad_nr) - # if self.scales.is_tonic_by_midnote(new_note): ### NOT WORKING CONPLETELY - if self.scales.is_tonic_by_padnr(pad_nr): - r = 0; g = 0; b = 255 - else: - r = 200; g = 200; b = 200 - print (f"found: Tonic {new_note}") - # self.set_pad_rgb(pad_nr, r, g, b) ## OLD FUnction - self._leds_rgb.set_rgb(pad_nr, r, g, b, overlay=False) - pass def set_pad_rgb(self, pad_nr: int, r:int ,g:int ,b:int): logging.error(" set_pad_rb aufgerufen.") From d9d4025a517f29527b74d431fb07436099a4d5a1 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Tue, 16 Sep 2025 03:20:38 +0200 Subject: [PATCH 27/57] no again working. self.zynseq in __init__ --- .../zynthian_ctrldev_ableton_push_1.py | 30 +++++++++++-------- .../zynthian_ctrldev_keystation_pro_88_mk1.py | 12 +++++++- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 98630d56f..ddf006ee9 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -1,6 +1,8 @@ #! /zynthian/venv/bin/python # -*- coding: utf-8 -*- +# TODO: DIsplay rowas are of different type. +# Row two seams to be monochrome green, just brightnes # ****************************************************************************** @@ -8,7 +10,7 @@ # # Zynthian Control Device Driver for "Ableton Push 1" # -# Copyright (C) 2025 Julius Brumby +# Copyright (C) 2025 Brumby # # ****************************************************************************** # @@ -68,7 +70,7 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer): - logging.info("Klassenaufruf - Ableton Push 1") + # logging.info("Class call") # Im Weblog wird angezeigt, dass der Treiber geladen wurde # dev_ids = ["Ableton Push IN 2", "Ableton Push IN 1"] # get by stepping through zynthian_ctrldev_manager.load_driver() @@ -80,9 +82,10 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ # Folgende Farben sind wohl die Sequencer Farben?? # siehe: https://pushmod.blogspot.com/p/pad-color-table.html # ORIGINAL PAD_COLOURS = [71, 104, 76, 51, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] - PAD_COLOURS = [61, 36, 63, 54, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] - STARTING_COLOUR = 123 - STOPPING_COLOUR = 120 + PAD_COLOURS = [61, 36, 63, 54, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] # not running + STARTING_COLOUR = 3 # WHITE + STOPPING_COLOUR = 120 # RED + RUNNING_COLOR = 123 # GREEM # dev_modes DEV_MODE_NONE = None @@ -109,12 +112,9 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ mode_name="Major", col_versatz=-5, middle_c=48, - middle_pad_nr=4) - # scales.init_scale(tonic=0, middle_c=48) # (0, "Major", 36-1, -5) # -3 = new start per row - + middle_pad_nr=4) # Function to initialise class - # called from parent (instance) def __init__(self, state_manager, idev_in, idev_out=None): logging.info("Created Instance from Ableton Push 1 driver - BRUMBY") @@ -311,9 +311,9 @@ def scales_set_pad_colors(self): # if self.scales.is_tonic_by_midnote(new_note): ### NOT WORKING CONPLETELY if self.scales.is_tonic_by_padnr(pad_nr): r = 0; g = 0; b = 255 + # print (f"found: Tonic {new_note}") else: r = 200; g = 200; b = 200 - print (f"found: Tonic {new_note}") # self.set_pad_rgb(pad_nr, r, g, b) ## OLD FUnction self._leds_rgb.set_rgb(pad_nr, r, g, b, overlay=False) pass @@ -443,7 +443,10 @@ def update_mixer_strip(self, chan, symbol, value): ### END of Mixer functions. -### Start of SEQUENCER FUNCTIONS + + +############################################################################################################## +################ Start of SEQUENCER FUNCTIONS ######################################################### # this function is called by zynseq when a sequencer state is changed # we have update pad LED to show state def update_seq_state(self, bank, seq, state, mode, group): @@ -474,7 +477,7 @@ def update_seq_state(self, bank, seq, state, mode, group): vel = self.PAD_COLOURS[group] elif state == zynseq.SEQ_PLAYING: chan = 2 - vel = self.PAD_COLOURS[group] + vel = self.RUNNING_COLOR elif state in [zynseq.SEQ_STOPPING, zynseq.SEQ_STOPPINGSYNC]: chan = 1 vel = self.STOPPING_COLOUR @@ -507,7 +510,8 @@ def pad_off(self, col, row): # logging.info(f"BRUMBY: row={row}; col={col} pad-note={note}") lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0) -### End of derived Sequencer Functions. +############### End of derived Sequencer Functions. ##################### +################################################################################################### # Just for me a helper function to set all pads off def pads_off(self): diff --git a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py index 04c9b0f86..23284f627 100644 --- a/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py @@ -53,6 +53,12 @@ class zynthian_ctrldev_keystation_pro_88_mk1(zynthian_ctrldev_base): EV_PITCHBEND = 0xE # 3 bytes: ev[1] = LSB 0-127; ev[2] = MSB 0-127 EV_SYSTEM = 0xF # System type = ev[0] & 0x0F + + def __init__(self, state_manager, idev_in, idev_out=None): + self.zynseq = state_manager.zynseq # we need to send midi events to zynthina + super().__init__(state_manager, idev_in, idev_out) + return + def midi_event(self, ev): """MIDI event handler for Keystation Pro 88""" evtype = (ev[0] >> 4) & 0x0F @@ -140,5 +146,9 @@ def send_midi(self, ev): return False status = (ev[0] & 0xF0) | chain.midi_chan - zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) + # zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) # was anytime working + self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) + # self.chain_manager + # self.state_manager + ###### self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) return True From a200dfe41005a4af719b27daf0d8f4d847fe679a Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 17 Sep 2025 00:10:20 +0200 Subject: [PATCH 28/57] renumbered beat buttons on right side of pads --- zyngine/ctrldev/ableton/push1_consts.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/zyngine/ctrldev/ableton/push1_consts.py b/zyngine/ctrldev/ableton/push1_consts.py index 45fb21d0e..8300128e6 100644 --- a/zyngine/ctrldev/ableton/push1_consts.py +++ b/zyngine/ctrldev/ableton/push1_consts.py @@ -105,14 +105,15 @@ BTN_LEFT = [0xB0, 44] BTN_RIGHT = [0xB0, 45] -BTN_QUATER = [0xB0, 36] -BTN_QUATER_T = [0xB0, 37] -BTN_EIGHTH = [0xB0, 38] -BTN_EIGHTH_T = [0xB0, 39] -BTN_SIXTEENTH = [0xB0, 40] -BTN_SIXTEENTH_T = [0xB0, 41] -BTN_THIRTYSECOND = [0xB0, 42] -BTN_THIRTYSECOND_T = [0xB0, 43] +# bottom up +BTN_TEMP1_QUATER = [0xB0, 36] +BTN_TEMP2_QUATER_T = [0xB0, 37] +BTN_TEMP3_EIGHTH = [0xB0, 38] +BTN_TEMP4_EIGHTH_T = [0xB0, 39] +BTN_TEMP5_SIXTEENTH = [0xB0, 40] +BTN_TEMP6_SIXTEENTH_T = [0xB0, 41] +BTN_TEMP7_THIRTYSECOND = [0xB0, 42] +BTN_TEMP8_THIRTYSECOND_T = [0xB0, 43] BTN_MASTER = [0xB0, 28] BTN_STOP = [0xB0, 29] From 304ebd9841655d7d338372df6e512fbe73612761 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 17 Sep 2025 00:15:25 +0200 Subject: [PATCH 29/57] written event-funcs for each driver state to clean up main event-func --- .../zynthian_ctrldev_ableton_push_1.py | 454 +++++++++++------- 1 file changed, 291 insertions(+), 163 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index ddf006ee9..849d05462 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -190,39 +190,8 @@ def end(self): ################################################################################################################# ################## START of scales fucntions ########################################################## - - def scale_update_leds(self, index_activated): # index defines blinkin LED - # Bicolor LEDs dim ## CC20-27 + 102-109 - scale_buttons = [ - ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], - ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], - ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] - ] - for t in scale_buttons: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_GREEN_DIM) - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, scale_buttons[index_activated], ABL.BI_GREEN_DIM_BLINK) - - def scales_cleanup(self): # set of any LED and display changes - # cleadup display - btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | | |" - btn_txt_row2 = "| | | | || | | | |" - self._display.write_xy_mem(btn_txt_row0, 0, 0) - self._display.write_xy_mem(btn_txt_row2, 0, 1) - self._display.write_xy_mem(btn_txt_row2, 0, 2) - self._display.write_xy_mem(btn_txt_row2, 0, 3) - self._display.update_screen() - - # cleanup scale LED - scale_buttons = [ - ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], - ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], - ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] - ] - for t in scale_buttons: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_LED_OFF) - - # def scales_setup(self) - + + # when changing to scales mode: start here # new in this class, to setup scales_mode = keyboard mode def set_dev_to_scales_mode(self): self.device_mode_active = self.DEV_MODE_SCALES @@ -246,26 +215,66 @@ def set_dev_to_scales_mode(self): self._display.update_screen() # set Display-Button_LEDS self.scale_update_leds(self.scales.tonic) # 0 is 'C' + + # Leaving scales mode: remove anything that is initailized + def scales_cleanup(self): # set of any LED and display changes + # cleadup display + btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | | |" + btn_txt_row2 = "| | | | || | | | |" + self._display.write_xy_mem(btn_txt_row0, 0, 0) + self._display.write_xy_mem(btn_txt_row2, 0, 1) + self._display.write_xy_mem(btn_txt_row2, 0, 2) + self._display.write_xy_mem(btn_txt_row2, 0, 3) + self._display.update_screen() + # cleanup scale LED + scale_buttons = [ + ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], + ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], + ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] + ] + for t in scale_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_LED_OFF) + + + # LED are setup to passive and the the actiavated LED is set + def scale_update_leds(self, index_activated): # index defines blinkin LED + # Bicolor LEDs dim ## CC20-27 + 102-109 + scale_buttons = [ + ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], + ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], + ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] + ] + for t in scale_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_GREEN_DIM) + # set scale LED blinking + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, scale_buttons[index_activated], ABL.BI_GREEN_DIM_BLINK) + + def scales_set_tonic(self, step): - if step > 63: step -=128 + if step > 63: step -=128 # make left turn values (+ 64 to +127) to negative value + # slowing down knob by factor ten self.steps_tonic = getattr(self, 'steps_tonic', 0) + step if not abs(self.steps_tonic) > 10: return # slow down. each 10th step self.steps_tonic = 0; + + # calculate new tonic new_tonic = self.scales.tonic + step if new_tonic < 0: new_tonic = 11 # target: B if new_tonic > 11: new_tonic = 0 # target: C - self.scales.tonic = new_tonic + self.scales.tonic = new_tonic # set tonic. thats all nothing to recalculate + + # set Display and LED from select buttons. PAD-LED don't need update, because mode isn't changed scale_n_mode = self.scales.harmony_get_scale_name_with_mode() self._display.write_xy_mem(scale_n_mode, 0, 2) self._display.update_screen() self.scale_update_leds(new_tonic) return + def scales_set_mode(self, step): - if step > 63: step -=128 # 127 = -1 - + if step > 63: step -=128 # 127 = -1 # make left turn values negative # lower knob speed by 10 self.steps_mode = getattr(self, 'steps_mode', 0) + step if not abs(self.steps_mode) > 10: return # slow down. each 10th steop @@ -293,16 +302,16 @@ def scales_set_mode(self, step): else: logging.error("Bug in set_mode") # do the magic - # self.set_dev_to_scales_mode(); ### Das ist overkill self.scales.init_scale(self.scales.tonic, self.scales.active_mode) # colorize pad array with tonic self.scales_set_pad_colors() # Display scale_n_mode = self.scales.harmony_get_scale_name_with_mode() - self._display.write_xy_mem(scale_n_mode, 0, 2) - self._display.update_screen() - # self.scales.tonic, self.scales.active_mode, 36-1, -5) - + self._display.write_xy_mem(scale_n_mode, 0, 2) # just little part with new text + self._display.update_screen() # just second row is dirty_tagged and updated + # because just mode changes, no change of tonic LEDs and Display + + # est color of 64 keyboard pads def scales_set_pad_colors(self): # def set_dev_scale_color(self): # self._leds_rgb.all_off(True) # led_states must not be deleted. is done in next lines. just for debugging @@ -318,6 +327,7 @@ def scales_set_pad_colors(self): self._leds_rgb.set_rgb(pad_nr, r, g, b, overlay=False) pass + # scale modes own midi_event routine, called by midi_event func def process_scale_event(self, ev) -> bool: if not self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected return False # we are not in scales mode @@ -334,7 +344,7 @@ def process_scale_event(self, ev) -> bool: if evtype == self.EV_PITCHBEND: # ribbon working as pitchwheel self._forward_like_niels_did(ev) - + # processing note events if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH]: # logging.debug(f"Scales mode -BRUMBY") @@ -371,7 +381,10 @@ def helper_set_new_tonic(tonic): ### return True - ### processing starts here + ### processing of Control Buttons and knobs starts here + ### because we set up push1_consts.py this way, it's so easy + ### to get differnt controls CC,PC,Note_on,Note_of... + ### so we get a very clean event-function. just name and function call. search_key = [ev[0], ev[1]] # build search key from event if ev[2] > 0: # just btn down eventes match search_key: @@ -408,9 +421,7 @@ def helper_set_new_tonic(tonic): self.scales_set_mode(ev[2]); return True case _: - pass - #print ("Button not defined in process_scale_event") - #logging.debug (f"Button {search_key}not defined in process_scale_event") + return False # event not for any of the defined buttons return False @@ -510,8 +521,76 @@ def pad_off(self, col, row): # logging.info(f"BRUMBY: row={row}; col={col} pad-note={note}") lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0) -############### End of derived Sequencer Functions. ##################### -################################################################################################### + # scenebuttons = right from pads + def sequencer_set_scene(self, ccnum): + # seams inconsistent, GUI says Scene. Api is: select Bank, or I misunderstaud + self.zynseq.select_bank (8- (ccnum - 36)) + # change LED state + for t in [ 36,37,38,39,40,41,42,43]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) # 2! + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL.MONO_LED_LIT_BLINK_FAST) # 2! + return True + + + def process_sequencer_event(self, ev) -> bool: + if not self.device_mode_active == self.DEV_MODE_PAD: # keyboard modus is selected + return False # we ignored here any event, we are not in Sequencer mode + + + cc = ev[1] # controller used to calculate bank. keys are in line + # cc_val=ev[2] + + search_key = [ev[0], ev[1]] + if ev[2] > 0: # just btn down eventes + match search_key: + case ABL.BTN_TEMP1_QUATER: + return self.sequencer_set_scene(cc) + case ABL.BTN_TEMP2_QUATER_T: + return self.sequencer_set_scene(cc) + case ABL.BTN_TEMP3_EIGHTH: + return self.sequencer_set_scene(cc) + case ABL.BTN_TEMP4_EIGHTH_T: + return self.sequencer_set_scene(cc) + case ABL.BTN_TEMP5_SIXTEENTH: + return self.sequencer_set_scene(cc) + case ABL.BTN_TEMP6_SIXTEENTH_T: + return self.sequencer_set_scene(cc) + case ABL.BTN_TEMP7_THIRTYSECOND: + return self.sequencer_set_scene(cc) + case ABL.BTN_TEMP8_THIRTYSECOND_T: + return self.sequencer_set_scene(cc) + + + + + + evtype = (ev[0] >> 4) & 0x0F + note = ev[1] & 0x7F + + if evtype == self.EV_NOTE_ON: # 0x9: # fitler just for note_on events + # all Pads send note_on events + # push are oriented buttom left to top right with cc 36 to 99 eq C2 to Eb7 + try: + pad_nr = note - ABL_PAD_START# eq C2 or ABL.PAD_36# so padnr ranges from 0 - 63 eq (range(64) + col = pad_nr // 8 # + row = pad_nr % 8 # + col = 7 - col; # midi notes start from bottom, so recalculate row + + # don't understand following XXXXXXXXXXXXXXXXX + pad = row * self.zynseq.col_in_bank + col + logging.debug(f"BRUMBY: row={row}; col={col}; pad={pad}") + if pad < self.zynseq.seq_in_bank: + self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) + return True + except: + pass + + + +# xxxxxx + + ############### End of derived Sequencer Functions. ##################### + ################################################################################################### # Just for me a helper function to set all pads off def pads_off(self): @@ -584,20 +663,24 @@ def midi_event(self, ev): # pad mode to control sequencer - elif self.device_mode_active == self.DEV_MODE_PAD: - if evtype == 0x9: # fitler just for note_on events - try: - col = (note - ABL_PAD_START) // 8 # - row = (note - ABL_PAD_START) % 8 # - col = 7 - col; # midi notes start from bottom, so recalculate row - pad = row * self.zynseq.col_in_bank + col - # logging.error(f"BRUMBY: row={row}; col={col}; pad={pad}") + elif self.device_mode_active == self.DEV_MODE_PAD: + if self.process_sequencer_event(ev): + return True + + ### old sequencer events + # if evtype == 0x9: # fitler just for note_on events + # try: + # col = (note - ABL_PAD_START) // 8 # + # row = (note - ABL_PAD_START) % 8 # + # col = 7 - col; # midi notes start from bottom, so recalculate row + # pad = row * self.zynseq.col_in_bank + col + # # logging.error(f"BRUMBY: row={row}; col={col}; pad={pad}") - if pad < self.zynseq.seq_in_bank: - self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) - return True # mark processed - except: - pass + # if pad < self.zynseq.seq_in_bank: + # self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) + # return True # mark processed + # except: + # pass # I think we dont need any note events... so filter out # NO note events anymore after this two lines. Comment out what you need after this lines in "Pad Mode" = DEV_MODE_PAD @@ -646,11 +729,6 @@ def midi_event(self, ev): elif (ccnum == ABL.BTN_SCALES[1]): logging.info("BRUMBY: BTN_SCALES processing") if not self.device_mode_active == self.DEV_MODE_SCALES: - # self.device_mode_active = self.DEV_MODE_SCALES - # visual feedback, let Scales Button blink - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) - # self.pads_off() # akk pad leds off - # self.set_dev_scale_color() self.set_dev_to_scales_mode() else: self.device_mode_active = self.DEV_MODE_PAD @@ -659,7 +737,6 @@ def midi_event(self, ev): lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) self.pads_off() # clean up visible state. all pad leds off self.refresh() # refreshe LEDs for Sequencer mode of this driver. - return True # Gui events moved to: @@ -680,124 +757,175 @@ def midi_event(self, ev): # to clean up the code GUI events are processed here def process_gui_events(self,ev) -> bool: + # on this device any button or knob we use sends 3-byte-events + # otherwise event is no control + if not len(ev) >= 3: return False + + # bild button search event + search_key = [ev[0], ev[1]] + data_val = ev[2] & 0x7F + + # make left turs on knobs negative + def helper_knob_calculation(ccval): + if ccval > 64: ccval -= 128 + return ccval + data_val_for_knobs = helper_knob_calculation(data_val) + + match search_key: + + # Knobs + case ABL.KNOB_1: + self.state_manager.send_cuia("ZYNPOT", [0, data_val_for_knobs]); return True + case ABL.KNOB_2: + self.state_manager.send_cuia("ZYNPOT", [1, data_val_for_knobs]); return True + case ABL.KNOB_3: + self.state_manager.send_cuia("ZYNPOT", [2, data_val_for_knobs]); return True + case ABL.KNOB_4: + self.state_manager.send_cuia("ZYNPOT", [3, data_val_for_knobs]); return True + case _: pass + + if data_val > 0: # just key-down events + match search_key: + # Buttons + case ABL.BTN_OK, ABL.BTN_R1_C3: + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]); return True + case ABL.BTN_R1_C1: + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [0,"S"]) ; return True + case ABL.BTN_R1_C2: + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) ; return True + case ABL.BTN_R1_C3: + self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [2,"S"]) ; return True + + case ABL.BTN_ESC: + self.state_manager.send_cuia("BACK"); return True + case ABL.BTN_RIGHT: + self.state_manager.send_cuia("ARROW_RIGHT"); return False + case ABL.BTN_LEFT: + self.state_manager.send_cuia("ARROW_LEFT"); return False + case ABL.BTN_UP: # CC46 + self.state_manager.send_cuia("ARROW_UP"); return True + case ABL.BTN_DOWN: + self.state_manager.send_cuia("ARROW_DOWN"); return True + case ABL.BTN_START: # ehemals ABL_PLAY: + if self.shift: # shift button pressed + self.state_manager.send_cuia("TOGGLE_MIDI_PLAY"); return True + else: + self.state_manager.send_cuia("TOGGLE_PLAY"); return True + case ABL.BTN_REC: # ABL_REC: + if self.shift: + self.state_manager.send_cuia("TOGGLE_MIDI_RECORD"); return True + else: + self.state_manager.send_cuia("TOGGLE_RECORD"); return True + case _: pass + + ################ OLD VERSION ############################## if ev[0] & 0xF0 == 0xB0: # event is midi CC ? ccnum = ev[1] & 0x7F ccval = ev[2] & 0x7F - # no more neede moved to scales_event... - # if ABL.KNOB_7[1] == ccnum: # scale - # self.scales_set_tonic(ccval) - - # elif ABL.KNOB_8[1] == ccnum: # mode - # self.scales_set_mode(ccval) - - # Zynpoties Werte an GUI - # Potis Oben 72 - 75 die ersten 4 - # if 70 < ccnum < 80: - if ABL.KNOB_1[1] <= ccnum <= ABL.KNOB_4[1]: - # self.state_manager.send_cuia("ZYNPOT_ABS", [ccnum - 72, ccval/127]) - val = ccval - if val > 68: - val = (val - 128) - # falsch geraten, nicht ZYNPT_REL. Vielleicht ZYNPOT? - self.state_manager.send_cuia("ZYNPOT", [ccnum - 71, val]) - logging.debug(f"BRUMBY: Poti={ccnum-71} val={val}") - return True - - elif (ccnum == ABL.BTN_OK[1]) or (ccnum == 23): - logging.debug("ABL_OK BRUMBY") - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) - return True - - elif ccnum == ABL.BTN_R1_C1[1]: # Zweiter Button unter dem Display - logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [0,"S"]) - return True - # elif ccnum == 21: Does that work? - elif ccnum == ABL.BTN_R1_C2[1]: # Zweiter Button unter dem Display - logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) - return True - elif ccnum == ABL.BTN_R1_C5[1]: # Zweiter Button unter dem Display - logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [2,"S"]) - return True - # elif ccnum == 21: Does that work? - elif ccnum == ABL.BTN_R1_C4[1]: # Zweiter Button unter dem Display - logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) - return True - + # # Zynpoties Werte an GUI + # # Potis Oben 72 - 75 die ersten 4 + # # if 70 < ccnum < 80: + # if ABL.KNOB_1[1] <= ccnum <= ABL.KNOB_4[1]: + # # self.state_manager.send_cuia("ZYNPOT_ABS", [ccnum - 72, ccval/127]) + # val = ccval + # if val > 68: + # val = (val - 128) + # # falsch geraten, nicht ZYNPT_REL. Vielleicht ZYNPOT? + # self.state_manager.send_cuia("ZYNPOT", [ccnum - 71, val]) + # # logging.debug(f"Poti={ccnum-71} val={val}") + # return True + + # elif (ccnum == ABL.BTN_OK[1]) or (ccnum == 23): + # # logging.debug("ABL_OK)") + # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) + # return True - - - - - elif ccnum == ABL.BTN_ESC[1]: - # logging.debug("BTN_ESC BRUMBY") - self.state_manager.send_cuia("BACK") - return True + # elif ccnum == ABL.BTN_R1_C1[1]: # Zweiter Button unter dem Display + # # logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") + # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [0,"S"]) + # return True + # # elif ccnum == 21: Does that work? + # elif ccnum == ABL.BTN_R1_C2[1]: # Zweiter Button unter dem Display + # # logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") + # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) + # return True + # elif ccnum == ABL.BTN_R1_C5[1]: # Zweiter Button unter dem Display + # # logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") + # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [2,"S"]) + # return True + # # elif ccnum == 21: Does that work? + # elif ccnum == ABL.BTN_R1_C4[1]: # Zweiter Button unter dem Display + # # logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") + # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) + # return True + + # elif ccnum == ABL.BTN_ESC[1]: + # # logging.debug("BTN_ESC BRUMBY") + # self.state_manager.send_cuia("BACK") + # return True - # elif ccnum == 45: - elif ccnum == ABL.BTN_RIGHT[1]: - # elif ccnum == 0x66: - # TRACK RIGHT - self.state_manager.send_cuia("ARROW_RIGHT") - return False + # # elif ccnum == 45: + # elif ccnum == ABL.BTN_RIGHT[1]: + # # elif ccnum == 0x66: + # # TRACK RIGHT + # self.state_manager.send_cuia("ARROW_RIGHT") + # return False - # elif ccnum == 44: - elif ccnum == ABL.BTN_LEFT[1]: - # elif ccnum == 0x67: - # TRACK LEFT - self.state_manager.send_cuia("ARROW_LEFT") - return False + # # elif ccnum == 44: + # elif ccnum == ABL.BTN_LEFT[1]: + # # elif ccnum == 0x67: + # # TRACK LEFT + # self.state_manager.send_cuia("ARROW_LEFT") + # return False - elif ccnum == ABL.BTN_UP[1]: # CC46 - # elif ccnum == 0x68: - # UP - self.state_manager.send_cuia("ARROW_UP") - return True + # elif ccnum == ABL.BTN_UP[1]: # CC46 + # # elif ccnum == 0x68: + # # UP + # self.state_manager.send_cuia("ARROW_UP") + # return True - elif ccnum == ABL.BTN_DOWN[1]: - # elif ccnum == 47: - # DOWN - self.state_manager.send_cuia("ARROW_DOWN") - return True + # elif ccnum == ABL.BTN_DOWN[1]: + # # elif ccnum == 47: + # # DOWN + # self.state_manager.send_cuia("ARROW_DOWN") + # return True - elif ccnum == ABL.BTN_START[1]: # ehemals ABL_PLAY: - # PLAY - if self.shift: - self.state_manager.send_cuia("TOGGLE_MIDI_PLAY") - else: - self.state_manager.send_cuia("TOGGLE_PLAY") - return True + # elif ccnum == ABL.BTN_START[1]: # ehemals ABL_PLAY: + # # PLAY + # if self.shift: + # self.state_manager.send_cuia("TOGGLE_MIDI_PLAY") + # else: + # self.state_manager.send_cuia("TOGGLE_PLAY") + # return True - elif ccnum == ABL.BTN_REC[1]: # ABL_REC: - # RECORD - if self.shift: - self.state_manager.send_cuia("TOGGLE_MIDI_RECORD") - else: - self.state_manager.send_cuia("TOGGLE_RECORD") - return True + # elif ccnum == ABL.BTN_REC[1]: # ABL_REC: + # # RECORD + # if self.shift: + # self.state_manager.send_cuia("TOGGLE_MIDI_RECORD") + # else: + # self.state_manager.send_cuia("TOGGLE_RECORD") + # return True # These are the note_length Buttons right of pads in Sequencer mode to start and stop a whole row of sequences - elif (ccnum > 35) and (ccnum < 44): - self.zynseq.select_bank (8- (ccnum - 36)) - # Leuchstatus ändern - for t in [ 36,37,38,39,40,41,42,43]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) # 2! - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL.MONO_LED_LIT_BLINK_FAST) # 2! - return True + # if (ccnum > 35) and (ccnum < 44): + # self.zynseq.select_bank (8- (ccnum - 36)) + # # Leuchstatus ändern + # for t in [ 36,37,38,39,40,41,42,43]: + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) # 2! + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL.MONO_LED_LIT_BLINK_FAST) # 2! + # return True return False # event is not processed + def button_name_from_midi_event(self, ev): ###, button_event): # button_event is a Constant from import abl. # create key_data from midi event # if too slow, we have to revert the array to a named array with buttonevent as name From 499582f4e64c771f55c9a451c800c203ff0e06d3 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 17 Sep 2025 00:39:50 +0200 Subject: [PATCH 30/57] code rearrangements for clearance --- .../zynthian_ctrldev_ableton_push_1.py | 190 ++++-------------- 1 file changed, 36 insertions(+), 154 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 849d05462..957c72726 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -33,21 +33,14 @@ import logging import traceback -# Brumbys imports +# Brumbys new mports from time import sleep # pause between sysex events. -#mport sys # for button detection -# vor editor use following. -# import ableton.push1_consts as ABL +import zyngine.ctrldev.ableton.push1_consts as ABL from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony from zyngine.zynthian_signal_manager import zynsigman from zyngine.zynthian_engine import zynthian_engine # to send directly to soundengine... from zyngine.ctrldev.zynthian_ctrldev_base_extended import RunTimer, KnobSpeedControl, ButtonTimer, CONST - -# for running driver this way: -import zyngine.ctrldev.ableton.push1_consts as ABL - - # Zynthian specific modules from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer from zyncoder.zyncore import lib_zyncore @@ -69,9 +62,8 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer): - # logging.info("Class call") - # Im Weblog wird angezeigt, dass der Treiber geladen wurde + # Weblog shows this messages # dev_ids = ["Ableton Push IN 2", "Ableton Push IN 1"] # get by stepping through zynthian_ctrldev_manager.load_driver() dev_ids = ["Ableton Push IN 2"] # get by stepping through zynthian_ctrldev_manager.load_driver(). Data just at Port 2 @@ -79,7 +71,9 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ driver_name = "Ableton Push v1" # not essential. class name would be used otherwise driver_description = "Interface Ableton Push v1 with zynpad and zynmixer" - # Folgende Farben sind wohl die Sequencer Farben?? + ################################ + + # Colors for LED-Pads in Sequencermode # TODO: Palette has to be fixed # siehe: https://pushmod.blogspot.com/p/pad-color-table.html # ORIGINAL PAD_COLOURS = [71, 104, 76, 51, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] PAD_COLOURS = [61, 36, 63, 54, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] # not running @@ -87,12 +81,7 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ STOPPING_COLOUR = 120 # RED RUNNING_COLOR = 123 # GREEM - # dev_modes - DEV_MODE_NONE = None - DEV_MODE_PAD = 1 - # DEV_MODE_DRUMS = 2 - DEV_MODE_SCALES = 3 # keyboard modes - + # evtype = (ev[0] >> 4) & 0x0F -> EV_NOTE_OFF = 0x8 # 3 Bytes EV_NOTE_ON = 0X9 # 3 Bytes @@ -104,8 +93,13 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ EV_SYSTEM = 0xF # Systemtype = ev[0] & 0x0F + # dev_modes + DEV_MODE_NONE = None + DEV_MODE_PAD = 1 + # DEV_MODE_DRUMS = 2 + DEV_MODE_SCALES = 3 # keyboard modes # pad_mode_active = PAD_MODE_SEQ - device_mode_active = DEV_MODE_SCALES + device_mode_active = DEV_MODE_SCALES # initial mode scales = Harmony(8,8) scales.init_scale(tonic=0, @@ -121,7 +115,7 @@ def __init__(self, state_manager, idev_in, idev_out=None): # super.__init__ saves state_manger, chainmanger, idev_in and idev_out # nothing more. - # Indecators of the device LEDs and Text + # Indecators of the device LEDs and Text # NOT USED self._leds_mono = Feedback_Mono_LEDs(idev_out) # control buttons right and left from pads self._leds_bi = Feedback_Bi_LEDs(idev_out) # display buttons below display, above pads self._leds_rgb = Feedback_RGB_LEDs(idev_out) # pads in rgb @@ -433,10 +427,12 @@ def helper_set_new_tonic(tonic): ### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. ### just copy the derived functions in the this driver and implement them accordingly + # DONT CHANGE FUNC NAME (is inherited) def update_mixer_active_chain(self, active_chain): """Update hardware indicators for active_chain""" logging.error(f"not implemented active_chain: {active_chain}") - + + # DONT CHANGE FUNC NAME (is inherited) def update_mixer_strip(self, chan, symbol, value): """Update hardware indicators for a mixer strip: mute, solo, level, balance, etc. # oh my goodness, what means etc. ? *SHOULD* be implemented by child class @@ -460,6 +456,7 @@ def update_mixer_strip(self, chan, symbol, value): ################ Start of SEQUENCER FUNCTIONS ######################################################### # this function is called by zynseq when a sequencer state is changed # we have update pad LED to show state + # DONT CHANGE FUNC NAME (is inherited) def update_seq_state(self, bank, seq, state, mode, group): try: # return @@ -510,11 +507,13 @@ def update_seq_state(self, bank, seq, state, mode, group): print(f"Fehler aufgetreten: {e}") # for LED feedback bei pad mode (Sequencer) + # DONT CHANGE FUNC NAME (is inherited) def refresh(self): # form zynseq classe # if not filtered, the pad loop kills any other LED setup if self.device_mode_active == self.DEV_MODE_PAD: return super().refresh() + # DONT CHANGE FUNC NAME (is inherited) def pad_off(self, col, row): # note = 96 + row * 16 + col # statt 96 -> 91 für Push note = ABL_PAD_END +1 -(row+1) * 8 + col # recalculate midi note from col and row @@ -533,9 +532,9 @@ def sequencer_set_scene(self, ccnum): def process_sequencer_event(self, ev) -> bool: + """event function in sequencer state""" if not self.device_mode_active == self.DEV_MODE_PAD: # keyboard modus is selected return False # we ignored here any event, we are not in Sequencer mode - cc = ev[1] # controller used to calculate bank. keys are in line # cc_val=ev[2] @@ -559,14 +558,14 @@ def process_sequencer_event(self, ev) -> bool: return self.sequencer_set_scene(cc) case ABL.BTN_TEMP8_THIRTYSECOND_T: return self.sequencer_set_scene(cc) - - - - + case _: + pass + evtype = (ev[0] >> 4) & 0x0F note = ev[1] & 0x7F + # we do pad calculation with pads numbered woth control registers if evtype == self.EV_NOTE_ON: # 0x9: # fitler just for note_on events # all Pads send note_on events # push are oriented buttom left to top right with cc 36 to 99 eq C2 to Eb7 @@ -586,23 +585,22 @@ def process_sequencer_event(self, ev) -> bool: pass - -# xxxxxx - ############### End of derived Sequencer Functions. ##################### ################################################################################################### # Just for me a helper function to set all pads off def pads_off(self): - dbg = False - logging.debug("BRUMBY: pads_off") + + # logging.debug("BRUMBY: pads_off") for row in range(self.rows): for col in range(self.cols): self.pad_off(col, row) + # https://discourse.zynthian.org/t/driver-for-ableton-push-1-first-steps/12166/8 def _forward_like_niels_did(self, ev): + # Direct keybed to chains #if (channel == 1): chain = self.chain_manager.get_active_chain() @@ -619,6 +617,8 @@ def _forward_like_niels_did(self, ev): # if not processed you call # return super()._on_midi_event(ev)` + + def midi_event(self, ev): ### For debugging purposes block can be commented out ! @@ -666,22 +666,7 @@ def midi_event(self, ev): elif self.device_mode_active == self.DEV_MODE_PAD: if self.process_sequencer_event(ev): return True - - ### old sequencer events - # if evtype == 0x9: # fitler just for note_on events - # try: - # col = (note - ABL_PAD_START) // 8 # - # row = (note - ABL_PAD_START) % 8 # - # col = 7 - col; # midi notes start from bottom, so recalculate row - # pad = row * self.zynseq.col_in_bank + col - # # logging.error(f"BRUMBY: row={row}; col={col}; pad={pad}") - - # if pad < self.zynseq.seq_in_bank: - # self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) - # return True # mark processed - # except: - # pass - + # I think we dont need any note events... so filter out # NO note events anymore after this two lines. Comment out what you need after this lines in "Pad Mode" = DEV_MODE_PAD if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH, self.EV_PITCHBEND]: @@ -754,6 +739,8 @@ def midi_event(self, ev): return False # When nothing matches, False shows that midi event has to be processed further + + ######### GUI EVENTS #################################### # to clean up the code GUI events are processed here def process_gui_events(self,ev) -> bool: @@ -817,112 +804,7 @@ def helper_knob_calculation(ccval): else: self.state_manager.send_cuia("TOGGLE_RECORD"); return True case _: pass - - ################ OLD VERSION ############################## - if ev[0] & 0xF0 == 0xB0: # event is midi CC ? - ccnum = ev[1] & 0x7F - ccval = ev[2] & 0x7F - - # # Zynpoties Werte an GUI - # # Potis Oben 72 - 75 die ersten 4 - # # if 70 < ccnum < 80: - # if ABL.KNOB_1[1] <= ccnum <= ABL.KNOB_4[1]: - # # self.state_manager.send_cuia("ZYNPOT_ABS", [ccnum - 72, ccval/127]) - # val = ccval - # if val > 68: - # val = (val - 128) - # # falsch geraten, nicht ZYNPT_REL. Vielleicht ZYNPOT? - # self.state_manager.send_cuia("ZYNPOT", [ccnum - 71, val]) - # # logging.debug(f"Poti={ccnum-71} val={val}") - # return True - - # elif (ccnum == ABL.BTN_OK[1]) or (ccnum == 23): - # # logging.debug("ABL_OK)") - # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) - # return True - - # elif ccnum == ABL.BTN_R1_C1[1]: # Zweiter Button unter dem Display - # # logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") - # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [0,"S"]) - # return True - # # elif ccnum == 21: Does that work? - # elif ccnum == ABL.BTN_R1_C2[1]: # Zweiter Button unter dem Display - # # logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") - # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) - # return True - # elif ccnum == ABL.BTN_R1_C5[1]: # Zweiter Button unter dem Display - # # logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") - # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [2,"S"]) - # return True - # # elif ccnum == 21: Does that work? - # elif ccnum == ABL.BTN_R1_C4[1]: # Zweiter Button unter dem Display - # # logging.debug("ZYNPUT_BUT 1 ESC BRUMBY") - # self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]) - # return True - - # elif ccnum == ABL.BTN_ESC[1]: - # # logging.debug("BTN_ESC BRUMBY") - # self.state_manager.send_cuia("BACK") - # return True - - # # elif ccnum == 45: - # elif ccnum == ABL.BTN_RIGHT[1]: - # # elif ccnum == 0x66: - # # TRACK RIGHT - # self.state_manager.send_cuia("ARROW_RIGHT") - # return False - - - # # elif ccnum == 44: - # elif ccnum == ABL.BTN_LEFT[1]: - # # elif ccnum == 0x67: - # # TRACK LEFT - # self.state_manager.send_cuia("ARROW_LEFT") - # return False - - - # elif ccnum == ABL.BTN_UP[1]: # CC46 - # # elif ccnum == 0x68: - # # UP - # self.state_manager.send_cuia("ARROW_UP") - # return True - - - # elif ccnum == ABL.BTN_DOWN[1]: - # # elif ccnum == 47: - # # DOWN - # self.state_manager.send_cuia("ARROW_DOWN") - # return True - - - # elif ccnum == ABL.BTN_START[1]: # ehemals ABL_PLAY: - # # PLAY - # if self.shift: - # self.state_manager.send_cuia("TOGGLE_MIDI_PLAY") - # else: - # self.state_manager.send_cuia("TOGGLE_PLAY") - # return True - - - # elif ccnum == ABL.BTN_REC[1]: # ABL_REC: - # # RECORD - # if self.shift: - # self.state_manager.send_cuia("TOGGLE_MIDI_RECORD") - # else: - # self.state_manager.send_cuia("TOGGLE_RECORD") - # return True - - - # These are the note_length Buttons right of pads in Sequencer mode to start and stop a whole row of sequences - # if (ccnum > 35) and (ccnum < 44): - # self.zynseq.select_bank (8- (ccnum - 36)) - # # Leuchstatus ändern - # for t in [ 36,37,38,39,40,41,42,43]: - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) # 2! - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL.MONO_LED_LIT_BLINK_FAST) # 2! - # return True - - + return False # event is not processed @@ -943,7 +825,7 @@ def button_name_from_midi_event(self, ev): ###, button_event): # button_event is def set_pad_rgb(self, pad_nr: int, r:int ,g:int ,b:int): - logging.error(" set_pad_rb aufgerufen.") + logging.error(" set_pad_rb aufgerufen. not implemented") # # Sysex : 240,71,127,21,4,0,8,,0,,,,,,,247 # # pad = 0-71 NICHT PAD_36 - PAD_99 # # blogspot.com From 9dd5db088d146393f8b2e51443e2c52f79715e6b Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 17 Sep 2025 01:19:37 +0200 Subject: [PATCH 31/57] Pads_45 to Pad_50 CC correction --- zyngine/ctrldev/ableton/push1_consts.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zyngine/ctrldev/ableton/push1_consts.py b/zyngine/ctrldev/ableton/push1_consts.py index 8300128e6..3567b1bb6 100644 --- a/zyngine/ctrldev/ableton/push1_consts.py +++ b/zyngine/ctrldev/ableton/push1_consts.py @@ -149,11 +149,11 @@ PAD_43 = [0x90, 43] # note PAD_44 = [0x90, 44] # note -PAD_45 = [0x90, 44] # note -PAD_46 = [0x90, 45] # note -PAD_47 = [0x90, 46] # note -PAD_48 = [0x90, 47] # note -PAD_49 = [0x90, 48] # note +PAD_45 = [0x90, 45] # note +PAD_46 = [0x90, 46] # note +PAD_47 = [0x90, 47] # note +PAD_48 = [0x90, 48] # note +PAD_49 = [0x90, 49] # note PAD_50 = [0x90, 50] # note PAD_51 = [0x90, 51] # note From a485826af07f81b475ce4e6e4129c64e6a0c28a8 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 17 Sep 2025 16:53:41 +0200 Subject: [PATCH 32/57] corrected wrong button values --- zyngine/ctrldev/ableton/push1_consts.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/zyngine/ctrldev/ableton/push1_consts.py b/zyngine/ctrldev/ableton/push1_consts.py index 3567b1bb6..6e8a34e32 100644 --- a/zyngine/ctrldev/ableton/push1_consts.py +++ b/zyngine/ctrldev/ableton/push1_consts.py @@ -106,14 +106,14 @@ BTN_RIGHT = [0xB0, 45] # bottom up -BTN_TEMP1_QUATER = [0xB0, 36] -BTN_TEMP2_QUATER_T = [0xB0, 37] -BTN_TEMP3_EIGHTH = [0xB0, 38] -BTN_TEMP4_EIGHTH_T = [0xB0, 39] -BTN_TEMP5_SIXTEENTH = [0xB0, 40] -BTN_TEMP6_SIXTEENTH_T = [0xB0, 41] -BTN_TEMP7_THIRTYSECOND = [0xB0, 42] -BTN_TEMP8_THIRTYSECOND_T = [0xB0, 43] +BTN_BEAT_1_QUATER = [0xB0, 36] +BTN_BEAT_2_QUATER_T = [0xB0, 37] +BTN_BEAT_3_EIGHTH = [0xB0, 38] +BTN_BEAT_4_EIGHTH_T = [0xB0, 39] +BTN_BEAT_5_SIXTEENTH = [0xB0, 40] +BTN_BEAT_6_SIXTEENTH_T = [0xB0, 41] +BTN_BEAT_7_THIRTYSECOND = [0xB0, 42] +BTN_BEAT_8_THIRTYSECOND_T = [0xB0, 43] BTN_MASTER = [0xB0, 28] BTN_STOP = [0xB0, 29] From 3cbfcf604fcd85a8d7c1de9e4cd1db4194361e9d Mon Sep 17 00:00:00 2001 From: JBrumby Date: Wed, 17 Sep 2025 16:58:51 +0200 Subject: [PATCH 33/57] cleanup of main event func --- .../zynthian_ctrldev_ableton_push_1.py | 302 ++++++++++++------ 1 file changed, 213 insertions(+), 89 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 957c72726..47bd20a49 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -77,9 +77,9 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ # siehe: https://pushmod.blogspot.com/p/pad-color-table.html # ORIGINAL PAD_COLOURS = [71, 104, 76, 51, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] PAD_COLOURS = [61, 36, 63, 54, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] # not running - STARTING_COLOUR = 3 # WHITE + STARTING_COLOUR = 123 # GREEM STOPPING_COLOUR = 120 # RED - RUNNING_COLOR = 123 # GREEM + RUNNING_COLOR = 3 # WHITE # evtype = (ev[0] >> 4) & 0x0F -> @@ -94,10 +94,11 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ # dev_modes - DEV_MODE_NONE = None - DEV_MODE_PAD = 1 + DEV_MODE_NONE = None + DEV_MODE_PAD = 1 + DEV_MODE_SCALES = 2 # keyboard modes + DEV_MODE_MIXER = 3 # DEV_MODE_DRUMS = 2 - DEV_MODE_SCALES = 3 # keyboard modes # pad_mode_active = PAD_MODE_SEQ device_mode_active = DEV_MODE_SCALES # initial mode @@ -196,11 +197,13 @@ def set_dev_to_scales_mode(self): self._display.clear() scale_n_mode = self.scales.harmony_get_scale_name_with_mode() self._display.write_xy_mem(scale_n_mode, 0, 2) - # Btn_row + btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | Scale | Mode |" - btn_txt_row1 = "| | | | || | | | |" + # btn_txt_row1 = f"| {chr(12)} {chr(11)} {chr(10)} | {chr(9)} {chr(8)} {chr(7)} {chr(6)} | | |" + btn_txt_row1 = " " btn_txt_row2 = "|modes here | | || G# | A | A# | B |" btn_txt_row3 = "| C | C# | D | D# || E | F | F# | G |" + self._display.write_xy_mem(btn_txt_row0, 0, 0) self._display.write_xy_mem(btn_txt_row1, 0, 1) self._display.write_xy_mem(btn_txt_row2, 0, 2) @@ -450,7 +453,12 @@ def update_mixer_strip(self, chan, symbol, value): ### END of Mixer functions. - + def process_sequencer_event(self, ev) -> bool: + """event function in sequencer state""" + # if using shift button with knob, then not following we are not in any mode + # if not self.device_mode_active == self.DEV_MODE_MIXER: # keyboard modus is selected + # return False # we ignored here any event, we are not in Sequencer mode + ############################################################################################################## ################ Start of SEQUENCER FUNCTIONS ######################################################### @@ -522,7 +530,7 @@ def pad_off(self, col, row): # scenebuttons = right from pads def sequencer_set_scene(self, ccnum): - # seams inconsistent, GUI says Scene. Api is: select Bank, or I misunderstaud + # seams inconsistent, GUI says Scene. Api is: select Bank, or I misunderstood self.zynseq.select_bank (8- (ccnum - 36)) # change LED state for t in [ 36,37,38,39,40,41,42,43]: @@ -542,21 +550,21 @@ def process_sequencer_event(self, ev) -> bool: search_key = [ev[0], ev[1]] if ev[2] > 0: # just btn down eventes match search_key: - case ABL.BTN_TEMP1_QUATER: + case ABL.BTN_BEAT_1_QUATER: return self.sequencer_set_scene(cc) - case ABL.BTN_TEMP2_QUATER_T: + case ABL.BTN_BEAT_2_QUATER_T: return self.sequencer_set_scene(cc) - case ABL.BTN_TEMP3_EIGHTH: + case ABL.BTN_BEAT_3_EIGHTH: return self.sequencer_set_scene(cc) - case ABL.BTN_TEMP4_EIGHTH_T: + case ABL.BTN_BEAT_4_EIGHTH_T: return self.sequencer_set_scene(cc) - case ABL.BTN_TEMP5_SIXTEENTH: + case ABL.BTN_BEAT_5_SIXTEENTH: return self.sequencer_set_scene(cc) - case ABL.BTN_TEMP6_SIXTEENTH_T: + case ABL.BTN_BEAT_6_SIXTEENTH_T: return self.sequencer_set_scene(cc) - case ABL.BTN_TEMP7_THIRTYSECOND: + case ABL.BTN_BEAT_7_THIRTYSECOND: return self.sequencer_set_scene(cc) - case ABL.BTN_TEMP8_THIRTYSECOND_T: + case ABL.BTN_BEAT_8_THIRTYSECOND_T: return self.sequencer_set_scene(cc) case _: pass @@ -565,6 +573,15 @@ def process_sequencer_event(self, ev) -> bool: evtype = (ev[0] >> 4) & 0x0F note = ev[1] & 0x7F + ### Program Change Event from Push 1 # It doesn't send such !!! just for explanatioin + # We filter them out. Push 1 has no midi in and sends nor PC. + # Or should we leave them in. + if evtype == self.EV_PC: # 0xC: + ## val1 = ev[1] & 0x7F + ## self.zynseq.select_bank(val1 + 1) #### That would shange Bank /Scene in Sequencer. We do it with Beat_Buttons + return True # + + # we do pad calculation with pads numbered woth control registers if evtype == self.EV_NOTE_ON: # 0x9: # fitler just for note_on events # all Pads send note_on events @@ -579,7 +596,8 @@ def process_sequencer_event(self, ev) -> bool: pad = row * self.zynseq.col_in_bank + col logging.debug(f"BRUMBY: row={row}; col={col}; pad={pad}") if pad < self.zynseq.seq_in_bank: - self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) + # this is the complete magic. Start and stop a track in a scene (bank) + self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) # yes Scene is bank !!! return True except: pass @@ -617,7 +635,32 @@ def _forward_like_niels_did(self, ev): # if not processed you call # return super()._on_midi_event(ev)` - + def set_device_mode_new(self, new_mode): + match self.device_mode_active: + case self.DEV_MODE_MIXER: + # deinit mixer + pass + case self.DEV_MODE_PAD: + # deinit scales + pass + case self.DEV_MODE_SCALES: + # deinit scales mode + pass + self.device_mode_active = new_mode + match new_mode: + case self.DEV_MODE_MIXER: + # init mixer + pass + case self.DEV_MODE_PAD: + # init scales + pass + case self.DEV_MODE_SCALES: + # init scales mode + pass + case _: + # code not defined + logging.error("DEVICE Mode not defined. Programming Error") + def midi_event(self, ev): @@ -627,6 +670,46 @@ def midi_event(self, ev): note_or_register = None val_or_vel = None + if len(ev) > 1: + search_key = [ev[0], ev[1]] # ev to search_key + match search_key: + case None: + pass + case ABL.BTN_SHIFT: # as momentary button ! hasto be hold for functions change + self.shift = val_or_vel != 0 # set shift variable. but just momenatary + # visual feedback with button LED + if self.shift: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_LIT_BLINK) + else: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_DIM) + return True # event processed. No further action required + + case ABL.BTN_VOLUME: # mode change to mixer? It isn't best chosen. + return self.set_device_mode_new(self.DEV_MODE_MIXER) + + pass + case _: + pass + + # try to process the ev with active mode + match self.device_mode_active: + case self.DEV_MODE_MIXER: + if self.process_mixer_event(ev): # dom't return if False + return True + case self.DEV_MODE_PAD: + if self.process_sequencer_event(ev): + return True + case self.DEV_MODE_SCALES: + if self.process_scale_event(ev): + return True + case _: + pass # no actual devicemode + + # now the Gui events. + # Gui events moved to: + if self.process_gui_events(ev): return True + return False # that should be all + if len(ev) > 0: evtype = (ev[0] >> 4) & 0x0F chan_or_instruction = ev[0] & 0xF @@ -646,7 +729,7 @@ def midi_event(self, ev): logging.debug(f"Button: {btn_name} on chan. {chan_or_instruction} gives midi_event: {aftertouch} {hex(ev[0])} {hex(ev[1])} {hex(val_or_vel)} = {evtype}, {note_or_register} {val_or_vel}") - ### End of debugging purposes. + ### End of "debugging purposes." # don't process 1-byte events. if len(ev)<2: @@ -657,23 +740,24 @@ def midi_event(self, ev): note = ev[1] & 0x7F # is that need? any event field is from 0-127 except the status field ### Scale mode - if self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected - if self.process_scale_event(ev): - return True # return value cuts out follwing + # if self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected + # if self.process_scale_event(ev): + # return True # return value cuts out follwing # pad mode to control sequencer - elif self.device_mode_active == self.DEV_MODE_PAD: - if self.process_sequencer_event(ev): - return True + # elif self.device_mode_active == self.DEV_MODE_PAD: + # if self.process_sequencer_event(ev): + # return True # I think we dont need any note events... so filter out # NO note events anymore after this two lines. Comment out what you need after this lines in "Pad Mode" = DEV_MODE_PAD - if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH, self.EV_PITCHBEND]: - return True + #### # if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH, self.EV_PITCHBEND]: + # return True # no return call. I don't know if I need note_on_events further down. + # if prceessd before there are just note_events lower PAD_START and higher PAD_END if processed before # GUI Control Changes @@ -724,17 +808,13 @@ def midi_event(self, ev): self.refresh() # refreshe LEDs for Sequencer mode of this driver. return True + # Gui events moved to: if self.process_gui_events(ev): return True - # evtype = MIDI_Program Change - elif evtype == 0xC: - val1 = ev[1] & 0x7F - self.zynseq.select_bank(val1 + 1) - return True - + # default return, when no match return False # When nothing matches, False shows that midi event has to be processed further @@ -821,7 +901,7 @@ def button_name_from_midi_event(self, ev): ###, button_event): # button_event is # logging.debug(f"midi_event {ev} {ev[0]}, {ev[1]}, from Button with name: {name} and value: {data}") return name # logging.debug(f"midi_event {ev} from Button not defined with value: {data}") - return "" + return "" # "" def set_pad_rgb(self, pad_nr: int, r:int ,g:int ,b:int): @@ -850,66 +930,110 @@ def set_pad_rgb(self, pad_nr: int, r:int ,g:int ,b:int): # sysex = bytes ([240,71,127,21,4,0,8,pad_nr,0,r1,r2,g1,g2,b1,b2,247] ) # lib_zyncore.dev_send_midi_event(self.idev_out, sysex, len(sysex)) - def send_sysex(self, data): - return - # Send SysEx universal inquiry. - # It's answered by some devices with a SysEx message. - # def send_sysex_universal_inquiry(self): - if self.idev_out > 0: - - #msg = bytes(ABL.SYSEX_DATA_SET_USER_MODE) - #logging.error(f"BRUMBY: set user mode SYSEX={msg};") - #lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - #sleep (0.05) - - # "240 71 127 21 24 0 69 0 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247") - s = "240 71 127 21 25 0 69 0 32 32 32 72 101 108 108 111 32 87 111 114 108 100 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247" - s2 = "240 71 127 21 26 0 69 0 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247" - # "240 71 127 21 27 0 69 0 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247" + +# ------------------------------------------------------------------------------ - # s = "240 71 127 21 25 0 69 0 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 247" - - # String S - integers = [int(x) for x in s.split()] - msg = bytes(integers) - logging.error(f"BRUMBY: DISPLAY LINE2 SYSEX={msg};") - lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - # String S2 - integers = [int(x) for x in s2.split()] - msg = bytes(integers) - logging.error(f"BRUMBY: DISPLAY LINE3 SYSEX={msg};") - lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - #logging.error(f"BRUMBY: MIDDLE OF send_sysex;") +class Feedback_Display: - #logging.error(f"BRUMBY: SYSEX={data};") - #lib_zyncore.dev_send_midi_event(self.idev_out, data, len(data)) - - #sleep(0.05) - logging.error(f"BRUMBY: END OF send_sysex;") + #// Special Dispay Characters + # char0) bis cahr(31) order by Symbol_name + # char(32) to char(127) is like ASCII + # partly from https://pushmod.blogspot.com has no valid evmail adress, so I couldnt send him the updated list! + # login per google didnt work on his blog for safte reasons. What a pitty, I wanted to thank for his work with the complete list + # of symbos + + DISP_ARROW_UP = 0 # ↑ (U+2191) + DISP_ARROW_DOWN = 1 # ↓ (U+2193) + DISP_ARROW_RIGHT = 30 # → (U+2192) + DISP_ARROW_LEFT = 31 # ← (U+2190) + + DISP_HORIZONTAL_LINES_THREE_STACKED = 2 # ≡ (U+2261) + DISP_HORIZONRAL_LINE_LOW = 95 # _ (U+005F) Lowbar + DISP_HOIZONTAL_LINE_SPLIT = 6 # ╌ (U+254C) LIGHT DOUBLE DASH HORIZONTAL + + DISP_VERTICAL_LINE_AND_HORIZONTAL_LINE = 3 # ┤ (U+2524) + DISP_HORIZONTAL_LINE_AND_VERTICAL_LINE = 4 # ├ (U+251C) + + DISP_VERTICAL_LINES_TWO = 5 # ║ (U+2551) + DISP_VERTICAL_LINE_MID = 174 # | (U+007C) + DISP_SPLIT_VERTICAL_LINES = 8 # ⫼ (U+2AFC) + + DISP_FOLDER_SYMBOL = 7 # 📁 (U+1F4C1) + DISP_FLAT_SYMBOLS = 27 # ♭ (U+266D) + DISP_THREE_SIDE_BY_SIDE_DOTS = 28 # ⋮ (U+22EE) + DISP_FULL_BLOCK = 29 # █ (U+2588) + DISP_LITTLE_BOX_SHIFTED_HIGH_MIDDLE = 9 # ▫ (U+25AB) Little box shifted high middle + + DISP_AE_UC = 10 # Ä (U+00C4) + DISP_CEDILLE_UC = 11 # Ç (U+00C7) + DISP_OE_UC = 12 # Ö (U+00D6) + DISP_UE_UC = 13 # Ü (U+00DC) + DISP_SZ = 14 # ß (U+00DF) + DISP_A_GRAVE = 15 # à (U+00E0) + DISP_AE_LIC = 16 # ä (U+00E4) + DISP_CEDILE = 17 # ç (U+00E7) + DISP_E_LC_GRAVE = 18 # è (U+00E8) + DISP_E_LC_EGUT = 19 # é (U+00E9) + DISP_E_LC_CIRCUM = 20 # ê (U+00EA) + DISP_I_LC_TREMA = 21 # ï (U+00EF) + DISP_N_LC_WITH_TILDE = 22 # ñ (U+00F1) + DISP_OE_LC = 23 # ö (U+00F6) + DISP_DIV_STROKE = 24 # ⁄ (U+2044) + DISP_CIRC_WITH_DIV_STROKE = 25 # Ø (U+00D8) + DISP_UE_LC = 26 # ü (U+00FC) + + + # with 32 (SPACE) starts pritable part from ASCII-Table + akai_to_unicode = { + # Pfeile + 0: "↑", # DISP_ARROW_UP (U+2191) + 1: "↓", # DISP_ARROW_DOWN (U+2193) + 30: "→", # DISP_ARROW_RIGHT (U+2192) + 31: "←", # DISP_ARROW_LEFT (U+2190) - -# ------------------------------------------------------------------------------ - -#// Special Dispay Characters -##define UP_ARROW 0 -##define DOWN_ARROW 1 -##define THREE_STACKED_HORIZONTAL_LINES 2 -##define VERTICAL_LINE_AND_HORIZONTAL_LINE 3 -##define HORIZONTAL_LINE_AND_VERTICAL_LINE 4 -##define TWO_VERTICAL_LINES 5 -##define TWO_SIDE_BY_SIDE_HORIZONTAL_LINES 6 -##define FOLDER_SYMBOL 7 -##define SPLIT_VERTICAL_LINES 8 -##define FLAT_SYMBOLS 27 -##define THREE_SIDE_BY_SIDE_DOTS 28 -##define FULL_BLOCK 29 -##define RIGHT_ARROW 30 -##define LEFT_ARROW 31 - -class Feedback_Display: + # Horizontale Linien + 2: "≡", # DISP_HORIZONTAL_LINES_THREE_STACKED (U+2261) + 6: "╌", # DISP_HOIZONTAL_LINE_SPLIT (U+2550) + 95: "_", # DISP_HORIZONRAL_LINE_LOW (U+005F) # might not look same + + # Kombinierte Linien + 3: "┤", # DISP_VERTICAL_LINE_AND_HORIZONTAL_LINE (U+2524) + 4: "├", # DISP_HORIZONTAL_LINE_AND_VERTICAL_LINE (U+251C) + + # Vertikale Linien + 5: "║", # DISP_VERTICAL_LINES_TWO (U+2551) + 8: "⫼", # DISP_SPLIT_VERTICAL_LINES (U+2AFC) + 174: "|", # DISP_VERTICAL_LINE_MID (U+007C) # might not look same + + # Symbole + 7: "📁", # DISP_FOLDER_SYMBOL (U+1F4C1) + 27: "♭", # DISP_FLAT_SYMBOLS (U+266D) + 28: "⋮", # DISP_THREE_SIDE_BY_SIDE_DOTS (U+22EE) + 29: "█", # DISP_FULL_BLOCK (U+2588) + 9: "▫", # DISP_HIGH_LITTLE_BOX (U+25AB - Kleines hochgestelltes Kästchen) + + # Umlaute und Sonderzeichen + 10: "Ä", # DISP_AE_UC (U+00C4) + 11: "Ç", # DISP_CEDILLE_UC (U+00C7) + 12: "Ö", # DISP_OE_UC (U+00D6) + 13: "Ü", # DISP_UE_UC (U+00DC) + 14: "ß", # DISP_SZ (U+00DF) + 15: "à", # DISP_A_GRAVE (U+00E0) + 16: "ä", # DISP_AE_LC (U+00E4) + 17: "ç", # DISP_CEDILE (U+00E7) + 18: "è", # DISP_E_LC_GRAVE (U+00E8) + 19: "é", # DISP_E_LC_EGUT (U+00E9) + 20: "ê", # DISP_E_LC_CIRCUM (U+00EA) + 21: "ï", # DISP_I_LC_WITH_3_POINTS_ABOVE (U+00EF - i mit Trema) + 22: "ñ", # DISP_N_LC_WITH_TILDE (U+00F1) + 23: "ö", # DISP_OE_LC (U+00F6) + 24: "⁄", # DISP_DIV_STROKE (U+2044) + 25: "Ø", # DISP_CIRC_WITH_DIV_STROKE (U+00D8) + 26: "ü", # DISP_UE_LC (U+00FC) + } display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten # _disp_line_dirty =[False, False, False, False] From ba049fb6159ae1e5dce136337f42a91ede91486f Mon Sep 17 00:00:00 2001 From: JBrumby Date: Thu, 18 Sep 2025 00:05:47 +0200 Subject: [PATCH 34/57] Try to get an api documentation --- .github/workflows/pdoc.yml | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/pdoc.yml diff --git a/.github/workflows/pdoc.yml b/.github/workflows/pdoc.yml new file mode 100644 index 000000000..d4b7e1d5f --- /dev/null +++ b/.github/workflows/pdoc.yml @@ -0,0 +1,51 @@ +name: Generate pdoc Documentation for Zynthian UI (Oram Branch) + +on: + push: + branches: [ oram ] # Nur auf den 'oram'-Branch reagieren + workflow_dispatch: # Manuelles Auslösen erlauben + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: write # Erlaubt das Schreiben für GitHub Pages Deployment + + steps: + # 1. Repository auschecken (inkl. Submodules falls nötig) + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: oram # Spezifischer Branch + token: ${{ secrets.GITHUB_TOKEN }} + + # 2. Python einrichten + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" # Zynthian verwendet typischerweise Python 3.11 + cache: 'pip' + + # 3. Abhängigkeiten installieren (angepasst für Zynthian) + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pdoc + # Grundlegende Abhängigkeiten, die für Importe nötig sein könnten + pip install tornado pyyaml + + # 4. Dokumentation generieren (mit Fehlerumgehung) + - name: Generate documentation + run: | + # Versuche, eingeschränkte Dokumentation zu generieren + pdoc --html zynthian_main --output-dir ./docs-build --force || echo "pdoc fehlgeschlagen, aber wir fahren fort" + # Alternative: Nur bestimmte Module dokumentieren + pdoc --html zynthian_config --output-dir ./docs-build --force + + # 5. GitHub Pages deployment + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs-build + keep_files: false From 3daed6acaa171ce403de862b7e172cbc2f7f94e0 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Thu, 18 Sep 2025 00:23:16 +0200 Subject: [PATCH 35/57] Versuch fehler in der Workflow datei, zur online dokumentationserstellung zu beheben --- .github/workflows/pdoc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pdoc.yml b/.github/workflows/pdoc.yml index d4b7e1d5f..d45f43335 100644 --- a/.github/workflows/pdoc.yml +++ b/.github/workflows/pdoc.yml @@ -24,7 +24,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" # Zynthian verwendet typischerweise Python 3.11 - cache: 'pip' + cache: '' # 'pip' caching deactivated # 3. Abhängigkeiten installieren (angepasst für Zynthian) - name: Install dependencies From cff2f4fe23703148e4a0e4cbd6bc0445a87752f1 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Fri, 19 Sep 2025 00:10:48 +0200 Subject: [PATCH 36/57] Try to work in another branch to merge when ok. Working on mixer --- .../zynthian_ctrldev_ableton_push_1.py | 392 ++++++++++++++---- 1 file changed, 319 insertions(+), 73 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 47bd20a49..8a8f0b4de 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -28,12 +28,30 @@ # # ****************************************************************************** - - import logging import traceback -# Brumbys new mports + +#### just local debug +debug_mode = True +if debug_mode: + + # Eigenen Logger für Ihre Library erstellen + logger = logging.getLogger("ABL-Push_1") # Eindeutiger Name für Ihre Library + + # Nur für Ihren Logger Level setzen + logger.setLevel(logging.DEBUG) # Nur DIESER Logger zeigt Debug messages + + # Handler for your logger (optional) + handler = logging.StreamHandler() + formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + +### end of just local debug + + +# Brumbys new imports from time import sleep # pause between sysex events. import zyngine.ctrldev.ableton.push1_consts as ABL from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony @@ -62,7 +80,7 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer): - # logging.info("Class call") + logging.info("Push 1 initializes instance of class") # Weblog shows this messages # dev_ids = ["Ableton Push IN 2", "Ableton Push IN 1"] # get by stepping through zynthian_ctrldev_manager.load_driver() @@ -81,16 +99,16 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ STOPPING_COLOUR = 120 # RED RUNNING_COLOR = 3 # WHITE - + # equal vars are in base_extended... # evtype = (ev[0] >> 4) & 0x0F -> EV_NOTE_OFF = 0x8 # 3 Bytes EV_NOTE_ON = 0X9 # 3 Bytes EV_AFTERTOUCH = 0xA # 3 Bytes (polyphonic = per note) EV_CC = 0xB # 3 Bytes EV_PC = 0xC # 2 Bytes - EV_CHAN_PRESS = 0xD # 2 Bytes + EV_CHAN_PRESS = 0xD # 2 Bytes EV_PITCHBEND = 0xE # 3 bytes ev[1] = LSB 0-127; ev[2] = MSB 0-127 - EV_SYSTEM = 0xF # Systemtype = ev[0] & 0x0F + EV_SYSTEM = 0xF # varies from 1 to many Bytes ### Systemtype = ev[0] & 0x0F # dev_modes @@ -100,7 +118,10 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ DEV_MODE_MIXER = 3 # DEV_MODE_DRUMS = 2 # pad_mode_active = PAD_MODE_SEQ - device_mode_active = DEV_MODE_SCALES # initial mode + device_mode_active = DEV_MODE_NONE # initial mode + + ### would be nice to see on display if class is found + ### self._display = Feedback_Display(idev_out) # Text display scales = Harmony(8,8) scales.init_scale(tonic=0, @@ -111,18 +132,22 @@ class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_ # Function to initialise class def __init__(self, state_manager, idev_in, idev_out=None): - logging.info("Created Instance from Ableton Push 1 driver - BRUMBY") + logging.info("Found Push 1 on USB") + # would be nice to say, correct USB Device is found # super.__init__ saves state_manger, chainmanger, idev_in and idev_out # nothing more. + super().__init__(state_manager, idev_in, idev_out) + + # Indecators of the device LEDs and Text # NOT USED self._leds_mono = Feedback_Mono_LEDs(idev_out) # control buttons right and left from pads self._leds_bi = Feedback_Bi_LEDs(idev_out) # display buttons below display, above pads self._leds_rgb = Feedback_RGB_LEDs(idev_out) # pads in rgb self._display = Feedback_Display(idev_out) # Text display + self.mixer_init() # Text display for mixer # suerp()__init__ has to be called earlier to set idev_out - super().__init__(state_manager, idev_in, idev_out) # seems to be necessary, because we send translated midi_events. o self.unroute_from_chains = True @@ -133,20 +158,21 @@ def init(self): try: logging.info("called init. Setting up Ableton Push 1 - BRUMBY") self.shift = False + + # setup device screen + # self._display.first_screen() # set initial device mode - self.device_mode_active = self.DEV_MODE_SCALES - - # setup device screen - self._display.first_screen() + self.set_device_mode_new(self.DEV_MODE_MIXER) # setup LEDS in Ctrl-Buttons # Monochrome Tasten die hell leuchten sollen for t in [ 36,37,38,39,40,41,42,43, ABL.BTN_START[1], ABL.BTN_OK[1], ABL.BTN_ESC[1], ABL.BTN_LEFT[1], - ABL.BTN_RIGHT[1], ABL.BTN_UP[1], ABL.BTN_DOWN[1], ABL.BTN_SCALES[1] ]: - + ABL.BTN_RIGHT[1], ABL.BTN_UP[1], ABL.BTN_DOWN[1], ABL.BTN_SCALES[1], + ABL.BTN_USER[1] + ]: lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_LIT) # monochrome Buttons than should be dim state @@ -164,16 +190,16 @@ def init(self): super().init() # aktiviert. Muss aktiviert sein! # self.pads_off() if self.device_mode_active == self.DEV_MODE_SCALES: - self.set_dev_to_scales_mode() + self.scales_set_dev_to_scales_mode() #except: # print("Fehler aufgetreten: {e}") except Exception as e: print("Exception aufgetreten:") # Gibt den vollständigen Traceback aus - traceback.print_exc() - # logging.error("Exception aufgetreten: %s", e) - # logging.error("Traceback: %s", traceback.format_exc()) + # traceback.print_exc() + logger.error("Exception aufgetreten: %s", e) + logger.error("Traceback: %s", traceback.format_exc()) # called from parent def end(self): @@ -188,7 +214,7 @@ def end(self): # when changing to scales mode: start here # new in this class, to setup scales_mode = keyboard mode - def set_dev_to_scales_mode(self): + def scales_set_dev_to_scales_mode(self): self.device_mode_active = self.DEV_MODE_SCALES # visual feedback, let Scales Button blink lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) @@ -210,7 +236,7 @@ def set_dev_to_scales_mode(self): self._display.write_xy_mem(scale_n_mode, 0, 2) # Scale and scale over row2 self._display.write_xy_mem(btn_txt_row3, 0, 3) self._display.update_screen() - # set Display-Button_LEDS + # set PAD LEDS self.scale_update_leds(self.scales.tonic) # 0 is 'C' # Leaving scales mode: remove anything that is initailized @@ -427,32 +453,193 @@ def helper_set_new_tonic(tonic): ################## END of scales fucntions ########################################################## ############################################################################################################### +####################################################################################### +### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. ### + + def mixer_helper_bar(self, value) -> str: + field_width = 10 # width of anzeige + int_val = int(value*10) + if float(value) > 0.0: # always minimum 1 bar if any sound! + int_val += 1 + erg = "".ljust(int_val,"|").ljust(10) + erg = "".ljust(int(value*10),"|").ljust(15)[:field_width] + return erg -### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. -### just copy the derived functions in the this driver and implement them accordingly + def mixer_helper_write_to_knobx_fieldy(self, text: any, knob_x:int, field_y:int, as_bar:bool = False): + """writes to a specified place below a knob + knob_x is the knob from 0 to 7 (push_1 has 9 knobs, but nith has no display) + field_y is the row written to see consts: _MIXER_DISP_ROW_* + text can be text or float value + """ + if knob_x > 7: + knob_x = 7 + logging.error("knob_x bigger 7 not implemented. Sum channel is directed to 7") + if isinstance(text, (int, float)): # when text of type float; change it to str + if as_bar: + text = self.mixer_helper_float_to_ascii_Bar(text) + else: + text = str(text) + + fields_start_knobs = [0, 9, 18, 26, 35, 42, 51, 60] + knobx_start = fields_start_knobs[knob_x] + text=text.ljust(10)[:10] # make text with minimal 10 and max 10 chars + self._display_mixer.write_xy_mem(text, knobx_start, field_y) + + def mixer_helper_float_to_ascii_Bar(self, value:float): + fieldlen = 8 + int_val = int(value * fieldlen) # val is 0.0 to 1.0. we want range 0-7 + return "".ljust(int_val, ">").ljust(fieldlen) # fill up with spaces to overwrite old values + + def mixer_init(self): + """mixer display functions are called during start. + Mixer is main functionality, so it hast to be setup in intit _function + """ + # create consts for mixer display + self.MIXER_DISP_ROW_VOLUME = 0 + self.MIXER_DISP_ROW_BALANCE = 1 + self.MIXER_DISP_ROW_3 = 2 + self.MIXER_DISP_ROW_4 = 3 + + self._display_mixer = Feedback_Display(self.idev_out); + self.mixer_set_dev_to_mixermode() + + return + + + def mixer_set_dev_to_mixermode(self): + + # creat private mixer display + + btn_txt_row0 = "| Ch 1 | Ch 2 | Ch 33 | Ch 4 || Ch 5 | Ch 6 | Ch 7 | Ch 8 |" + btn_txt_row1 = " | This is the Mixer Display " + btn_txt_row2 = "|modes here | | || | | | |" + btn_txt_row3 = "| | | | || | | | |" + + btn_text_row2 = self._display_mixer.format_help + + self._display_mixer.write_xy_mem(btn_txt_row0, 0, 0) + self._display_mixer.write_xy_mem(btn_txt_row1, 0, 1) + self._display_mixer.write_xy_mem(btn_txt_row2, 0, 2) + self._display_mixer.write_xy_mem( "Volume Mode".ljust(20)[:20], 0, 2 ) # Mode of knobs + self._display_mixer.write_xy_mem(btn_txt_row3, 0, 3) + self._display_mixer.update_screen() + + # paint into tha test data + ch1_level = self.zynmixer.zctrls[0]['level'].get_value() # is this a set level or the real sound lovel + ch1_level_bar = self.mixer_helper_float_to_ascii_Bar(ch1_level) + first_knob_nr = 0 + self.mixer_helper_write_to_knobx_fieldy(ch1_level_bar, first_knob_nr, self.MIXER_DISP_ROW_VOLUME) + + self._display_mixer.update_screen()# send display_data to display + + + def mixer_cleanup(self): + pass + + ### just copy the derived functions in the this driver and implement them accordingly # DONT CHANGE FUNC NAME (is inherited) def update_mixer_active_chain(self, active_chain): """Update hardware indicators for active_chain""" - logging.error(f"not implemented active_chain: {active_chain}") - + + try: + mix_state = self.zynmixer.get_state() + volume = self.zynmixer.zctrls[0]['level'].get_value() + for c in mix_state.keys(): + if c[:5] == "chan_": + chan_nr = int(c[5:7]) + ch_level = self.zynmixer.zctrls[chan_nr]['level'].get_value() # we use level from here, so we no field exists + ch_level_bar = self.mixer_helper_float_to_ascii_Bar(ch_level) + self.mixer_helper_write_to_knobx_fieldy(ch_level_bar, chan_nr, self.MIXER_DISP_ROW_VOLUME) + + # write names + # Check if chain exists + # if zynmixer.get_chain_level(chain_index) is not None: + # Namen von der Engine holen + #engine_index = self.zynmixer.get_chain_engine(chan_nr) + #engine_info = lib_zyncore.get_engine_info(engine_index) + # name = engine_info.get('name', f"CH{chan_nr}") + # name = self.zynmixer.get_chain_name(chan_nr) + # name = self.zynmixer. + self._display_mixer.update_screen() + # logging.error(f"not implemented active_chain: {active_chain}") + return + except Exception as e: + logging.error(f"Error in update_mixer_active_chain: {e}") + logging.exception(traceback.format_exc()) + + + + + # DONT CHANGE FUNC NAME (is inherited) def update_mixer_strip(self, chan, symbol, value): - """Update hardware indicators for a mixer strip: mute, solo, level, balance, etc. # oh my goodness, what means etc. ? + """Update hardware indicators for a mixer strip: mute, solo, level, balance, etc. *SHOULD* be implemented by child class chan - Mixer strip index symbol - Control name value - Control value - - Idiea for display - ||||||||||| = lefel indicator - M S L B = M=Mute; S=Solo L=changing the Lefel; B=changing balance; But what else ??? """ - logging.debug( - f"Update mixer strip for {type(self).__name__}: NOT IMPLEMENTED! chan: {chan}; symbol: {symbol} value: {value}") + + try: + match symbol: + case 'level': + if chan > 7: + chan = 7 + self.mixer_helper_write_to_knobx_fieldy(value, chan, self.MIXER_DISP_ROW_VOLUME, as_bar=True) + self._display_mixer.update_screen() + return # Wichtig: Return nach erfolgreicher Verarbeitung! + + case 'balance': + # Implementierung für balance + pass + + case 'mute': + # Implementierung für mute + pass + + case 'solo': + # Implementierung für solo + pass + + case 'mono': + # Implementierung für mono + pass + + case 'm+s': # Mono / Stereo + # Implementierung für m+s + pass + + case 'phase': + # Implementierung für phase + pass + + case _: + # Fall für unbekannte symbols + logging.debug( + f"Update mixer strip for {type(self).__name__}: UNKNOWN SYMBOL! chan: {chan}; symbol: {symbol} value: {value}") + return + + # Diese Zeile wird nur erreicht, wenn ein Case gematcht aber nicht behandelt wurde + logging.debug( + f"Update mixer strip for {type(self).__name__}: NOT IMPLEMENTED! chan: {chan}; symbol: {symbol} value: {value}") + + except Exception as e: + logging.error(f"Error in update_mixer_strip: {e}") + logging.exception(traceback.format_exc()) + + def process_mixer_event(self, ev) -> bool: + #self.zyn + #self.zynmixer.setlevel() + pass + + +### END of Mixer functions. ### +######################################################################### -### END of Mixer functions. +######################################################################### +### Start of Sequencer / Pad Functions ### def process_sequencer_event(self, ev) -> bool: """event function in sequencer state""" # if using shift button with knob, then not following we are not in any mode @@ -460,9 +647,8 @@ def process_sequencer_event(self, ev) -> bool: # return False # we ignored here any event, we are not in Sequencer mode -############################################################################################################## -################ Start of SEQUENCER FUNCTIONS ######################################################### - # this function is called by zynseq when a sequencer state is changed + + # tzynseq updates LED states # we have update pad LED to show state # DONT CHANGE FUNC NAME (is inherited) def update_seq_state(self, bank, seq, state, mode, group): @@ -636,58 +822,112 @@ def _forward_like_niels_did(self, ev): # return super()._on_midi_event(ev)` def set_device_mode_new(self, new_mode): - match self.device_mode_active: - case self.DEV_MODE_MIXER: - # deinit mixer - pass - case self.DEV_MODE_PAD: - # deinit scales - pass - case self.DEV_MODE_SCALES: - # deinit scales mode - pass - self.device_mode_active = new_mode - match new_mode: - case self.DEV_MODE_MIXER: - # init mixer - pass - case self.DEV_MODE_PAD: - # init scales - pass - case self.DEV_MODE_SCALES: - # init scales mode - pass - case _: - # code not defined - logging.error("DEVICE Mode not defined. Programming Error") + try: + if new_mode == self.device_mode_active: return # devmode was same + # clean up old device state + # NO USE RETURNS + match self.device_mode_active: + case self.DEV_MODE_MIXER: + # deinit mixer + lib_zyncore.dev_send_ccontrol_change( + self.idev_out, 0, ABL.BTN_VOLUME[1], ABL.MONO_LED_LIT) + self.mixer_cleanup() + + case self.DEV_MODE_PAD: + # there is no cleanup. do following + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_USER[1], ABL.MONO_LED_LIT) + self.pads_off() + + case self.DEV_MODE_SCALES: + # deinit scales mode + self.scales_cleanup() + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) + self.pads_off() # clean up visible state. all pad leds off + + # now you can save new active mode + self.device_mode_active = new_mode + + # Now Setup new device mode + # HERE USE RETURNS + match new_mode: + case self.DEV_MODE_MIXER: + # init mixer + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_VOLUME[1], ABL.MONO_LED_LIT_BLINK) + return self.mixer_set_dev_to_mixermode() + + case self.DEV_MODE_PAD: + self.refresh() # refreshe LEDs for Sequencer mode of this driver. + # there is no clean_up method. so do following + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_USER[1], ABL.MONO_LED_LIT_BLINK) + + case self.DEV_MODE_SCALES: + self.scales_set_dev_to_scales_mode() + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) + + + case _: + # code not defined + logging.error("DEVICE Mode not defined. Programming Error") + + # if not done till now, mark as succed + return True # Whatever event is processed + + except Exception as e: + logger.error(f"Error in set_device_mode_new: {e}") + logger.exception(traceback.format_exc()) + + + + def midi_event(self, ev): ### For debugging purposes block can be commented out ! + dbg = True + if len(ev) > 1 and dbg: + search_key = [ev[0], ev[1]] # ev to search_key + btn_name = self.button_name_from_midi_event(search_key) # ev[0] and ev[1] fields are proved. so any status can be a button + if not btn_name == "": # just log known btns + logger.debug(f"Button: {btn_name} on chan. {ev[0] & 0x0F} gives midi_event: {hex(ev[0])} {hex(ev[1])} {hex(ev[2])} = {int(ev[1])}, {int(ev[2])}, {int(ev[2])}") + evtype = None chan_or_instruction = None note_or_register = None val_or_vel = None - if len(ev) > 1: + ### end of debug + + if len(ev) > 1: # Btn is possible + + if len(ev) > 2: + val_or_vel = ev[2] + is_key_push = val_or_vel > 0 + search_key = [ev[0], ev[1]] # ev to search_key match search_key: case None: pass - case ABL.BTN_SHIFT: # as momentary button ! hasto be hold for functions change - self.shift = val_or_vel != 0 # set shift variable. but just momenatary + case ABL.BTN_SHIFT: # as momentary button NOT toggle! has to be hold for functions change + self.shift = is_key_push # set shift variable. but just momenatary # visual feedback with button LED if self.shift: lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_LIT_BLINK) - else: + else: # key is teleased lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_DIM) return True # event processed. No further action required case ABL.BTN_VOLUME: # mode change to mixer? It isn't best chosen. - return self.set_device_mode_new(self.DEV_MODE_MIXER) - - pass + if is_key_push: + return self.set_device_mode_new(self.DEV_MODE_MIXER) + + case ABL.BTN_SCALES: + if is_key_push: + return self.set_device_mode_new(self.DEV_MODE_SCALES) + + case ABL.BTN_USER: + if is_key_push: + return self.set_device_mode_new(self.DEV_MODE_PAD) case _: pass @@ -708,6 +948,8 @@ def midi_event(self, ev): # now the Gui events. # Gui events moved to: if self.process_gui_events(ev): return True + + # nothing below the line ??? return False # that should be all if len(ev) > 0: @@ -725,8 +967,7 @@ def midi_event(self, ev): button_ev == [0x90, ev[1]] aftertouch = 'aftertouch' - btn_name = self.button_name_from_midi_event(button_ev) # ev[0] and ev[1] fields are proved. so any status can be a button - + btn_name = self.button_name_from_midi_event(button_ev) # ev[0] and ev[1] fields are proved. so any status can be a button logging.debug(f"Button: {btn_name} on chan. {chan_or_instruction} gives midi_event: {aftertouch} {hex(ev[0])} {hex(ev[1])} {hex(val_or_vel)} = {evtype}, {note_or_register} {val_or_vel}") ### End of "debugging purposes." @@ -1035,6 +1276,9 @@ class Feedback_Display: 26: "ü", # DISP_UE_LC (U+00FC) } + format_help = b'123456789A123456789B123456789C123456789D123456789E123456789F123456789' + + display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten # _disp_line_dirty =[False, False, False, False] @@ -1095,8 +1339,10 @@ def write_xy_mem (self, text, col_in:int, row_in:int): elif isinstance(text, bytes): # print("Die Variable ist Bytes") pass # is fine + elif isinstance(text, (int, float)): # is a number? + text = str(text).encode() else: - # print("Die Variable ist weder String noch Bytes") + # print("type error") text = "Typeerror in textconversion".encode() # Koordinaten prüfen @@ -1184,9 +1430,9 @@ def first_screen(self): self.clear() # self.brightnes(36) sleep(0.1) - self.write_xy_mem(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) + #self.write_xy_mem(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) # Positionierungshilfe - self.write_xy_mem(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) + #self.write_xy_mem(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) self.write_xy_mem(b'** Zynthian Push1Driver 0.1 **', 17,2) self.write_xy_mem(b'++ Make MusicNot War ++', 20,3) self.update_screen() From 22e640fb05fa41df0c67a6ede407f164306dbcf5 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Fri, 19 Sep 2025 00:28:25 +0200 Subject: [PATCH 37/57] removed this try. Not working because this github is just sub --- .github/workflows/pdoc.yml | 51 -------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 .github/workflows/pdoc.yml diff --git a/.github/workflows/pdoc.yml b/.github/workflows/pdoc.yml deleted file mode 100644 index d45f43335..000000000 --- a/.github/workflows/pdoc.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Generate pdoc Documentation for Zynthian UI (Oram Branch) - -on: - push: - branches: [ oram ] # Nur auf den 'oram'-Branch reagieren - workflow_dispatch: # Manuelles Auslösen erlauben - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - permissions: - contents: write # Erlaubt das Schreiben für GitHub Pages Deployment - - steps: - # 1. Repository auschecken (inkl. Submodules falls nötig) - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: oram # Spezifischer Branch - token: ${{ secrets.GITHUB_TOKEN }} - - # 2. Python einrichten - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" # Zynthian verwendet typischerweise Python 3.11 - cache: '' # 'pip' caching deactivated - - # 3. Abhängigkeiten installieren (angepasst für Zynthian) - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pdoc - # Grundlegende Abhängigkeiten, die für Importe nötig sein könnten - pip install tornado pyyaml - - # 4. Dokumentation generieren (mit Fehlerumgehung) - - name: Generate documentation - run: | - # Versuche, eingeschränkte Dokumentation zu generieren - pdoc --html zynthian_main --output-dir ./docs-build --force || echo "pdoc fehlgeschlagen, aber wir fahren fort" - # Alternative: Nur bestimmte Module dokumentieren - pdoc --html zynthian_config --output-dir ./docs-build --force - - # 5. GitHub Pages deployment - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs-build - keep_files: false From 1a75f6b6816e3d84bcc32ce13d9a51caac38d144 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Fri, 19 Sep 2025 10:18:21 +0200 Subject: [PATCH 38/57] mixer not ready. but on a way to get working. --- .../zynthian_ctrldev_ableton_push_1.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 8a8f0b4de..3a943437b 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -456,14 +456,14 @@ def helper_set_new_tonic(tonic): ####################################################################################### ### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. ### - def mixer_helper_bar(self, value) -> str: - field_width = 10 # width of anzeige - int_val = int(value*10) - if float(value) > 0.0: # always minimum 1 bar if any sound! - int_val += 1 - erg = "".ljust(int_val,"|").ljust(10) - erg = "".ljust(int(value*10),"|").ljust(15)[:field_width] - return erg + # def mixer_helper_bar(self, value) -> str: + # field_width = 8# width of anzeige + # int_val = int(value * field_width) + # if float(value) > 0.0: # always minimum 1 bar if any sound! + # int_val += 1 + # erg = "".ljust(int_val,chr(6)).ljust(10)[:field_width] + # # erg = "".ljust(int(value*10),"|").ljust(15)[:field_width] + # return erg def mixer_helper_write_to_knobx_fieldy(self, text: any, knob_x:int, field_y:int, as_bar:bool = False): """writes to a specified place below a knob @@ -474,21 +474,21 @@ def mixer_helper_write_to_knobx_fieldy(self, text: any, knob_x:int, field_y:int, if knob_x > 7: knob_x = 7 logging.error("knob_x bigger 7 not implemented. Sum channel is directed to 7") - if isinstance(text, (int, float)): # when text of type float; change it to str + if isinstance(text, (int, float)): # when text of type float or int change it to str if as_bar: text = self.mixer_helper_float_to_ascii_Bar(text) else: text = str(text) - fields_start_knobs = [0, 9, 18, 26, 35, 42, 51, 60] + fields_start_knobs = [0,9, 17,26, 34,43, 51,60] knobx_start = fields_start_knobs[knob_x] - text=text.ljust(10)[:10] # make text with minimal 10 and max 10 chars + text=text.ljust(8)[:8] # make text with minimal 10 and max 10 chars self._display_mixer.write_xy_mem(text, knobx_start, field_y) def mixer_helper_float_to_ascii_Bar(self, value:float): fieldlen = 8 int_val = int(value * fieldlen) # val is 0.0 to 1.0. we want range 0-7 - return "".ljust(int_val, ">").ljust(fieldlen) # fill up with spaces to overwrite old values + return "".ljust(int_val, chr(6)).ljust(fieldlen) # fill up with spaces to overwrite old values def mixer_init(self): """mixer display functions are called during start. @@ -511,16 +511,16 @@ def mixer_set_dev_to_mixermode(self): # creat private mixer display btn_txt_row0 = "| Ch 1 | Ch 2 | Ch 33 | Ch 4 || Ch 5 | Ch 6 | Ch 7 | Ch 8 |" - btn_txt_row1 = " | This is the Mixer Display " - btn_txt_row2 = "|modes here | | || | | | |" - btn_txt_row3 = "| | | | || | | | |" + btn_txt_row1 = " | This is the Mixer Display " + btn_txt_row2 = f"|modes here {chr(5)} {chr(6)} {chr(5)}{chr(6)} | |" + btn_txt_row3 = "| | | | || | | | |" btn_text_row2 = self._display_mixer.format_help self._display_mixer.write_xy_mem(btn_txt_row0, 0, 0) self._display_mixer.write_xy_mem(btn_txt_row1, 0, 1) self._display_mixer.write_xy_mem(btn_txt_row2, 0, 2) - self._display_mixer.write_xy_mem( "Volume Mode".ljust(20)[:20], 0, 2 ) # Mode of knobs + self._display_mixer.write_xy_mem( "Volume Mode".ljust(20)[:15], 0, 2 ) # Mode of knobs self._display_mixer.write_xy_mem(btn_txt_row3, 0, 3) self._display_mixer.update_screen() @@ -1192,7 +1192,7 @@ class Feedback_Display: DISP_ARROW_LEFT = 31 # ← (U+2190) DISP_HORIZONTAL_LINES_THREE_STACKED = 2 # ≡ (U+2261) - DISP_HORIZONRAL_LINE_LOW = 95 # _ (U+005F) Lowbar + DISP_HORIZONTAL_LINE_LOW = 95 # _ (U+005F) Lowbar DISP_HOIZONTAL_LINE_SPLIT = 6 # ╌ (U+254C) LIGHT DOUBLE DASH HORIZONTAL DISP_VERTICAL_LINE_AND_HORIZONTAL_LINE = 3 # ┤ (U+2524) From 7ea00baf63c310a34e3364a5020d43884fd95f1d Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 20 Sep 2025 00:19:37 +0200 Subject: [PATCH 39/57] cleaned up meide_event from unneeded code. integrate Knob_ease to gui-knobs --- .../zynthian_ctrldev_ableton_push_1.py | 145 ++++-------------- 1 file changed, 27 insertions(+), 118 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 3a943437b..d30bb2590 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -139,7 +139,9 @@ def __init__(self, state_manager, idev_in, idev_out=None): # nothing more. super().__init__(state_manager, idev_in, idev_out) - + # to slow knob-events.translates 127 to -1 + # TODO experiment with setup values when live + self._knobs_ease = KnobSpeedControl() # Indecators of the device LEDs and Text # NOT USED self._leds_mono = Feedback_Mono_LEDs(idev_out) # control buttons right and left from pads @@ -944,6 +946,10 @@ def midi_event(self, ev): return True case _: pass # no actual devicemode + + # if nothing els then + #if self.process_scale_event(ev): + # return True # now the Gui events. # Gui events moved to: @@ -951,114 +957,7 @@ def midi_event(self, ev): # nothing below the line ??? return False # that should be all - - if len(ev) > 0: - evtype = (ev[0] >> 4) & 0x0F - chan_or_instruction = ev[0] & 0xF - if len(ev) > 1: - note_or_register = ev[1] & 0x7F - if len(ev) > 2: - val_or_vel = ev[2] & 0x7F - - if note_or_register: # len > 1 -> Button / Pad detection is possible - button_ev = ev - aftertouch = '' - if button_ev[1] == 0xa0: # EV_AFTERTOUCH: - button_ev == [0x90, ev[1]] - aftertouch = 'aftertouch' - - btn_name = self.button_name_from_midi_event(button_ev) # ev[0] and ev[1] fields are proved. so any status can be a button - logging.debug(f"Button: {btn_name} on chan. {chan_or_instruction} gives midi_event: {aftertouch} {hex(ev[0])} {hex(ev[1])} {hex(val_or_vel)} = {evtype}, {note_or_register} {val_or_vel}") - - ### End of "debugging purposes." - - # don't process 1-byte events. - if len(ev)<2: - return False - - # processing starts here - evtype = (ev[0] >> 4) & 0x0F - note = ev[1] & 0x7F # is that need? any event field is from 0-127 except the status field - - ### Scale mode - # if self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected - # if self.process_scale_event(ev): - # return True # return value cuts out follwing - - - # pad mode to control sequencer - # elif self.device_mode_active == self.DEV_MODE_PAD: - # if self.process_sequencer_event(ev): - # return True - - # I think we dont need any note events... so filter out - # NO note events anymore after this two lines. Comment out what you need after this lines in "Pad Mode" = DEV_MODE_PAD - #### # if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH, self.EV_PITCHBEND]: - # return True - - # no return call. I don't know if I need note_on_events further down. - - - # if prceessd before there are just note_events lower PAD_START and higher PAD_END if processed before - - # GUI Control Changes - # evtype = EV_CC - if evtype == 0xB: - ccnum = ev[1] & 0x7F - ccval = ev[2] & 0x7F - - # Sate of shoft button CC49 wird abgefragt. CCVall > 0 means pressed - if ccnum == ABL.BTN_SHIFT[1]: - # if ccnum == 49: - # SHIFT - self.shift = ccval != 0 # set shift variable - # visual feedback with button LED - if self.shift: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_LIT_BLINK) - else: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_DIM) - return True # event processed. No further action required - - # From here filter any event with velocyty=0 We just need notepressed values to come through - elif ccnum == 0 or ccval == 0: # is that midi bank change? - return False # Warning: With "return True" no further processing in zynthian. - # So no Controlchange with data=0 gets through to zynthian. - # Is that, what we want ??? - # Also bank changes are msb or isit LSB are filtered waay. - # I assume, it has to return False, so Zynthian can do bank chanages !!! - - # From here only positive Values are processed! - - # Displays bi-color Buttons - elif (self.shift and 20 < ccnum < 29) or (20 < ccnum < 25): - chain = self.chain_manager.get_chain_by_position(ccnum - 21, midi=False) - if chain and chain.mixer_chan is not None and chain.mixer_chan < 17: - self.zynmixer.set_level(chain.mixer_chan, ccval / 127.0) # "/127.0" creates a float val from 0.0 .. 1.0 - - # This swtches between this drivers pad states: Pad (Sequencer) and Scales - elif (ccnum == ABL.BTN_SCALES[1]): - logging.info("BRUMBY: BTN_SCALES processing") - if not self.device_mode_active == self.DEV_MODE_SCALES: - self.set_dev_to_scales_mode() - else: - self.device_mode_active = self.DEV_MODE_PAD - self.scales_cleanup() - # visual feedback, set LED to solid on - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) - self.pads_off() # clean up visible state. all pad leds off - self.refresh() # refreshe LEDs for Sequencer mode of this driver. - return True - - - # Gui events moved to: - if self.process_gui_events(ev): return True - - - - - # default return, when no match - return False # When nothing matches, False shows that midi event has to be processed further - + ######### GUI EVENTS #################################### @@ -1073,23 +972,33 @@ def process_gui_events(self,ev) -> bool: search_key = [ev[0], ev[1]] data_val = ev[2] & 0x7F - # make left turs on knobs negative - def helper_knob_calculation(ccval): - if ccval > 64: ccval -= 128 - return ccval - data_val_for_knobs = helper_knob_calculation(data_val) + # TODO remove if knob ease is fine + # # make left turs on knobs negative + # def helper_knob_calculation(ccval): + # if ccval > 64: ccval -= 128 + # return ccval + # # this could be changed to + # # delta = self._knobs_ease.feed(btn_id, ev[2], self._is_shiftedxxx) + # data_val_for_knobs = helper_knob_calculation(data_val) match search_key: # Knobs case ABL.KNOB_1: - self.state_manager.send_cuia("ZYNPOT", [0, data_val_for_knobs]); return True + # translate 127 to -1 and slow down + delta = self._knobs_ease.feed(bytes(ABL.KNOB_1), data_val, is_shifted=False) + self.state_manager.send_cuia("ZYNPOT", [0, delta]); return True + # self.state_manager.send_cuia("ZYNPOT", [0, data_val_for_knobs]); return True case ABL.KNOB_2: - self.state_manager.send_cuia("ZYNPOT", [1, data_val_for_knobs]); return True + delta = self._knobs_ease.feed(bytes(ABL.KNOB_1), data_val, is_shifted=False) + self.state_manager.send_cuia("ZYNPOT", [1, delta]); return True case ABL.KNOB_3: - self.state_manager.send_cuia("ZYNPOT", [2, data_val_for_knobs]); return True + delta = self._knobs_ease.feed(bytes(ABL.KNOB_3), data_val, is_shifted=False) + self.state_manager.send_cuia("ZYNPOT", [2, delta]); return True case ABL.KNOB_4: - self.state_manager.send_cuia("ZYNPOT", [3, data_val_for_knobs]); return True + delta = self._knobs_ease.feed(bytes(ABL.KNOB_4), data_val, is_shifted=True) + self.state_manager.send_cuia("ZYNPOT", [3, delta]); return True + # self.state_manager.send_cuia("ZYNPOT", [3, data_val_for_knobs]); return True case _: pass if data_val > 0: # just key-down events From 8a2e193a97b74254589a7fe022af6364326253f0 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 20 Sep 2025 01:28:27 +0200 Subject: [PATCH 40/57] octave up and down buttons integrated --- .../zynthian_ctrldev_ableton_push_1.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index d30bb2590..cc1984c2e 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -159,10 +159,9 @@ def __init__(self, state_manager, idev_in, idev_out=None): def init(self): try: logging.info("called init. Setting up Ableton Push 1 - BRUMBY") - self.shift = False - - # setup device screen - # self._display.first_screen() + self.shift = False # BTN_SHIFT is pressed + self.shift_note = 0 # Octave buttons + # set initial device mode self.set_device_mode_new(self.DEV_MODE_MIXER) @@ -240,6 +239,10 @@ def scales_set_dev_to_scales_mode(self): self._display.update_screen() # set PAD LEDS self.scale_update_leds(self.scales.tonic) # 0 is 'C' + # set up buttons + for t in [ABL.BTN_OCTAVE_DOWN[1], ABL.BTN_OCTAVE_UP[1]]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_LIT) + # Leaving scales mode: remove anything that is initailized def scales_cleanup(self): # set of any LED and display changes @@ -260,6 +263,9 @@ def scales_cleanup(self): # set of any LED and display changes ] for t in scale_buttons: lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_LED_OFF) + # set up buttons + for t in [ABL.BTN_OCTAVE_DOWN[1], ABL.BTN_OCTAVE_UP[1]]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_OFF) @@ -357,6 +363,8 @@ def process_scale_event(self, ev) -> bool: if not self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected return False # we are not in scales mode + + ##### event part for sounds # Filter out note events created by push 1 when touching Knobs and Ribbon note = ev[1] @@ -378,6 +386,10 @@ def process_scale_event(self, ev) -> bool: # here magic for different sccale layouts happens. # it translates midi_note events to the translated note_events note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes are based 0 pad_nr based 1 + + # ocatve_buttons used + note_translated += self.shift_note + vel = ev[2] # my push1 is insensitive so I double any velocity val if evtype == self.EV_NOTE_ON: vel = ev[2] *2 # this creates junk with aftertouch und pitchbend.. @@ -444,6 +456,12 @@ def helper_set_new_tonic(tonic): self.scales_set_tonic(ev[2]); return True case ABL.KNOB_8: # mode self.scales_set_mode(ev[2]); return True + + # Octave Buttons + case ABL.BTN_OCTAVE_UP: + self.shift_note += 12 + case ABL.BTN_OCTAVE_DOWN: + self.shift_note -= 12 case _: return False # event not for any of the defined buttons From 584ebf88334301918edc9a558bea2842cbb2f37f Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 20 Sep 2025 19:27:18 +0200 Subject: [PATCH 41/57] cleanup --- .../ctrldev/zynthian_ctrldev_base_scale.py | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index ba9524320..22aa13b13 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -51,7 +51,11 @@ class Harmony: modes = _MODES scales = _SCALES - def __init__(self, pad_cols, pad_rows): + def __init__(self, pad_cols=8, pad_rows=8): + """pad_cols : number of cols of the pad array + pad_rows : number of rows of the pad array + mostly 8 by 8 + """ self.cols = pad_cols self.rows = pad_rows self.target_notes = [] # Instance variable @@ -76,18 +80,22 @@ def must_reset_led_colors(self) -> bool: return self.must_redraw_led_colors def init_scale(self, - tonic: int = None, # (C) semitone distance counted from from C = 0 - mode_name: str = None, # mode as str. look in _SCALES - col_versatz: int = None, # per row recess - middle_c: int = None, # must be middle_c % 12 = 60 - middle_pad_nr: int = None): # padnr of middle tonic. pad where middle_c is placed + tonic: int = 0, # (C) semitone distance counted from from C = 0 + mode_name: str = "Major", # mode as str. look in _SCALES + col_versatz: int = -5, # per row recess + middle_c: int = 48, # must be middle_c % 12 = 60 + middle_pad_nr: int = 5): # padnr of middle tonic. pad where middle_c is placed + + """Defaults set: C Major with next row is sub_dominant to row before. Middle_C is on 5th pad - """tonic : tonic of scale as midinote 0-11 (semitones) - mode_name: name of mode from self._modes - middle_C: number of the tone in scale with octaves (12 would be second octave tonic) - col_versatz: each row can start with a different offset, so -5 means in C-Major-Scale an "F" above the C in row-1 line + tonic : tonic of scale as 0 <= semitones <= 11 (semitones) + mode_name: name of mode from self._modes + col_versatz: shift next row by col_versatz steps. e.g. -5 makes sub_dominant above tonic + middle_c: must be real midi notd c (c % 12 == 0). this will later be shifted by the + tonic value 60 as middle c has to be % 12 == 0 + middle_pad_nr: which pad should be middle of your pad array """ - # for new tonic initialization is not necessary. Tonics just change returnvlaiues of notes + # for new tonic initialization is not necessary. Tonics just changes return values of notes if not tonic is None: if tonic > 11: tonic = 0; if tonic < 0: tonic = len(self.scales)-1 @@ -194,6 +202,7 @@ def init_scale(self, return def set_new_tonic(self, new_tonic:int): + """new_tonic is next tonic in selected scale""" if new_tonic == self.tonic: return False if new_tonic < 0: new_tonic = 11 # target: B if new_tonic > 11: new_tonic = 0 # target: C @@ -201,6 +210,7 @@ def set_new_tonic(self, new_tonic:int): return True # yes update display. we changed it def step_to_next_tonic(self, step): + """For Knob Control. 127 converted -1""" if step > 63: step -=128 # for controller sending 127 for -1 new_tonic = self.scales.tonic + step if new_tonic < 0: new_tonic = 11 # target: B @@ -215,7 +225,7 @@ def is_tonic_by_padnr(self, pad_nr:int)-> bool: def is_tonic_by_midnote(self, midi_note:int) -> bool: - return (midi_note - self.tonic) % 12 + return (midi_note - self.tonic) % 12 == 0 def get_equi_sound_pads_with_midi_note(self, midi_note) -> list: # Subtract tonic to get internal representation @@ -238,6 +248,7 @@ def harmony_get_mode_names(self): return list(self.modes) def harmony_get_scale_name_with_mode (self) -> str: + """actual scale and mode as string for display""" result = self.scales[self.tonic] + ' ' + self.active_mode result = result.ljust(20)[:20] return result From 8e221f4b2a7c779107f2758583c2e6b539954cbf Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 20 Sep 2025 19:32:02 +0200 Subject: [PATCH 42/57] worked on mixer_state: 8 Volume Knobs are working --- .../zynthian_ctrldev_ableton_push_1.py | 239 +++++++++++++----- 1 file changed, 177 insertions(+), 62 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index cc1984c2e..8839491ad 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -28,6 +28,10 @@ # # ****************************************************************************** + +# Display: Top two rows mixer +# bottm two for rows scales / Sequencer display + import logging import traceback @@ -221,18 +225,18 @@ def scales_set_dev_to_scales_mode(self): lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) self.pads_off() # akk pad leds off self.scales_set_pad_colors() # set LEDs for scale mode - self._display.clear() + # self._display.clear() scale_n_mode = self.scales.harmony_get_scale_name_with_mode() self._display.write_xy_mem(scale_n_mode, 0, 2) - btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | Scale | Mode |" + #btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | Scale | Mode |" # btn_txt_row1 = f"| {chr(12)} {chr(11)} {chr(10)} | {chr(9)} {chr(8)} {chr(7)} {chr(6)} | | |" - btn_txt_row1 = " " + #btn_txt_row1 = " " btn_txt_row2 = "|modes here | | || G# | A | A# | B |" btn_txt_row3 = "| C | C# | D | D# || E | F | F# | G |" - self._display.write_xy_mem(btn_txt_row0, 0, 0) - self._display.write_xy_mem(btn_txt_row1, 0, 1) + #self._display.write_xy_mem(btn_txt_row0, 0, 0) + #self._display.write_xy_mem(btn_txt_row1, 0, 1) self._display.write_xy_mem(btn_txt_row2, 0, 2) self._display.write_xy_mem(scale_n_mode, 0, 2) # Scale and scale over row2 self._display.write_xy_mem(btn_txt_row3, 0, 3) @@ -242,15 +246,16 @@ def scales_set_dev_to_scales_mode(self): # set up buttons for t in [ABL.BTN_OCTAVE_DOWN[1], ABL.BTN_OCTAVE_UP[1]]: lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_LIT) + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) # Leaving scales mode: remove anything that is initailized def scales_cleanup(self): # set of any LED and display changes # cleadup display - btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | | |" + #btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | | |" btn_txt_row2 = "| | | | || | | | |" - self._display.write_xy_mem(btn_txt_row0, 0, 0) - self._display.write_xy_mem(btn_txt_row2, 0, 1) + #self._display.write_xy_mem(btn_txt_row0, 0, 0) + # self._display.write_xy_mem(btn_txt_row2, 0, 1) self._display.write_xy_mem(btn_txt_row2, 0, 2) self._display.write_xy_mem(btn_txt_row2, 0, 3) self._display.update_screen() @@ -263,6 +268,9 @@ def scales_cleanup(self): # set of any LED and display changes ] for t in scale_buttons: lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_LED_OFF) + + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) + # set up buttons for t in [ABL.BTN_OCTAVE_DOWN[1], ABL.BTN_OCTAVE_UP[1]]: lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_OFF) @@ -485,40 +493,46 @@ def helper_set_new_tonic(tonic): # # erg = "".ljust(int(value*10),"|").ljust(15)[:field_width] # return erg - def mixer_helper_write_to_knobx_fieldy(self, text: any, knob_x:int, field_y:int, as_bar:bool = False): - """writes to a specified place below a knob - knob_x is the knob from 0 to 7 (push_1 has 9 knobs, but nith has no display) - field_y is the row written to see consts: _MIXER_DISP_ROW_* - text can be text or float value - """ - if knob_x > 7: - knob_x = 7 - logging.error("knob_x bigger 7 not implemented. Sum channel is directed to 7") - if isinstance(text, (int, float)): # when text of type float or int change it to str - if as_bar: - text = self.mixer_helper_float_to_ascii_Bar(text) - else: - text = str(text) - - fields_start_knobs = [0,9, 17,26, 34,43, 51,60] - knobx_start = fields_start_knobs[knob_x] - text=text.ljust(8)[:8] # make text with minimal 10 and max 10 chars - self._display_mixer.write_xy_mem(text, knobx_start, field_y) + # def mixer_helper_write_to_knobx_fieldy(self, text: any, knob_x:int, field_y:int, as_bar:bool = False): + # """writes to a specified place below a knob + # knob_x is the knob from 0 to 7 (push_1 has 9 knobs, but nith has no display) + # field_y is the row written to see consts: _MIXER_DISP_ROW_* + # text can be text or float value + # """ + # logging.error ("Call deprcated") + # sleep(3) + # if knob_x > 7: + # knob_x = 7 + # logging.error("knob_x bigger 7 not implemented. Sum channel is directed to 7") + # if isinstance(text, (int, float)): # when text of type float or int change it to str + # if as_bar: + # text = self.mixer_helper_float_to_ascii_Bar(text) + # else: + # text = str(text) + + # fields_start_knobs = [0,9, 17,26, 34,43, 51,60] + # knobx_start = fields_start_knobs[knob_x] + # text=text.ljust(8)[:8] # make text with minimal 10 and max 10 chars + # self._display_mixer.write_xy_mem(text, knobx_start, field_y) - def mixer_helper_float_to_ascii_Bar(self, value:float): - fieldlen = 8 - int_val = int(value * fieldlen) # val is 0.0 to 1.0. we want range 0-7 - return "".ljust(int_val, chr(6)).ljust(fieldlen) # fill up with spaces to overwrite old values + # def mixer_helper_float_to_ascii_Bar(self, value:float): + # fieldlen = 8 + # int_val = int(value * fieldlen) # val is 0.0 to 1.0. we want range 0-7 + # return "".ljust(int_val, chr(6)).ljust(fieldlen) # fill up with spaces to overwrite old values def mixer_init(self): """mixer display functions are called during start. Mixer is main functionality, so it hast to be setup in intit _function """ # create consts for mixer display - self.MIXER_DISP_ROW_VOLUME = 0 - self.MIXER_DISP_ROW_BALANCE = 1 - self.MIXER_DISP_ROW_3 = 2 - self.MIXER_DISP_ROW_4 = 3 + self.MIXER_DISP_ROW_VOLUME = 1 + self.MIXER_DISP_ROW_BALANCE = 0 + # NO Mixer has rows 1, 2 exclusiv. doesn't need more + # self.MIXER_DISP_ROW_3 = 2 + # self.MIXER_DISP_ROW_4 = 3 + + self.KNOB_VOLUME = ABL.KNOB_8 + self._mixer_chains_bank = 0 self._display_mixer = Feedback_Display(self.idev_out); self.mixer_set_dev_to_mixermode() @@ -530,25 +544,25 @@ def mixer_set_dev_to_mixermode(self): # creat private mixer display - btn_txt_row0 = "| Ch 1 | Ch 2 | Ch 33 | Ch 4 || Ch 5 | Ch 6 | Ch 7 | Ch 8 |" - btn_txt_row1 = " | This is the Mixer Display " - btn_txt_row2 = f"|modes here {chr(5)} {chr(6)} {chr(5)}{chr(6)} | |" - btn_txt_row3 = "| | | | || | | | |" + btn_txt_row0 = " Ch 1 Ch 2 Ch 3 Ch 4 Ch 5 Ch 6 Ch 7 Main " + btn_txt_row1 = " " + #btn_txt_row2 = f"|modes here {chr(5)} {chr(6)} {chr(5)}{chr(6)} | |" + #btn_txt_row3 = "| | | | || | | | |" btn_text_row2 = self._display_mixer.format_help self._display_mixer.write_xy_mem(btn_txt_row0, 0, 0) - self._display_mixer.write_xy_mem(btn_txt_row1, 0, 1) - self._display_mixer.write_xy_mem(btn_txt_row2, 0, 2) - self._display_mixer.write_xy_mem( "Volume Mode".ljust(20)[:15], 0, 2 ) # Mode of knobs - self._display_mixer.write_xy_mem(btn_txt_row3, 0, 3) + # self._display_mixer.write_xy_mem(btn_txt_row1, 0, 1) + # self._display_mixer.write_xy_mem(btn_txt_row2, 0, 2) + # self._display_mixer.write_xy_mem( "Volume Mode".ljust(20)[:15], 0, 2 ) # Mode of knobs + # self._display_mixer.write_xy_mem(btn_txt_row3, 0, 3) self._display_mixer.update_screen() - # paint into tha test data - ch1_level = self.zynmixer.zctrls[0]['level'].get_value() # is this a set level or the real sound lovel - ch1_level_bar = self.mixer_helper_float_to_ascii_Bar(ch1_level) - first_knob_nr = 0 - self.mixer_helper_write_to_knobx_fieldy(ch1_level_bar, first_knob_nr, self.MIXER_DISP_ROW_VOLUME) + # # paint into test data + # ch1_level = self.zynmixer.zctrls[0]['level'].get_value() # is this a set level or the real sound lovel + # ch1_level_bar = self.mixer_helper_float_to_ascii_Bar(ch1_level) + # first_knob_nr = 0 + # self._display_mixer.write_to_knobx_mem(ch1_level_bar, first_knob_nr, self.MIXER_DISP_ROW_VOLUME) self._display_mixer.update_screen()# send display_data to display @@ -568,8 +582,8 @@ def update_mixer_active_chain(self, active_chain): if c[:5] == "chan_": chan_nr = int(c[5:7]) ch_level = self.zynmixer.zctrls[chan_nr]['level'].get_value() # we use level from here, so we no field exists - ch_level_bar = self.mixer_helper_float_to_ascii_Bar(ch_level) - self.mixer_helper_write_to_knobx_fieldy(ch_level_bar, chan_nr, self.MIXER_DISP_ROW_VOLUME) + # ch_level_bar = self._display_mixer. mixer_helper_float_to_ascii_Bar(ch_level) + self._display_mixer.write_to_knobx_mem(ch_level, chan_nr, self.MIXER_DISP_ROW_VOLUME, as_bar = True) # write names # Check if chain exists @@ -599,6 +613,7 @@ def update_mixer_strip(self, chan, symbol, value): chan - Mixer strip index symbol - Control name value - Control value + we use just volume """ try: @@ -606,32 +621,53 @@ def update_mixer_strip(self, chan, symbol, value): case 'level': if chan > 7: chan = 7 - self.mixer_helper_write_to_knobx_fieldy(value, chan, self.MIXER_DISP_ROW_VOLUME, as_bar=True) + self._display_mixer.write_to_knobx_mem(value, chan, self.MIXER_DISP_ROW_VOLUME, as_bar=True) self._display_mixer.update_screen() return # Wichtig: Return nach erfolgreicher Verarbeitung! case 'balance': - # Implementierung für balance + # # Implementierung für balance + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # self._display_mixer.update_screen() pass case 'mute': - # Implementierung für mute - pass + # # Implementierung für mute + # if value == 1: + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # else: + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # self._display_mixer.update_screen() + pass # update bar display case 'solo': # Implementierung für solo + # if value == 1: + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # else: + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # self._display_mixer.update_screen() pass case 'mono': - # Implementierung für mono + # # Implementierung für mono + # if value: + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # else: + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # self._display_mixer.update_screen() pass - case 'm+s': # Mono / Stereo - # Implementierung für m+s + case 'm+s': # Mono / Stereo ??? what does that mean + # # Implementierung für m+s + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # self._display_mixer.update_screen() pass case 'phase': - # Implementierung für phase + # # Implementierung für phase + # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) + # self._display_mixer.update_screen() pass case _: @@ -647,13 +683,61 @@ def update_mixer_strip(self, chan, symbol, value): except Exception as e: logging.error(f"Error in update_mixer_strip: {e}") logging.exception(traceback.format_exc()) + + def process_mixer_event(self, ev) -> bool: + # turning knob changes now volume from chanel + + if len(ev) != 3: return # we meed 3 bytes + + # Set Mixer Level from KNOBS 1 to 8 + search_knob = [ev[0], ev[1]] + if search_knob in [ABL.KNOB_1, ABL.KNOB_2, ABL.KNOB_3, ABL.KNOB_4, + ABL.KNOB_5, ABL.KNOB_6, ABL.KNOB_7, ABL.KNOB_8 + ]: + return self._update_control("level", ev, 0, 100) + + #self.zyn #self.zynmixer.setlevel() pass + def _update_control(self, type:str, ev:bytes, minv, maxv): + ccnum = ev[1] & 0x7F + ccval = ev[2] & 0x7F + # if self.shift: + # # Only main chain is handled with SHIFT, ignore the rest + # if ev[:2] == bytes(self.KNOB_VOLUME): + # return False + # mixer_chan = 255 # is mixer mains volume + if ev[:2] == bytes(self.KNOB_VOLUME): # detect mains knob + mixer_chan = 255 + else: + # _chains_bank shifts the knob to the visual bank in mixer panel + index = (ccnum - ABL.KNOB_1[1]) + self._mixer_chains_bank * 8 + chain = self.chain_manager.get_chain_by_index(index) + if chain is None or chain.chain_id == 0: + return False + mixer_chan = chain.mixer_chan + + if type == "level": + value = self.zynmixer.get_level(mixer_chan) # get value + set_value = self.zynmixer.set_level # get function belonging to that chain + elif type == "balance": + value = self.zynmixer.get_balance(mixer_chan) + set_value = self.zynmixer.set_balance # get function belonging to that chain + else: + return False + + # NOTE: knobs are encoders, not pots (so ccval is relative) + value *= 100 # translat value 0.0 .. 1.. to 0.0 .. 100.0 + value += ccval if ccval < 64 else ccval - 128 # translate knob values to plus or minus valus and add + value = max(minv, min(value, maxv)) # check and set range + set_value(mixer_chan, value / 100) # move val back to 0..1 range # use saved function to set chains target + return True + ### END of Mixer functions. ### ######################################################################### @@ -873,7 +957,7 @@ def set_device_mode_new(self, new_mode): match new_mode: case self.DEV_MODE_MIXER: # init mixer - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_VOLUME[1], ABL.MONO_LED_LIT_BLINK) + # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_VOLUME[1], ABL.MONO_LED_LIT_BLINK) return self.mixer_set_dev_to_mixermode() case self.DEV_MODE_PAD: @@ -1102,7 +1186,8 @@ def set_pad_rgb(self, pad_nr: int, r:int ,g:int ,b:int): # ------------------------------------------------------------------------------ - +##################################################################################### +#### Special Class for Ableton Pushh 1 Display class Feedback_Display: @@ -1122,8 +1207,8 @@ class Feedback_Display: DISP_HORIZONTAL_LINE_LOW = 95 # _ (U+005F) Lowbar DISP_HOIZONTAL_LINE_SPLIT = 6 # ╌ (U+254C) LIGHT DOUBLE DASH HORIZONTAL - DISP_VERTICAL_LINE_AND_HORIZONTAL_LINE = 3 # ┤ (U+2524) - DISP_HORIZONTAL_LINE_AND_VERTICAL_LINE = 4 # ├ (U+251C) + DISP_VERTICAL_LINE_AND_HORIZONTAL_LINE = 3 # ├ (U+251C) + DISP_HORIZONTAL_LINE_AND_VERTICAL_LINE = 4 # ┤ (U+2524) DISP_VERTICAL_LINES_TWO = 5 # ║ (U+2551) DISP_VERTICAL_LINE_MID = 174 # | (U+007C) @@ -1287,6 +1372,36 @@ def write_xy_mem (self, text, col_in:int, row_in:int): self._disp_line_dirty[row_in] = True # self.update() return + + + def write_to_knobx_mem(self, text: any, knob_x:int, row_y:int, as_bar:bool = False): + """writes to a specified place below a knob + knob_x is the knob from 0 to 7 (push_1 has 9 knobs, but nith has no display) + field_y is the row written to see consts: _MIXER_DISP_ROW_* + text can be text or float value + """ + if knob_x > 7: + knob_x = 7 + logging.error("program error: knob_x bigger 7 not implemented. Sum channel is directed to 7") + + if isinstance(text, (int, float)): # when text of type float or int change it to str + # text contains float or int + if as_bar: + # create an ascii bar representation + fieldlen = 8 + if float(text) == 0.0: text = "".ljust(fieldlen) # 0 vals as "off" + else: + int_val = int(text * fieldlen) # val is 0.0 to 1.0. we want range 0-7 + if int_val == 0: text=".".ljust(fieldlen) # if minimal signal show a dot + else: text = "".ljust(int_val, chr(self.DISP_VERTICAL_LINES_TWO)).ljust(fieldlen) # fill up with spaces to overwrite old values + else: + text = str(text).ljust(fieldlen) + + fields_start_knobs = [0,9, 17,26, 34,43, 51,60] + knobx_start = fields_start_knobs[knob_x] + text=text.ljust(8)[:8] # make text with minimal 10 and max 10 chars + self.write_xy_mem(text, knobx_start, row_y) + def contrast (self, i=None) -> int: """ From b06bb0f2fd1faf27b0dde551f8f449cb497225bd Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 20 Sep 2025 20:03:05 +0200 Subject: [PATCH 43/57] added comments to every function as help --- .../ctrldev/zynthian_ctrldev_base_scale.py | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index 22aa13b13..c63fd90e3 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -158,29 +158,22 @@ def init_scale(self, if self.middle_pad_nr is None: self.middle_pad_nr = 4 self.must_redraw_led_colors = True - is_dirty == True - + is_dirty == True # if not is_dirty: return # if just tonica changed go back self.target_notes = [] # Reset for new scale self.target_notes_reverse = {} # Reset for new scale - - # if middle_pad_nr < 0 : middle_pad_nr = 0 - # if middle_pad_nr >= self.cols * self.rows: - # middle_pad_nr = self.cols * self.rows -1 - - mode = self.modes[self.active_mode] + mode = self.modes[self.active_mode] + pad_counter = -1 for i in range (- self.middle_pad_nr, (self.cols*self.rows) - self.middle_pad_nr): - pad_counter += 1 + pad_counter += 1 row_nr = pad_counter // self.cols - note_nr_in_scale = i + (row_nr * self.col_versatz) - octave = note_nr_in_scale // len(mode) if console_debug: print (f"{octave}:{pad_counter}=", end="") @@ -201,6 +194,7 @@ def init_scale(self, print("*", end="\n", flush=True) # Newline at end of row return + # direct set from program def set_new_tonic(self, new_tonic:int): """new_tonic is next tonic in selected scale""" if new_tonic == self.tonic: return False @@ -209,6 +203,7 @@ def set_new_tonic(self, new_tonic:int): self.tonic = new_tonic return True # yes update display. we changed it + # for Knob Control. step to next def step_to_next_tonic(self, step): """For Knob Control. 127 converted -1""" if step > 63: step -=128 # for controller sending 127 for -1 @@ -217,25 +212,29 @@ def step_to_next_tonic(self, step): if new_tonic > 11: new_tonic = 0 # target: C self.scales.tonic = new_tonic + # pad nr must be colorized as tonic def is_tonic_by_padnr(self, pad_nr:int)-> bool: res = self.target_notes[pad_nr] res2 = res % 12 return res2 == 0 #return self.target_notes[padnr] % 12 == 0 - + # midi note, which hast to be colorized as tonic def is_tonic_by_midnote(self, midi_note:int) -> bool: return (midi_note - self.tonic) % 12 == 0 - + + # get back list of pads, that have same midi_note def get_equi_sound_pads_with_midi_note(self, midi_note) -> list: # Subtract tonic to get internal representation internal_note = midi_note - self.tonic return self.target_notes_reverse.get(internal_note, []) + # get back list of pads, that have same midi_note by pad_nr def get_equi_sound_pads_with_pad_nr(self, pad_nr): midi_note = self.target_notes[pad_nr] return self.target_notes_reverse.get(midi_note,[]) + # scale contains how much notes def harmony_get_mode_len(self, mode:str) -> int: """Return count of tones mode""" try: @@ -244,24 +243,28 @@ def harmony_get_mode_len(self, mode:str) -> int: logging.error(f"Error: get_mode_len: mode '{mode}' not defined") return 0 + # get back a list if strings containing mode names def harmony_get_mode_names(self): return list(self.modes) + # get scale and mode as string def harmony_get_scale_name_with_mode (self) -> str: """actual scale and mode as string for display""" result = self.scales[self.tonic] + ' ' + self.active_mode result = result.ljust(20)[:20] return result + # THIS IS THE MAIN FUNCTION THAT DOES THE MAGIC + # transaltes pad_nr to midi_notes in the selected mode and scale !!! def harmony_get_target_note(self, pad_nr: int) -> int: if not 0 <= pad_nr < len(self.target_notes): logging.error("Program error, pad_nr out of range for len(target_notes)") return None return self.target_notes[pad_nr] + self.tonic - def harmony_get_padnrs_with_same_note(self, midi_note: int): - internal_note = midi_note - self.tonic - return self.target_notes_reverse.get(internal_note, []) + # def harmony_get_padnrs_with_same_note(self, midi_note: int): + # internal_note = midi_note - self.tonic + # return self.target_notes_reverse.get(internal_note, []) ### End of class definition Harmony ############################################## From dc4b8e0f65108af6b5e32ebe4559f9bd82941d02 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 20 Sep 2025 20:48:51 +0200 Subject: [PATCH 44/57] Made it ready for pull request in zynthian man repo --- .../ctrldev/zynthian_ctrldev_base_scale.py | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index c63fd90e3..8d3177f0b 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -1,5 +1,67 @@ #!/zynthian/venv/bin/python +# ****************************************************************************** +# ZYNTHIAN PROJECT: Zynthian Control Device Driver +# +# Zynthian Control Device Driver for "Ableton Push 1" +# +# Copyright (C) 2025 Brumby +# +# ****************************************************************************** +# +# 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. +# +# ****************************************************************************** + + +# how to use this Class minimalistic +# +""" + scales = Harmony() # set pad array + scales.init_scale() # set scales, modes, etc + + pushed_pad_nr = 5 + target_note = scales.harmony_get_target_note(pushed_pad_nr) + + ### send this target_note as note_on_event. See REMARK +""" +### REMARK +### a self programmed driver doesn'send anymore note_on events down the line. +# drivers are intended as control drivers for zynthian divices not any more for sound genarating +# I think there was a latency problem with keyboard drivers. +# I use following hack on PI 4 and it seams to work for me. +# One (?) possible way is to send your new events with zynseq.libseq.sendMidiCommand +""" +# EXAMPLE HERE: +### solution to send new events down the chain comes from niels in Zynthian forum + def _forward_new_midi_event(self, ev): + + # get selected chain in mixer + chain = self.chain_manager.get_active_chain() . + + # is it midi chain + if chain.midi_chan is None: # is it a midi chain? + return False + + # set up vars + status = (ev[0] & 0xF0) | chain.midi_chan + self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) + return True # work is done. main event can start over and get a new midi event. + """ + + +### START OF LIBRARY + import logging # Do not change. Only if this file is started directly from console @@ -67,6 +129,7 @@ def __init__(self, pad_cols=8, pad_rows=8): self.must_redraw_led_colors = False self._lock = 0 + # helper functions def is_initialized(self): if self.target_notes == []: return False if self.target_notes_reverse == {}: return False @@ -78,7 +141,9 @@ def is_initialized(self): def must_reset_led_colors(self) -> bool: return self.must_redraw_led_colors - + + # here setup for scale and mode with defaults. + # can be caled as scales = init_scale() def init_scale(self, tonic: int = 0, # (C) semitone distance counted from from C = 0 mode_name: str = "Major", # mode as str. look in _SCALES From 08bd5e5e8d3d3ca2a81212b64e6bf21ef92bfde5 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sat, 20 Sep 2025 23:38:50 +0200 Subject: [PATCH 45/57] Secure last stage. Befor trying v2 file. --- .../zynthian_ctrldev_ableton_push_1.py | 50 +++++++++++++------ .../ctrldev/zynthian_ctrldev_base_scale.py | 35 ++++++++----- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py index 8839491ad..05796b661 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py @@ -28,9 +28,30 @@ # # ****************************************************************************** - -# Display: Top two rows mixer -# bottm two for rows scales / Sequencer display +# +# I was in need for a driver for Push 1 because without it, it is completly dead. +# this driver brings it to some live and gives you: +# +# - a rudimentary mixer mode with just Volume Knobs for chain 1-7 and main Volume aat knob 8 +# (push "Volume"-Button) +# +# - a pad array mode for the Sequencer +# (Puser "User"-Button) +# +# - an illuminated PadArray with selectable Scales and Modes +# (push "Scales"-Button) +# +# driver is not complete but in usable state. +# +# in Mixermode Knobs1 to Knob4 no more Zynpots. They control chain 1-4 Volume +# in other Modes they act as Zynpots for Zynthian GUI +# +# Buttons that have functions are illuminated +# actual driver modes are blinki its mode buttons +# +# Display: +# - Top two rows exclusive for mixer +# - bottom two rows for modes: scales and sequencer import logging import traceback @@ -39,14 +60,15 @@ #### just local debug debug_mode = True if debug_mode: - - # Eigenen Logger für Ihre Library erstellen - logger = logging.getLogger("ABL-Push_1") # Eindeutiger Name für Ihre Library + + # Set own logger, so you can set it to debug. But the rest of zynthian debug is invisble + # Start + logger = logging.getLogger("ABL-Push_1") # set own name for messags - # Nur für Ihren Logger Level setzen - logger.setLevel(logging.DEBUG) # Nur DIESER Logger zeigt Debug messages + # set level to debug, so you can see you own debug messages + logger.setLevel(logging.DEBUG) - # Handler for your logger (optional) + # possible own handler handler = logging.StreamHandler() formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) @@ -56,11 +78,11 @@ # Brumbys new imports -from time import sleep # pause between sysex events. -import zyngine.ctrldev.ableton.push1_consts as ABL -from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony -from zyngine.zynthian_signal_manager import zynsigman -from zyngine.zynthian_engine import zynthian_engine # to send directly to soundengine... +from time import sleep # pause between sysex events. push 1 is too slow +import zyngine.ctrldev.ableton.push1_consts as ABL # Button definitions +from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony # my Class for scales mode +# from zyngine.zynthian_signal_manager import zynsigman +# from zyngine.zynthian_engine import zynthian_engine from zyngine.ctrldev.zynthian_ctrldev_base_extended import RunTimer, KnobSpeedControl, ButtonTimer, CONST # Zynthian specific modules diff --git a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py index 8d3177f0b..578f076e8 100755 --- a/zyngine/ctrldev/zynthian_ctrldev_base_scale.py +++ b/zyngine/ctrldev/zynthian_ctrldev_base_scale.py @@ -3,7 +3,7 @@ # ****************************************************************************** # ZYNTHIAN PROJECT: Zynthian Control Device Driver # -# Zynthian Control Device Driver for "Ableton Push 1" +# Zynthian Control Device Base Driver for "scales supperot for pads" # # Copyright (C) 2025 Brumby # @@ -24,36 +24,47 @@ # ****************************************************************************** +# helps implement 2-D-arrays with fixed scales and modes +# +# you can change the scales and modes on the fly and are not fixed to +# hard coded arrays like on my launchpad mk3 mini +# +# my push 1 is an wonderful controller but it has no stand alone mode. With out specian +# zynthian driver it is completly dead. +# # how to use this Class minimalistic # """ scales = Harmony() # set pad array scales.init_scale() # set scales, modes, etc - pushed_pad_nr = 5 + pushed_pad_nr = 39 # use the number of the pushed pad. You have to calculate that from midi event target_note = scales.harmony_get_target_note(pushed_pad_nr) - ### send this target_note as note_on_event. See REMARK + ### send this target_note with note_on_event. See REMARK """ ### REMARK -### a self programmed driver doesn'send anymore note_on events down the line. -# drivers are intended as control drivers for zynthian divices not any more for sound genarating -# I think there was a latency problem with keyboard drivers. +### a device driver in Zynthian doesn'send anymore note_on events down the line. +# drivers are intended as control drivers for zynthian divices. Sending note_on events +# is blocked by default. +# ( I think there was a latency problem with keyboard device drivers. ) # I use following hack on PI 4 and it seams to work for me. # One (?) possible way is to send your new events with zynseq.libseq.sendMidiCommand """ # EXAMPLE HERE: +### many thanks to niels, who gave me this solution: ### ### solution to send new events down the chain comes from niels in Zynthian forum - def _forward_new_midi_event(self, ev): + def _forward_new_midi_event_to_active_chain(self, ev): # get selected chain in mixer chain = self.chain_manager.get_active_chain() . - # is it midi chain - if chain.midi_chan is None: # is it a midi chain? - return False + # is this a midi chain + if chain.midi_chan is None: + return False # if not, do nothing - # set up vars + # set up needed vars for function + # Fill in chains midi chanel into stauts from event status = (ev[0] & 0xF0) | chain.midi_chan self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) return True # work is done. main event can start over and get a new midi event. @@ -333,7 +344,7 @@ def harmony_get_target_note(self, pad_nr: int) -> int: ### End of class definition Harmony ############################################## - +################################################################################## ### For test purposes from command line From 7917cb84c734639bbcd9d7d371361c7a5242794b Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 01:27:20 +0200 Subject: [PATCH 46/57] Push 1 driver completel rearranged. Made consts to tuples and cleaned comments --- .../zynthian_ctrldev_ableton_push_1.py | 1615 ----------------- .../zynthian_ctrldev_ableton_push_1_v2.py | 1319 ++++++++++++++ 2 files changed, 1319 insertions(+), 1615 deletions(-) delete mode 100755 zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py create mode 100644 zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py deleted file mode 100755 index 05796b661..000000000 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1.py +++ /dev/null @@ -1,1615 +0,0 @@ -#! /zynthian/venv/bin/python -# -*- coding: utf-8 -*- - -# TODO: DIsplay rowas are of different type. -# Row two seams to be monochrome green, just brightnes - - -# ****************************************************************************** -# ZYNTHIAN PROJECT: Zynthian Control Device Driver -# -# Zynthian Control Device Driver for "Ableton Push 1" -# -# Copyright (C) 2025 Brumby -# -# ****************************************************************************** -# -# 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. -# -# ****************************************************************************** - -# -# I was in need for a driver for Push 1 because without it, it is completly dead. -# this driver brings it to some live and gives you: -# -# - a rudimentary mixer mode with just Volume Knobs for chain 1-7 and main Volume aat knob 8 -# (push "Volume"-Button) -# -# - a pad array mode for the Sequencer -# (Puser "User"-Button) -# -# - an illuminated PadArray with selectable Scales and Modes -# (push "Scales"-Button) -# -# driver is not complete but in usable state. -# -# in Mixermode Knobs1 to Knob4 no more Zynpots. They control chain 1-4 Volume -# in other Modes they act as Zynpots for Zynthian GUI -# -# Buttons that have functions are illuminated -# actual driver modes are blinki its mode buttons -# -# Display: -# - Top two rows exclusive for mixer -# - bottom two rows for modes: scales and sequencer - -import logging -import traceback - - -#### just local debug -debug_mode = True -if debug_mode: - - # Set own logger, so you can set it to debug. But the rest of zynthian debug is invisble - # Start - logger = logging.getLogger("ABL-Push_1") # set own name for messags - - # set level to debug, so you can see you own debug messages - logger.setLevel(logging.DEBUG) - - # possible own handler - handler = logging.StreamHandler() - formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') - handler.setFormatter(formatter) - logger.addHandler(handler) - -### end of just local debug - - -# Brumbys new imports -from time import sleep # pause between sysex events. push 1 is too slow -import zyngine.ctrldev.ableton.push1_consts as ABL # Button definitions -from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony # my Class for scales mode -# from zyngine.zynthian_signal_manager import zynsigman -# from zyngine.zynthian_engine import zynthian_engine -from zyngine.ctrldev.zynthian_ctrldev_base_extended import RunTimer, KnobSpeedControl, ButtonTimer, CONST - -# Zynthian specific modules -from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer -from zyncoder.zyncore import lib_zyncore -from zynlibs.zynseq import zynseq - -# ------------------------------------------------------------------------------------------------------------------ -# Ableton Push 1 -# ------------------------------------------------------------------------------------------------------------------ - -# zynthian_ctrldev_zynpad is class for Controlling the sequencer with pads -#zynthian_ctrldev_zynmixer cpntrolls the main mixer - - -# note werte Push 1 Midimapping -# don't delete. -ABL_PAD_START = 36 # 1. Pad = pad_36 -ABL_PAD_END = 99 # letztes Paad = pad_99 - - -class zynthian_ctrldev_ableton_push_1(zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer): - - logging.info("Push 1 initializes instance of class") - # Weblog shows this messages - - # dev_ids = ["Ableton Push IN 2", "Ableton Push IN 1"] # get by stepping through zynthian_ctrldev_manager.load_driver() - dev_ids = ["Ableton Push IN 2"] # get by stepping through zynthian_ctrldev_manager.load_driver(). Data just at Port 2 - - driver_name = "Ableton Push v1" # not essential. class name would be used otherwise - driver_description = "Interface Ableton Push v1 with zynpad and zynmixer" - - ################################ - - # Colors for LED-Pads in Sequencermode # TODO: Palette has to be fixed - # siehe: https://pushmod.blogspot.com/p/pad-color-table.html - # ORIGINAL PAD_COLOURS = [71, 104, 76, 51, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] - PAD_COLOURS = [61, 36, 63, 54, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] # not running - STARTING_COLOUR = 123 # GREEM - STOPPING_COLOUR = 120 # RED - RUNNING_COLOR = 3 # WHITE - - # equal vars are in base_extended... - # evtype = (ev[0] >> 4) & 0x0F -> - EV_NOTE_OFF = 0x8 # 3 Bytes - EV_NOTE_ON = 0X9 # 3 Bytes - EV_AFTERTOUCH = 0xA # 3 Bytes (polyphonic = per note) - EV_CC = 0xB # 3 Bytes - EV_PC = 0xC # 2 Bytes - EV_CHAN_PRESS = 0xD # 2 Bytes - EV_PITCHBEND = 0xE # 3 bytes ev[1] = LSB 0-127; ev[2] = MSB 0-127 - EV_SYSTEM = 0xF # varies from 1 to many Bytes ### Systemtype = ev[0] & 0x0F - - - # dev_modes - DEV_MODE_NONE = None - DEV_MODE_PAD = 1 - DEV_MODE_SCALES = 2 # keyboard modes - DEV_MODE_MIXER = 3 - # DEV_MODE_DRUMS = 2 - # pad_mode_active = PAD_MODE_SEQ - device_mode_active = DEV_MODE_NONE # initial mode - - ### would be nice to see on display if class is found - ### self._display = Feedback_Display(idev_out) # Text display - - scales = Harmony(8,8) - scales.init_scale(tonic=0, - mode_name="Major", - col_versatz=-5, - middle_c=48, - middle_pad_nr=4) - - # Function to initialise class - def __init__(self, state_manager, idev_in, idev_out=None): - logging.info("Found Push 1 on USB") - # would be nice to say, correct USB Device is found - - # super.__init__ saves state_manger, chainmanger, idev_in and idev_out - # nothing more. - super().__init__(state_manager, idev_in, idev_out) - - # to slow knob-events.translates 127 to -1 - # TODO experiment with setup values when live - self._knobs_ease = KnobSpeedControl() - - # Indecators of the device LEDs and Text # NOT USED - self._leds_mono = Feedback_Mono_LEDs(idev_out) # control buttons right and left from pads - self._leds_bi = Feedback_Bi_LEDs(idev_out) # display buttons below display, above pads - self._leds_rgb = Feedback_RGB_LEDs(idev_out) # pads in rgb - self._display = Feedback_Display(idev_out) # Text display - self.mixer_init() # Text display for mixer # suerp()__init__ has to be called earlier to set idev_out - - - # seems to be necessary, because we send translated midi_events. o - self.unroute_from_chains = True - return - - # called from parent - def init(self): - try: - logging.info("called init. Setting up Ableton Push 1 - BRUMBY") - self.shift = False # BTN_SHIFT is pressed - self.shift_note = 0 # Octave buttons - - - # set initial device mode - self.set_device_mode_new(self.DEV_MODE_MIXER) - - - # setup LEDS in Ctrl-Buttons - # Monochrome Tasten die hell leuchten sollen - for t in [ 36,37,38,39,40,41,42,43, - ABL.BTN_START[1], ABL.BTN_OK[1], ABL.BTN_ESC[1], ABL.BTN_LEFT[1], - ABL.BTN_RIGHT[1], ABL.BTN_UP[1], ABL.BTN_DOWN[1], ABL.BTN_SCALES[1], - ABL.BTN_USER[1] - ]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_LIT) - - # monochrome Buttons than should be dim state - for t in [ ABL.BTN_REC[1], ABL.BTN_SHIFT[1] ]: # ,ABL_REC, ABL_SHIFT]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) - - # Bicolor LEDs dim ## CC20-27 + 102-109 - for t in [ABL.BTN_R1_C1[1], ABL.BTN_R1_C2[1], ABL.BTN_R1_C3[1], ABL.BTN_R1_C4[1]]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_ORANGE_DIM) - - ### lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 127) - # setup device pad arry size - self.cols = 8 - self.rows = 8 # war 2 20250829-2134 - super().init() # aktiviert. Muss aktiviert sein! - # self.pads_off() - if self.device_mode_active == self.DEV_MODE_SCALES: - self.scales_set_dev_to_scales_mode() - - #except: - # print("Fehler aufgetreten: {e}") - except Exception as e: - print("Exception aufgetreten:") - # Gibt den vollständigen Traceback aus - # traceback.print_exc() - logger.error("Exception aufgetreten: %s", e) - logger.error("Traceback: %s", traceback.format_exc()) - - # called from parent - def end(self): - # logging.error("end Ableton Push 1 - BRUMBY") - super().end() - ### Disable session mode on launchkey - ## lib_zyncore.dev_send_note_on(self.idev_out, 15, 12, 0) # device, channel, note, velocity - - -################################################################################################################# -################## START of scales fucntions ########################################################## - - # when changing to scales mode: start here - # new in this class, to setup scales_mode = keyboard mode - def scales_set_dev_to_scales_mode(self): - self.device_mode_active = self.DEV_MODE_SCALES - # visual feedback, let Scales Button blink - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) - self.pads_off() # akk pad leds off - self.scales_set_pad_colors() # set LEDs for scale mode - # self._display.clear() - scale_n_mode = self.scales.harmony_get_scale_name_with_mode() - self._display.write_xy_mem(scale_n_mode, 0, 2) - - #btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | Scale | Mode |" - # btn_txt_row1 = f"| {chr(12)} {chr(11)} {chr(10)} | {chr(9)} {chr(8)} {chr(7)} {chr(6)} | | |" - #btn_txt_row1 = " " - btn_txt_row2 = "|modes here | | || G# | A | A# | B |" - btn_txt_row3 = "| C | C# | D | D# || E | F | F# | G |" - - #self._display.write_xy_mem(btn_txt_row0, 0, 0) - #self._display.write_xy_mem(btn_txt_row1, 0, 1) - self._display.write_xy_mem(btn_txt_row2, 0, 2) - self._display.write_xy_mem(scale_n_mode, 0, 2) # Scale and scale over row2 - self._display.write_xy_mem(btn_txt_row3, 0, 3) - self._display.update_screen() - # set PAD LEDS - self.scale_update_leds(self.scales.tonic) # 0 is 'C' - # set up buttons - for t in [ABL.BTN_OCTAVE_DOWN[1], ABL.BTN_OCTAVE_UP[1]]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_LIT) - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) - - - # Leaving scales mode: remove anything that is initailized - def scales_cleanup(self): # set of any LED and display changes - # cleadup display - #btn_txt_row0 = "| ZynP1 | ZynP2 | ZynP3 | ZynP4 || | | | |" - btn_txt_row2 = "| | | | || | | | |" - #self._display.write_xy_mem(btn_txt_row0, 0, 0) - # self._display.write_xy_mem(btn_txt_row2, 0, 1) - self._display.write_xy_mem(btn_txt_row2, 0, 2) - self._display.write_xy_mem(btn_txt_row2, 0, 3) - self._display.update_screen() - - # cleanup scale LED - scale_buttons = [ - ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], - ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], - ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] - ] - for t in scale_buttons: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_LED_OFF) - - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) - - # set up buttons - for t in [ABL.BTN_OCTAVE_DOWN[1], ABL.BTN_OCTAVE_UP[1]]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_OFF) - - - - # LED are setup to passive and the the actiavated LED is set - def scale_update_leds(self, index_activated): # index defines blinkin LED - # Bicolor LEDs dim ## CC20-27 + 102-109 - scale_buttons = [ - ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], - ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], - ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] - ] - for t in scale_buttons: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.BI_GREEN_DIM) - # set scale LED blinking - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, scale_buttons[index_activated], ABL.BI_GREEN_DIM_BLINK) - - - def scales_set_tonic(self, step): - if step > 63: step -=128 # make left turn values (+ 64 to +127) to negative value - # slowing down knob by factor ten - self.steps_tonic = getattr(self, 'steps_tonic', 0) + step - if not abs(self.steps_tonic) > 10: return # slow down. each 10th step - self.steps_tonic = 0; - - # calculate new tonic - new_tonic = self.scales.tonic + step - if new_tonic < 0: new_tonic = 11 # target: B - if new_tonic > 11: new_tonic = 0 # target: C - self.scales.tonic = new_tonic # set tonic. thats all nothing to recalculate - - # set Display and LED from select buttons. PAD-LED don't need update, because mode isn't changed - scale_n_mode = self.scales.harmony_get_scale_name_with_mode() - self._display.write_xy_mem(scale_n_mode, 0, 2) - self._display.update_screen() - self.scale_update_leds(new_tonic) - return - - - def scales_set_mode(self, step): - if step > 63: step -=128 # 127 = -1 # make left turn values negative - # lower knob speed by 10 - self.steps_mode = getattr(self, 'steps_mode', 0) + step - if not abs(self.steps_mode) > 10: return # slow down. each 10th steop - self.steps_mode = 0; - - # all mode names - modenames = self.scales.harmony_get_mode_names() - nr_of_modes = len(modenames) - result = None - - # if not mode is set, get first name - if not self.scales.active_mode: self.scales.active_mode = modenames[0] # "Chromatic" - - for i in range(nr_of_modes): - if modenames[i] == self.scales.active_mode: - result = i - break - if not result is None: - result += step - if result >= nr_of_modes: result = 0 - elif result < 0 : result = nr_of_modes-1 - - new_mode = modenames[result] - self.scales.active_mode = new_mode - else: - logging.error("Bug in set_mode") - # do the magic - self.scales.init_scale(self.scales.tonic, self.scales.active_mode) - # colorize pad array with tonic - self.scales_set_pad_colors() - # Display - scale_n_mode = self.scales.harmony_get_scale_name_with_mode() - self._display.write_xy_mem(scale_n_mode, 0, 2) # just little part with new text - self._display.update_screen() # just second row is dirty_tagged and updated - # because just mode changes, no change of tonic LEDs and Display - - # est color of 64 keyboard pads - def scales_set_pad_colors(self): - # def set_dev_scale_color(self): - # self._leds_rgb.all_off(True) # led_states must not be deleted. is done in next lines. just for debugging - for pad_nr in range(64): - new_note = self.scales.harmony_get_target_note(pad_nr) - # if self.scales.is_tonic_by_midnote(new_note): ### NOT WORKING CONPLETELY - if self.scales.is_tonic_by_padnr(pad_nr): - r = 0; g = 0; b = 255 - # print (f"found: Tonic {new_note}") - else: - r = 200; g = 200; b = 200 - # self.set_pad_rgb(pad_nr, r, g, b) ## OLD FUnction - self._leds_rgb.set_rgb(pad_nr, r, g, b, overlay=False) - pass - - # scale modes own midi_event routine, called by midi_event func - def process_scale_event(self, ev) -> bool: - if not self.device_mode_active == self.DEV_MODE_SCALES: # keyboard modus is selected - return False # we are not in scales mode - - - - ##### event part for sounds - # Filter out note events created by push 1 when touching Knobs and Ribbon - note = ev[1] - if ABL_PAD_START <= note <= ABL_PAD_END: # just note events, which should sound. - # filter for getting any vent that is sound event - - - evtype = (ev[0] >> 4) & 0x0F - - if evtype == self.EV_PITCHBEND: # ribbon working as pitchwheel - self._forward_like_niels_did(ev) - - # processing note events - if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH]: - - # logging.debug(f"Scales mode -BRUMBY") - pad_nr = note -35# translates input note to hardware pad_nr - - # here magic for different sccale layouts happens. - # it translates midi_note events to the translated note_events - note_translated = self.scales.harmony_get_target_note(pad_nr-1) # midinotes are based 0 pad_nr based 1 - - # ocatve_buttons used - note_translated += self.shift_note - - vel = ev[2] # my push1 is insensitive so I double any velocity val - if evtype == self.EV_NOTE_ON: - vel = ev[2] *2 # this creates junk with aftertouch und pitchbend.. - if vel > 255: vel = 255 - new_ev = bytes([ev[0], note_translated, vel]) - #if note_translated % 12 == 0: # Oktave detected - # pass - # for note_on events following. - - self._forward_like_niels_did(new_ev) # - return True # return to caller and mark event as processed - - # here any other ebent - # self.EV_CC, self.EV_CHAN_PRESS, self.EV_SEXSTEM, self.EV_PC - # and ALL events from Pads < PAD_START and Pads > PAD_END - - # we want to process display buttons: - ## helper for display buttons - def helper_set_new_tonic(tonic): - if self.scales.set_new_tonic(tonic): - # yes it changed. update display - scale_n_mode = self.scales.harmony_get_scale_name_with_mode() - self._display.write_xy_mem(scale_n_mode, 0, 2) - self._display.update_screen() - self.scale_update_leds(tonic) - ### - return True - - ### processing of Control Buttons and knobs starts here - ### because we set up push1_consts.py this way, it's so easy - ### to get differnt controls CC,PC,Note_on,Note_of... - ### so we get a very clean event-function. just name and function call. - search_key = [ev[0], ev[1]] # build search key from event - if ev[2] > 0: # just btn down eventes - match search_key: - case ABL.BTN_R2_C1: - helper_set_new_tonic(0); return True - case ABL.BTN_R2_C2: - helper_set_new_tonic(1); return True - case ABL.BTN_R2_C3: - helper_set_new_tonic(2); return True - case ABL.BTN_R2_C4: - helper_set_new_tonic(3); return True - case ABL.BTN_R2_C5: - helper_set_new_tonic(4); return True - case ABL.BTN_R2_C6: - helper_set_new_tonic(5); return True - case ABL.BTN_R2_C7: - helper_set_new_tonic(6); return True - case ABL.BTN_R2_C8: - helper_set_new_tonic(7); return True - case ABL.BTN_R1_C5: - helper_set_new_tonic(8); return True - case ABL.BTN_R1_C6: - helper_set_new_tonic(9); return True - case ABL.BTN_R1_C7: - helper_set_new_tonic(10); return True - case ABL.BTN_R1_C8: - helper_set_new_tonic(11); return True - - # Display Knobs here - # knobs - case ABL.KNOB_7: # scale - self.scales_set_tonic(ev[2]); return True - case ABL.KNOB_8: # mode - self.scales_set_mode(ev[2]); return True - - # Octave Buttons - case ABL.BTN_OCTAVE_UP: - self.shift_note += 12 - case ABL.BTN_OCTAVE_DOWN: - self.shift_note -= 12 - - case _: - return False # event not for any of the defined buttons - - - return False - - -################## END of scales fucntions ########################################################## -############################################################################################################### - -####################################################################################### -### Mixer FUNCTIONS FOR DISPLAY ACTION from zynmixer. ### - - # def mixer_helper_bar(self, value) -> str: - # field_width = 8# width of anzeige - # int_val = int(value * field_width) - # if float(value) > 0.0: # always minimum 1 bar if any sound! - # int_val += 1 - # erg = "".ljust(int_val,chr(6)).ljust(10)[:field_width] - # # erg = "".ljust(int(value*10),"|").ljust(15)[:field_width] - # return erg - - # def mixer_helper_write_to_knobx_fieldy(self, text: any, knob_x:int, field_y:int, as_bar:bool = False): - # """writes to a specified place below a knob - # knob_x is the knob from 0 to 7 (push_1 has 9 knobs, but nith has no display) - # field_y is the row written to see consts: _MIXER_DISP_ROW_* - # text can be text or float value - # """ - # logging.error ("Call deprcated") - # sleep(3) - # if knob_x > 7: - # knob_x = 7 - # logging.error("knob_x bigger 7 not implemented. Sum channel is directed to 7") - # if isinstance(text, (int, float)): # when text of type float or int change it to str - # if as_bar: - # text = self.mixer_helper_float_to_ascii_Bar(text) - # else: - # text = str(text) - - # fields_start_knobs = [0,9, 17,26, 34,43, 51,60] - # knobx_start = fields_start_knobs[knob_x] - # text=text.ljust(8)[:8] # make text with minimal 10 and max 10 chars - # self._display_mixer.write_xy_mem(text, knobx_start, field_y) - - # def mixer_helper_float_to_ascii_Bar(self, value:float): - # fieldlen = 8 - # int_val = int(value * fieldlen) # val is 0.0 to 1.0. we want range 0-7 - # return "".ljust(int_val, chr(6)).ljust(fieldlen) # fill up with spaces to overwrite old values - - def mixer_init(self): - """mixer display functions are called during start. - Mixer is main functionality, so it hast to be setup in intit _function - """ - # create consts for mixer display - self.MIXER_DISP_ROW_VOLUME = 1 - self.MIXER_DISP_ROW_BALANCE = 0 - # NO Mixer has rows 1, 2 exclusiv. doesn't need more - # self.MIXER_DISP_ROW_3 = 2 - # self.MIXER_DISP_ROW_4 = 3 - - self.KNOB_VOLUME = ABL.KNOB_8 - self._mixer_chains_bank = 0 - - self._display_mixer = Feedback_Display(self.idev_out); - self.mixer_set_dev_to_mixermode() - - return - - - def mixer_set_dev_to_mixermode(self): - - # creat private mixer display - - btn_txt_row0 = " Ch 1 Ch 2 Ch 3 Ch 4 Ch 5 Ch 6 Ch 7 Main " - btn_txt_row1 = " " - #btn_txt_row2 = f"|modes here {chr(5)} {chr(6)} {chr(5)}{chr(6)} | |" - #btn_txt_row3 = "| | | | || | | | |" - - btn_text_row2 = self._display_mixer.format_help - - self._display_mixer.write_xy_mem(btn_txt_row0, 0, 0) - # self._display_mixer.write_xy_mem(btn_txt_row1, 0, 1) - # self._display_mixer.write_xy_mem(btn_txt_row2, 0, 2) - # self._display_mixer.write_xy_mem( "Volume Mode".ljust(20)[:15], 0, 2 ) # Mode of knobs - # self._display_mixer.write_xy_mem(btn_txt_row3, 0, 3) - self._display_mixer.update_screen() - - # # paint into test data - # ch1_level = self.zynmixer.zctrls[0]['level'].get_value() # is this a set level or the real sound lovel - # ch1_level_bar = self.mixer_helper_float_to_ascii_Bar(ch1_level) - # first_knob_nr = 0 - # self._display_mixer.write_to_knobx_mem(ch1_level_bar, first_knob_nr, self.MIXER_DISP_ROW_VOLUME) - - self._display_mixer.update_screen()# send display_data to display - - - def mixer_cleanup(self): - pass - - ### just copy the derived functions in the this driver and implement them accordingly - # DONT CHANGE FUNC NAME (is inherited) - def update_mixer_active_chain(self, active_chain): - """Update hardware indicators for active_chain""" - - try: - mix_state = self.zynmixer.get_state() - volume = self.zynmixer.zctrls[0]['level'].get_value() - for c in mix_state.keys(): - if c[:5] == "chan_": - chan_nr = int(c[5:7]) - ch_level = self.zynmixer.zctrls[chan_nr]['level'].get_value() # we use level from here, so we no field exists - # ch_level_bar = self._display_mixer. mixer_helper_float_to_ascii_Bar(ch_level) - self._display_mixer.write_to_knobx_mem(ch_level, chan_nr, self.MIXER_DISP_ROW_VOLUME, as_bar = True) - - # write names - # Check if chain exists - # if zynmixer.get_chain_level(chain_index) is not None: - # Namen von der Engine holen - #engine_index = self.zynmixer.get_chain_engine(chan_nr) - #engine_info = lib_zyncore.get_engine_info(engine_index) - # name = engine_info.get('name', f"CH{chan_nr}") - # name = self.zynmixer.get_chain_name(chan_nr) - # name = self.zynmixer. - self._display_mixer.update_screen() - # logging.error(f"not implemented active_chain: {active_chain}") - return - except Exception as e: - logging.error(f"Error in update_mixer_active_chain: {e}") - logging.exception(traceback.format_exc()) - - - - - - # DONT CHANGE FUNC NAME (is inherited) - def update_mixer_strip(self, chan, symbol, value): - """Update hardware indicators for a mixer strip: mute, solo, level, balance, etc. - *SHOULD* be implemented by child class - - chan - Mixer strip index - symbol - Control name - value - Control value - we use just volume - """ - - try: - match symbol: - case 'level': - if chan > 7: - chan = 7 - self._display_mixer.write_to_knobx_mem(value, chan, self.MIXER_DISP_ROW_VOLUME, as_bar=True) - self._display_mixer.update_screen() - return # Wichtig: Return nach erfolgreicher Verarbeitung! - - case 'balance': - # # Implementierung für balance - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # self._display_mixer.update_screen() - pass - - case 'mute': - # # Implementierung für mute - # if value == 1: - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # else: - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # self._display_mixer.update_screen() - pass # update bar display - - case 'solo': - # Implementierung für solo - # if value == 1: - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # else: - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # self._display_mixer.update_screen() - pass - - case 'mono': - # # Implementierung für mono - # if value: - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # else: - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # self._display_mixer.update_screen() - pass - - case 'm+s': # Mono / Stereo ??? what does that mean - # # Implementierung für m+s - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # self._display_mixer.update_screen() - pass - - case 'phase': - # # Implementierung für phase - # self._display_mixer.write_to_knobx_mem("", chan, self.MIXER_DISP_ROW_VOLUME) - # self._display_mixer.update_screen() - pass - - case _: - # Fall für unbekannte symbols - logging.debug( - f"Update mixer strip for {type(self).__name__}: UNKNOWN SYMBOL! chan: {chan}; symbol: {symbol} value: {value}") - return - - # Diese Zeile wird nur erreicht, wenn ein Case gematcht aber nicht behandelt wurde - logging.debug( - f"Update mixer strip for {type(self).__name__}: NOT IMPLEMENTED! chan: {chan}; symbol: {symbol} value: {value}") - - except Exception as e: - logging.error(f"Error in update_mixer_strip: {e}") - logging.exception(traceback.format_exc()) - - - - def process_mixer_event(self, ev) -> bool: - # turning knob changes now volume from chanel - - if len(ev) != 3: return # we meed 3 bytes - - # Set Mixer Level from KNOBS 1 to 8 - search_knob = [ev[0], ev[1]] - if search_knob in [ABL.KNOB_1, ABL.KNOB_2, ABL.KNOB_3, ABL.KNOB_4, - ABL.KNOB_5, ABL.KNOB_6, ABL.KNOB_7, ABL.KNOB_8 - ]: - return self._update_control("level", ev, 0, 100) - - - #self.zyn - #self.zynmixer.setlevel() - pass - - - def _update_control(self, type:str, ev:bytes, minv, maxv): - ccnum = ev[1] & 0x7F - ccval = ev[2] & 0x7F - # if self.shift: - # # Only main chain is handled with SHIFT, ignore the rest - # if ev[:2] == bytes(self.KNOB_VOLUME): - # return False - # mixer_chan = 255 # is mixer mains volume - if ev[:2] == bytes(self.KNOB_VOLUME): # detect mains knob - mixer_chan = 255 - else: - # _chains_bank shifts the knob to the visual bank in mixer panel - index = (ccnum - ABL.KNOB_1[1]) + self._mixer_chains_bank * 8 - chain = self.chain_manager.get_chain_by_index(index) - if chain is None or chain.chain_id == 0: - return False - mixer_chan = chain.mixer_chan - - if type == "level": - value = self.zynmixer.get_level(mixer_chan) # get value - set_value = self.zynmixer.set_level # get function belonging to that chain - elif type == "balance": - value = self.zynmixer.get_balance(mixer_chan) - set_value = self.zynmixer.set_balance # get function belonging to that chain - else: - return False - - # NOTE: knobs are encoders, not pots (so ccval is relative) - value *= 100 # translat value 0.0 .. 1.. to 0.0 .. 100.0 - value += ccval if ccval < 64 else ccval - 128 # translate knob values to plus or minus valus and add - value = max(minv, min(value, maxv)) # check and set range - set_value(mixer_chan, value / 100) # move val back to 0..1 range # use saved function to set chains target - return True - -### END of Mixer functions. ### -######################################################################### - - -######################################################################### -### Start of Sequencer / Pad Functions ### - def process_sequencer_event(self, ev) -> bool: - """event function in sequencer state""" - # if using shift button with knob, then not following we are not in any mode - # if not self.device_mode_active == self.DEV_MODE_MIXER: # keyboard modus is selected - # return False # we ignored here any event, we are not in Sequencer mode - - - - # tzynseq updates LED states - # we have update pad LED to show state - # DONT CHANGE FUNC NAME (is inherited) - def update_seq_state(self, bank, seq, state, mode, group): - try: - # return - # Onlyreturn if Push1 driver is not in sequencer_mode_view - if not self.device_mode_active == self.DEV_MODE_PAD: - return - - # logging.info(f"BRUMBY bank={bank}; seq={seq}; state={state}; mode={mode}; group:{group}") - if self.idev_out is None or bank != self.zynseq.bank: - return - - col, row = self.zynseq.get_xy_from_pad(seq) - note = ABL_PAD_END +1 -(row+1) * 8 + col - # logging.info(f"BRUMBY-P col={col}; row={row} ergibt note:{note}") - - # Alles abfangen, was ausserhalb des Pad-Bereichs ist BRUMBY_NEU. - #if (note > ABL_PAD_END) or (note < ABL_PAD_START): - # return - - try: - if mode == 0 or group > 16: - chan = 0 - vel = 0 - elif state == zynseq.SEQ_STOPPED: - chan = 0 - vel = self.PAD_COLOURS[group] - elif state == zynseq.SEQ_PLAYING: - chan = 2 - vel = self.RUNNING_COLOR - elif state in [zynseq.SEQ_STOPPING, zynseq.SEQ_STOPPINGSYNC]: - chan = 1 - vel = self.STOPPING_COLOUR - elif state == zynseq.SEQ_STARTING: - chan = 1 - vel = self.STARTING_COLOUR - - else: # Wenn nichts passt Pad-Beleuchtung ausschalten - chan = 0 - vel = 0 - - except Exception as e: # Bei Fehler Pad-beleuchtung ausschalten - chan = 0 - vel = 0 - - # set pad color with velocity value - lib_zyncore.dev_send_note_on(self.idev_out, chan, note, vel) - except ValueError as e: - print(f"Fehler aufgetreten: {e}") - - # for LED feedback bei pad mode (Sequencer) - # DONT CHANGE FUNC NAME (is inherited) - def refresh(self): # form zynseq classe - # if not filtered, the pad loop kills any other LED setup - if self.device_mode_active == self.DEV_MODE_PAD: - return super().refresh() - - # DONT CHANGE FUNC NAME (is inherited) - def pad_off(self, col, row): - # note = 96 + row * 16 + col # statt 96 -> 91 für Push - note = ABL_PAD_END +1 -(row+1) * 8 + col # recalculate midi note from col and row - # logging.info(f"BRUMBY: row={row}; col={col} pad-note={note}") - lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0) - - # scenebuttons = right from pads - def sequencer_set_scene(self, ccnum): - # seams inconsistent, GUI says Scene. Api is: select Bank, or I misunderstood - self.zynseq.select_bank (8- (ccnum - 36)) - # change LED state - for t in [ 36,37,38,39,40,41,42,43]: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, t, ABL.MONO_LED_DIM) # 2! - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL.MONO_LED_LIT_BLINK_FAST) # 2! - return True - - - def process_sequencer_event(self, ev) -> bool: - """event function in sequencer state""" - if not self.device_mode_active == self.DEV_MODE_PAD: # keyboard modus is selected - return False # we ignored here any event, we are not in Sequencer mode - - cc = ev[1] # controller used to calculate bank. keys are in line - # cc_val=ev[2] - - search_key = [ev[0], ev[1]] - if ev[2] > 0: # just btn down eventes - match search_key: - case ABL.BTN_BEAT_1_QUATER: - return self.sequencer_set_scene(cc) - case ABL.BTN_BEAT_2_QUATER_T: - return self.sequencer_set_scene(cc) - case ABL.BTN_BEAT_3_EIGHTH: - return self.sequencer_set_scene(cc) - case ABL.BTN_BEAT_4_EIGHTH_T: - return self.sequencer_set_scene(cc) - case ABL.BTN_BEAT_5_SIXTEENTH: - return self.sequencer_set_scene(cc) - case ABL.BTN_BEAT_6_SIXTEENTH_T: - return self.sequencer_set_scene(cc) - case ABL.BTN_BEAT_7_THIRTYSECOND: - return self.sequencer_set_scene(cc) - case ABL.BTN_BEAT_8_THIRTYSECOND_T: - return self.sequencer_set_scene(cc) - case _: - pass - - - evtype = (ev[0] >> 4) & 0x0F - note = ev[1] & 0x7F - - ### Program Change Event from Push 1 # It doesn't send such !!! just for explanatioin - # We filter them out. Push 1 has no midi in and sends nor PC. - # Or should we leave them in. - if evtype == self.EV_PC: # 0xC: - ## val1 = ev[1] & 0x7F - ## self.zynseq.select_bank(val1 + 1) #### That would shange Bank /Scene in Sequencer. We do it with Beat_Buttons - return True # - - - # we do pad calculation with pads numbered woth control registers - if evtype == self.EV_NOTE_ON: # 0x9: # fitler just for note_on events - # all Pads send note_on events - # push are oriented buttom left to top right with cc 36 to 99 eq C2 to Eb7 - try: - pad_nr = note - ABL_PAD_START# eq C2 or ABL.PAD_36# so padnr ranges from 0 - 63 eq (range(64) - col = pad_nr // 8 # - row = pad_nr % 8 # - col = 7 - col; # midi notes start from bottom, so recalculate row - - # don't understand following XXXXXXXXXXXXXXXXX - pad = row * self.zynseq.col_in_bank + col - logging.debug(f"BRUMBY: row={row}; col={col}; pad={pad}") - if pad < self.zynseq.seq_in_bank: - # this is the complete magic. Start and stop a track in a scene (bank) - self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) # yes Scene is bank !!! - return True - except: - pass - - - ############### End of derived Sequencer Functions. ##################### - ################################################################################################### - - # Just for me a helper function to set all pads off - def pads_off(self): - - # logging.debug("BRUMBY: pads_off") - for row in range(self.rows): - for col in range(self.cols): - self.pad_off(col, row) - - - - # https://discourse.zynthian.org/t/driver-for-ableton-push-1-first-steps/12166/8 - def _forward_like_niels_did(self, ev): - - # Direct keybed to chains - #if (channel == 1): - chain = self.chain_manager.get_active_chain() - # print(chain.midi_chan) - # @todo: find out how to get 'last' active chain, for now: just back out. - - if chain.midi_chan is None: - return False - - status = (ev[0] & 0xF0) | chain.midi_chan - self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) - return True - - # if not processed you call - # return super()._on_midi_event(ev)` - - def set_device_mode_new(self, new_mode): - try: - if new_mode == self.device_mode_active: return # devmode was same - - # clean up old device state - # NO USE RETURNS - match self.device_mode_active: - case self.DEV_MODE_MIXER: - # deinit mixer - lib_zyncore.dev_send_ccontrol_change( - self.idev_out, 0, ABL.BTN_VOLUME[1], ABL.MONO_LED_LIT) - self.mixer_cleanup() - - case self.DEV_MODE_PAD: - # there is no cleanup. do following - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_USER[1], ABL.MONO_LED_LIT) - self.pads_off() - - case self.DEV_MODE_SCALES: - # deinit scales mode - self.scales_cleanup() - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) - self.pads_off() # clean up visible state. all pad leds off - - # now you can save new active mode - self.device_mode_active = new_mode - - # Now Setup new device mode - # HERE USE RETURNS - match new_mode: - case self.DEV_MODE_MIXER: - # init mixer - # lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_VOLUME[1], ABL.MONO_LED_LIT_BLINK) - return self.mixer_set_dev_to_mixermode() - - case self.DEV_MODE_PAD: - self.refresh() # refreshe LEDs for Sequencer mode of this driver. - # there is no clean_up method. so do following - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_USER[1], ABL.MONO_LED_LIT_BLINK) - - case self.DEV_MODE_SCALES: - self.scales_set_dev_to_scales_mode() - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) - - - case _: - # code not defined - logging.error("DEVICE Mode not defined. Programming Error") - - # if not done till now, mark as succed - return True # Whatever event is processed - - except Exception as e: - logger.error(f"Error in set_device_mode_new: {e}") - logger.exception(traceback.format_exc()) - - - - - - def midi_event(self, ev): - - ### For debugging purposes block can be commented out ! - dbg = True - if len(ev) > 1 and dbg: - search_key = [ev[0], ev[1]] # ev to search_key - btn_name = self.button_name_from_midi_event(search_key) # ev[0] and ev[1] fields are proved. so any status can be a button - if not btn_name == "": # just log known btns - logger.debug(f"Button: {btn_name} on chan. {ev[0] & 0x0F} gives midi_event: {hex(ev[0])} {hex(ev[1])} {hex(ev[2])} = {int(ev[1])}, {int(ev[2])}, {int(ev[2])}") - - evtype = None - chan_or_instruction = None - note_or_register = None - val_or_vel = None - - ### end of debug - - if len(ev) > 1: # Btn is possible - - if len(ev) > 2: - val_or_vel = ev[2] - is_key_push = val_or_vel > 0 - - search_key = [ev[0], ev[1]] # ev to search_key - match search_key: - case None: - pass - case ABL.BTN_SHIFT: # as momentary button NOT toggle! has to be hold for functions change - self.shift = is_key_push # set shift variable. but just momenatary - # visual feedback with button LED - if self.shift: - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_LIT_BLINK) - else: # key is teleased - lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_DIM) - return True # event processed. No further action required - - case ABL.BTN_VOLUME: # mode change to mixer? It isn't best chosen. - if is_key_push: - return self.set_device_mode_new(self.DEV_MODE_MIXER) - - case ABL.BTN_SCALES: - if is_key_push: - return self.set_device_mode_new(self.DEV_MODE_SCALES) - - case ABL.BTN_USER: - if is_key_push: - return self.set_device_mode_new(self.DEV_MODE_PAD) - case _: - pass - - # try to process the ev with active mode - match self.device_mode_active: - case self.DEV_MODE_MIXER: - if self.process_mixer_event(ev): # dom't return if False - return True - case self.DEV_MODE_PAD: - if self.process_sequencer_event(ev): - return True - case self.DEV_MODE_SCALES: - if self.process_scale_event(ev): - return True - case _: - pass # no actual devicemode - - # if nothing els then - #if self.process_scale_event(ev): - # return True - - # now the Gui events. - # Gui events moved to: - if self.process_gui_events(ev): return True - - # nothing below the line ??? - return False # that should be all - - - - ######### GUI EVENTS #################################### - # to clean up the code GUI events are processed here - def process_gui_events(self,ev) -> bool: - - # on this device any button or knob we use sends 3-byte-events - # otherwise event is no control - if not len(ev) >= 3: return False - - # bild button search event - search_key = [ev[0], ev[1]] - data_val = ev[2] & 0x7F - - # TODO remove if knob ease is fine - # # make left turs on knobs negative - # def helper_knob_calculation(ccval): - # if ccval > 64: ccval -= 128 - # return ccval - # # this could be changed to - # # delta = self._knobs_ease.feed(btn_id, ev[2], self._is_shiftedxxx) - # data_val_for_knobs = helper_knob_calculation(data_val) - - match search_key: - - # Knobs - case ABL.KNOB_1: - # translate 127 to -1 and slow down - delta = self._knobs_ease.feed(bytes(ABL.KNOB_1), data_val, is_shifted=False) - self.state_manager.send_cuia("ZYNPOT", [0, delta]); return True - # self.state_manager.send_cuia("ZYNPOT", [0, data_val_for_knobs]); return True - case ABL.KNOB_2: - delta = self._knobs_ease.feed(bytes(ABL.KNOB_1), data_val, is_shifted=False) - self.state_manager.send_cuia("ZYNPOT", [1, delta]); return True - case ABL.KNOB_3: - delta = self._knobs_ease.feed(bytes(ABL.KNOB_3), data_val, is_shifted=False) - self.state_manager.send_cuia("ZYNPOT", [2, delta]); return True - case ABL.KNOB_4: - delta = self._knobs_ease.feed(bytes(ABL.KNOB_4), data_val, is_shifted=True) - self.state_manager.send_cuia("ZYNPOT", [3, delta]); return True - # self.state_manager.send_cuia("ZYNPOT", [3, data_val_for_knobs]); return True - case _: pass - - if data_val > 0: # just key-down events - match search_key: - # Buttons - case ABL.BTN_OK, ABL.BTN_R1_C3: - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [3,"S"]); return True - case ABL.BTN_R1_C1: - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [0,"S"]) ; return True - case ABL.BTN_R1_C2: - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [1,"S"]) ; return True - case ABL.BTN_R1_C3: - self.state_manager.send_cuia("V5_ZYNPOT_SWITCH", [2,"S"]) ; return True - - case ABL.BTN_ESC: - self.state_manager.send_cuia("BACK"); return True - case ABL.BTN_RIGHT: - self.state_manager.send_cuia("ARROW_RIGHT"); return False - case ABL.BTN_LEFT: - self.state_manager.send_cuia("ARROW_LEFT"); return False - case ABL.BTN_UP: # CC46 - self.state_manager.send_cuia("ARROW_UP"); return True - case ABL.BTN_DOWN: - self.state_manager.send_cuia("ARROW_DOWN"); return True - case ABL.BTN_START: # ehemals ABL_PLAY: - if self.shift: # shift button pressed - self.state_manager.send_cuia("TOGGLE_MIDI_PLAY"); return True - else: - self.state_manager.send_cuia("TOGGLE_PLAY"); return True - case ABL.BTN_REC: # ABL_REC: - if self.shift: - self.state_manager.send_cuia("TOGGLE_MIDI_RECORD"); return True - else: - self.state_manager.send_cuia("TOGGLE_RECORD"); return True - case _: pass - - return False # event is not processed - - - def button_name_from_midi_event(self, ev): ###, button_event): # button_event is a Constant from import abl. - # create key_data from midi event - # if too slow, we have to revert the array to a named array with buttonevent as name - if len(ev) < 2 : return None # event is too short to get two byte data. Time event or so? - if len(ev) > 2: data = ev[2] - search_key = [ ev[0] & 0xF0, ev[1] ] - for name in dir(ABL): # all vars as textstring - if not name.startswith('__'): # no attributes with '__' - attr = getattr(ABL, name) - if attr == search_key and name.isupper(): - # logging.debug(f"midi_event {ev} {ev[0]}, {ev[1]}, from Button with name: {name} and value: {data}") - return name - # logging.debug(f"midi_event {ev} from Button not defined with value: {data}") - return "" # "" - - - def set_pad_rgb(self, pad_nr: int, r:int ,g:int ,b:int): - logging.error(" set_pad_rb aufgerufen. not implemented") - # # Sysex : 240,71,127,21,4,0,8,,0,,,,,,,247 - # # pad = 0-71 NICHT PAD_36 - PAD_99 - # # blogspot.com - # # To set a pad color to a RGB(0-255) value, the RGB values need to be set into "Push" format, for example: - # # r1 = r /(integer division) 16 - # # r2 = r %(modulo) 16 - # # So a value of R132 would become: r1=8 r2=4. - # # The pad index if from 0 to 71, zero being the bottom left pad, all the way up to the second row button to the right (second row of buttons is RGB). - # if r > 255: r = 255; - # if r < 0: r = 0 - # if g > 255: g = 255; - # if g < 0: g = 0 - # if b > 255: b = 255; - # if b < 0: b = 0 - # if not 0 <= pad_nr <= 64: - # logging.error(f"Padnr wrong. not in 1..64 pad_nr = {pad_nr}") - # return False - # - # r1= r // 16 ; r2= r % 16 - # g1= g // 16 ; g2= g % 16 - # b1= b // 16 ; b2= b % 16 - # sysex = bytes ([240,71,127,21,4,0,8,pad_nr,0,r1,r2,g1,g2,b1,b2,247] ) - # lib_zyncore.dev_send_midi_event(self.idev_out, sysex, len(sysex)) - - -# ------------------------------------------------------------------------------ - - -##################################################################################### -#### Special Class for Ableton Pushh 1 Display - -class Feedback_Display: - - #// Special Dispay Characters - # char0) bis cahr(31) order by Symbol_name - # char(32) to char(127) is like ASCII - # partly from https://pushmod.blogspot.com has no valid evmail adress, so I couldnt send him the updated list! - # login per google didnt work on his blog for safte reasons. What a pitty, I wanted to thank for his work with the complete list - # of symbos - - DISP_ARROW_UP = 0 # ↑ (U+2191) - DISP_ARROW_DOWN = 1 # ↓ (U+2193) - DISP_ARROW_RIGHT = 30 # → (U+2192) - DISP_ARROW_LEFT = 31 # ← (U+2190) - - DISP_HORIZONTAL_LINES_THREE_STACKED = 2 # ≡ (U+2261) - DISP_HORIZONTAL_LINE_LOW = 95 # _ (U+005F) Lowbar - DISP_HOIZONTAL_LINE_SPLIT = 6 # ╌ (U+254C) LIGHT DOUBLE DASH HORIZONTAL - - DISP_VERTICAL_LINE_AND_HORIZONTAL_LINE = 3 # ├ (U+251C) - DISP_HORIZONTAL_LINE_AND_VERTICAL_LINE = 4 # ┤ (U+2524) - - DISP_VERTICAL_LINES_TWO = 5 # ║ (U+2551) - DISP_VERTICAL_LINE_MID = 174 # | (U+007C) - DISP_SPLIT_VERTICAL_LINES = 8 # ⫼ (U+2AFC) - - DISP_FOLDER_SYMBOL = 7 # 📁 (U+1F4C1) - DISP_FLAT_SYMBOLS = 27 # ♭ (U+266D) - DISP_THREE_SIDE_BY_SIDE_DOTS = 28 # ⋮ (U+22EE) - DISP_FULL_BLOCK = 29 # █ (U+2588) - DISP_LITTLE_BOX_SHIFTED_HIGH_MIDDLE = 9 # ▫ (U+25AB) Little box shifted high middle - - DISP_AE_UC = 10 # Ä (U+00C4) - DISP_CEDILLE_UC = 11 # Ç (U+00C7) - DISP_OE_UC = 12 # Ö (U+00D6) - DISP_UE_UC = 13 # Ü (U+00DC) - DISP_SZ = 14 # ß (U+00DF) - DISP_A_GRAVE = 15 # à (U+00E0) - DISP_AE_LIC = 16 # ä (U+00E4) - DISP_CEDILE = 17 # ç (U+00E7) - DISP_E_LC_GRAVE = 18 # è (U+00E8) - DISP_E_LC_EGUT = 19 # é (U+00E9) - DISP_E_LC_CIRCUM = 20 # ê (U+00EA) - DISP_I_LC_TREMA = 21 # ï (U+00EF) - DISP_N_LC_WITH_TILDE = 22 # ñ (U+00F1) - DISP_OE_LC = 23 # ö (U+00F6) - DISP_DIV_STROKE = 24 # ⁄ (U+2044) - DISP_CIRC_WITH_DIV_STROKE = 25 # Ø (U+00D8) - DISP_UE_LC = 26 # ü (U+00FC) - - - # with 32 (SPACE) starts pritable part from ASCII-Table - akai_to_unicode = { - # Pfeile - 0: "↑", # DISP_ARROW_UP (U+2191) - 1: "↓", # DISP_ARROW_DOWN (U+2193) - 30: "→", # DISP_ARROW_RIGHT (U+2192) - 31: "←", # DISP_ARROW_LEFT (U+2190) - - # Horizontale Linien - 2: "≡", # DISP_HORIZONTAL_LINES_THREE_STACKED (U+2261) - 6: "╌", # DISP_HOIZONTAL_LINE_SPLIT (U+2550) - 95: "_", # DISP_HORIZONRAL_LINE_LOW (U+005F) # might not look same - - # Kombinierte Linien - 3: "┤", # DISP_VERTICAL_LINE_AND_HORIZONTAL_LINE (U+2524) - 4: "├", # DISP_HORIZONTAL_LINE_AND_VERTICAL_LINE (U+251C) - - # Vertikale Linien - 5: "║", # DISP_VERTICAL_LINES_TWO (U+2551) - 8: "⫼", # DISP_SPLIT_VERTICAL_LINES (U+2AFC) - 174: "|", # DISP_VERTICAL_LINE_MID (U+007C) # might not look same - - # Symbole - 7: "📁", # DISP_FOLDER_SYMBOL (U+1F4C1) - 27: "♭", # DISP_FLAT_SYMBOLS (U+266D) - 28: "⋮", # DISP_THREE_SIDE_BY_SIDE_DOTS (U+22EE) - 29: "█", # DISP_FULL_BLOCK (U+2588) - 9: "▫", # DISP_HIGH_LITTLE_BOX (U+25AB - Kleines hochgestelltes Kästchen) - - # Umlaute und Sonderzeichen - 10: "Ä", # DISP_AE_UC (U+00C4) - 11: "Ç", # DISP_CEDILLE_UC (U+00C7) - 12: "Ö", # DISP_OE_UC (U+00D6) - 13: "Ü", # DISP_UE_UC (U+00DC) - 14: "ß", # DISP_SZ (U+00DF) - 15: "à", # DISP_A_GRAVE (U+00E0) - 16: "ä", # DISP_AE_LC (U+00E4) - 17: "ç", # DISP_CEDILE (U+00E7) - 18: "è", # DISP_E_LC_GRAVE (U+00E8) - 19: "é", # DISP_E_LC_EGUT (U+00E9) - 20: "ê", # DISP_E_LC_CIRCUM (U+00EA) - 21: "ï", # DISP_I_LC_WITH_3_POINTS_ABOVE (U+00EF - i mit Trema) - 22: "ñ", # DISP_N_LC_WITH_TILDE (U+00F1) - 23: "ö", # DISP_OE_LC (U+00F6) - 24: "⁄", # DISP_DIV_STROKE (U+2044) - 25: "Ø", # DISP_CIRC_WITH_DIV_STROKE (U+00D8) - 26: "ü", # DISP_UE_LC (U+00FC) - } - - format_help = b'123456789A123456789B123456789C123456789D123456789E123456789F123456789' - - - display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten - # _disp_line_dirty =[False, False, False, False] - - - def __init__ (self, idev_out): - # self.dbg = True - self.idev_out = idev_out - self._disp_line_dirty =[False, False, False, False] - # if self.dbg: - #logging.error(f"BRUMBY: Class Display instantiiert") - - - - def clear (self): - """Overwrites whole display with ascii 32""" - # logging.info(f"Display.clear: end of func idev_out={self.idev_out}") - - # clear out display_memory with blanks. - self.display_mem = [[32] * 68 for _ in range(4)] # 4 Zeilen mit 68 Spalten - - # SYSEX_ZEILE_LÖSCHEN = 240,71,127,21,<28+line(0-3)>,0,0,247 - s0 = bytes([240,71,127,21,28,0,0,247]) # Zeile 0 - s1 = bytes([240,71,127,21,29,0,0,247]) # Zeile 1 - s2 = bytes([240,71,127,21,30,0,0,247]) # Zeile 2 - s3 = bytes([240,71,127,21,31,0,0,247]) # Zeile 3 - for x in [s0, s1, s2, s3]: - lib_zyncore.dev_send_midi_event(self.idev_out, x, len(x)) - sleep(0.01) - self._disp_line_dirty =[False, False, False, False] - - - def update_screen (self): - # move display memory to display with sysex - for row in range(4): - if self._disp_line_dirty[row]: - #msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) - text = bytes(self.display_mem[row]) - text_len = len(text) - col = 0 - # here the magic happens and sysex is cunstructed - # 240, 71, 127, 21, <24+line(0-3)>,0, ,,, 247 - msg = bytes([240, 71, 127, 21, row+24, 0, text_len+1, col]) + text+ bytes([247]) - # logging.error(f"BRUMBY: Display.update SYSEX={msg}") - lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - sleep(0.01) - self._disp_line_dirty[row] = False - return - - - def write_xy_mem (self, text, col_in:int, row_in:int): - # writes to display memory at Position col_in, row_in in display memory und auf Display - # mit update - - #convert text to bytes - if isinstance(text, str): - # print("Die Variable ist ein String (Text)") - text = text.encode() - elif isinstance(text, bytes): - # print("Die Variable ist Bytes") - pass # is fine - elif isinstance(text, (int, float)): # is a number? - text = str(text).encode() - else: - # print("type error") - text = "Typeerror in textconversion".encode() - - # Koordinaten prüfen - if(row_in > 3): row_in = 3 - if(row_in < 0): row_in = 0 - if(col_in > 63): col_in = 63 - if(col_in < 0): col_in = 0 - - # Textlänge prüfen, ob im erlaubten Bereich. - text_len = len(text) - if text_len + col_in > 68 : text = text[:68-col_in] # Rest abschneiden - text_len = len(text) - - self.display_mem[row_in][col_in:col_in+text_len] = list(text) - self._disp_line_dirty[row_in] = True - # self.update() - return - - - def write_to_knobx_mem(self, text: any, knob_x:int, row_y:int, as_bar:bool = False): - """writes to a specified place below a knob - knob_x is the knob from 0 to 7 (push_1 has 9 knobs, but nith has no display) - field_y is the row written to see consts: _MIXER_DISP_ROW_* - text can be text or float value - """ - if knob_x > 7: - knob_x = 7 - logging.error("program error: knob_x bigger 7 not implemented. Sum channel is directed to 7") - - if isinstance(text, (int, float)): # when text of type float or int change it to str - # text contains float or int - if as_bar: - # create an ascii bar representation - fieldlen = 8 - if float(text) == 0.0: text = "".ljust(fieldlen) # 0 vals as "off" - else: - int_val = int(text * fieldlen) # val is 0.0 to 1.0. we want range 0-7 - if int_val == 0: text=".".ljust(fieldlen) # if minimal signal show a dot - else: text = "".ljust(int_val, chr(self.DISP_VERTICAL_LINES_TWO)).ljust(fieldlen) # fill up with spaces to overwrite old values - else: - text = str(text).ljust(fieldlen) - - fields_start_knobs = [0,9, 17,26, 34,43, 51,60] - knobx_start = fields_start_knobs[knob_x] - text=text.ljust(8)[:8] # make text with minimal 10 and max 10 chars - self.write_xy_mem(text, knobx_start, row_y) - - - def contrast (self, i=None) -> int: - """ - Setzt oder liest den Kontrast des Geräts via SysEx. - - Args: - i (int, optional): Der gewünschte Kontrastwert (typischerweise 0-127). - Wenn None, wird der aktuelle Kontrast gelesen. - Returns: - int: Der aktuelle Kontrastwert (nach Setzung oder Abfrage). - Raises: - ValueError: Wenn der Kontrastwert außerhalb des gültigen Bereichs liegt. - """ - # Überprüfen, ob ein Wert zum Setzen übergeben wurde - if i is not None: - - if i < 0 : i = 0 - if i > 63: - i = 63 - logging.error(f"Constrast values more than 63, seem to do nothing. values set to 63") - - - # set contrast - # 240,71,127,21,122,0,1,, 247 - msg = bytes([240,71,127,21,122,0,1,i , 247]) - lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - return i - - ### send sysexrequest - ### Contrast request 240,71,127,21,122,0,0,247 - # msg = bytes([240,71,127,21,122,0,0,247]) - # lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - ### Return Contrast. Not implemented. Must be in event chain ais sysex anwser - return None - - - - def brightnes (self, i=None) -> int: - logging.error(f"BRUMBY") - - if i is not None: - - if i < 9: i = 0 - if i > 63: - i = 63 - logging.error(f"Brightnes values more than 63, seem to do nothing. values set to 63") - - logging.error(f"BRUMBY brightnes={i}") - - # set brightnes - # 240,71,127,21,124,0,1,,247 - - msg = bytes([240,71,127,21,124,0,1,i , 247]) - lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - logging.error(f"BRUMBY zu brightnes={i} geändert") - - - # send sysexrequest - # Brightness request 240,71,127,21,124,0,0,247 - # commented out; getting return value not implemented yet. don't know how to - # msg = bytes([240,71,127,21,124,0,0,247]) - # lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) - - # Return Brightnes. Not implemented. Answer comes as sysex in event chain - return None - - def first_screen(self): - self.clear() - # self.brightnes(36) - sleep(0.1) - #self.write_xy_mem(b'* Pot 1 * Pot 2 ** Pot 3 * Pot 4 *', 0,0) - # Positionierungshilfe - #self.write_xy_mem(b'123456789A123456789B123456789C123456789D123456789E123456789F123456789', 0,1) - self.write_xy_mem(b'** Zynthian Push1Driver 0.1 **', 17,2) - self.write_xy_mem(b'++ Make MusicNot War ++', 20,3) - self.update_screen() - return - - - -# -------------------------------------------------------------------------- -# Feedback LEDs controller -# -------------------------------------------------------------------------- -class Feedback_Mono_LEDs: - #Takt Notenlängen Pfeile Track-Modifier Copy/Del/undo - _all_mono = [3,9, 28,29, 36,37,38,39,40,41,42,43, 44,45,46,47, 48,49,50,51,52,53,54,55,56,57,58,59,61,62,63, 85,86,87,88,89,90, 110,111,11,113,114,115, 116,117,118,119, ] - - def __init__(self, idev): - self._idev = idev - self._state = {} - self._timer = RunTimer() - - def all_off(self, overlay = False): - for note in self._all_mono: - lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, 0) - if not overlay: - self._led_state[note] = 0 - return - - - def set_mono(self, note:int, grey_val:int, overlay=False): - lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, grey_val) # grey_val something of ABL.MONO_LED_DIM) - if not overlay: - self._led_state[note] = 0 - return - - def refresh_one(self, note): - if self._led_state[note]: # is one saved? - lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, self._led_state[note]) - - def refresh(self): - for note in self._all_mono: - if self._led_state[note]: - lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, self._led_state[note]) - - -class Feedback_Bi_LEDs: - def __init__(self, idev): - self._idev = idev - self._state = {} - self._timer = RunTimer() - - def all_off(self): - pass - -# RGB LED Class for the pads rgb-LEDs -class Feedback_RGB_LEDs: - - # _led_states = {} - - def __init__(self, idev): - self._idev = idev - self._state = {} - self._timer = RunTimer() - self._led_state = {} - - def all_off(self, overlay): - for pad_nr in range(ABL_PAD_END+1-ABL_PAD_START): - self.set_rgb(pad_nr,0,0,0, overlay) - if not overlay: - self._led_state[pad_nr] = [0,0,0] - return - - def refresh(self): # whole array of pads - for pad_nr in range(ABL_PAD_END+1-ABL_PAD_START): - self.refresh_one(pad_nr) - - - def refresh_one(self, pad_nr): # get back to saved value - self.set_rgb(pad_nr, self._led_state[pad_nr][0], self._led_state[pad_nr][1], self._led_state[pad_nr][2]) - - - def off_col_row(self, col, row): - # note = 96 + row * 16 + col # statt 96 -> 91 für Push - note = ABL_PAD_END +1 -(row+1) * 8 + col # recalculate midi note from col and row - # logging.info(f"BRUMBY: row={row}; col={col} pad-note={note}") - lib_zyncore.dev_send_note_on(self._idev, 0, note, 0) # this is palette mode. - - def set_rgb(self, pad_nr: int, r:int ,g:int ,b:int, overlay=False): - # Sysex : 240,71,127,21,4,0,8,,0,,,,,,,247 - # pad = 0-71 NICHT PAD_36 - PAD_99 - # blogspot.com - # To set a pad color to a RGB(0-255) value, the RGB values need to be set into "Push" format, for example: - # r1 = r /(integer division) 16 - # r2 = r %(modulo) 16 - # So a value of R132 would become: r1=8 r2=4. - # The pad index if from 0 to 71, zero being the bottom left pad, all the way up to the second row button to the right (second row of buttons is RGB). - if r > 255: r = 255; - if r < 0: r = 0 - if g > 255: g = 255; - if g < 0: g = 0 - if b > 255: b = 255; - if b < 0: b = 0 - if not 0 <= pad_nr <= 64: - logging.error(f"Padnr wrong. not in 1..64 pad_nr = {pad_nr}") - return False - - # self._led_state.setdefault(pad_nr, []).append(i) - if not overlay: - self._led_state[pad_nr] = [r,g,b] - - r1= r // 16 ; r2= r % 16 - g1= g // 16 ; g2= g % 16 - b1= b // 16 ; b2= b % 16 - sysex = bytes ([240,71,127,21,4,0,8,pad_nr,0,r1,r2,g1,g2,b1,b2,247] ) - # lib_zyncore.dev - lib_zyncore.dev_send_midi_event(self._idev, sysex, len(sysex)) - - diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py new file mode 100644 index 000000000..ef5c82c15 --- /dev/null +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py @@ -0,0 +1,1319 @@ +#! /zynthian/venv/bin/python +# -*- coding: utf-8 -*- + +# TODO: Display rows are of different types. +# Row two appears to be monochrome green, only brightness varies + +# ****************************************************************************** +# ZYNTHIAN PROJECT: Zynthian Control Device Driver +# +# Zynthian Control Device Driver for "Ableton Push 1" +# +# Copyright (C) 2025 Brumby +# +# ****************************************************************************** +# +# 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. +# +# ****************************************************************************** + +""" +Ableton Push 1 Driver for Zynthian + +This driver provides basic functionality for the Ableton Push 1 controller: +- Rudimentary mixer mode with volume knobs for chains 1-7 and main volume on knob 8 + (activated by pressing the "Volume" button) +- Pad array mode for the sequencer (activated by pressing the "User" button) +- Illuminated pad array with selectable scales and modes (activated by pressing the "Scales" button) + +NOTE: This driver is incomplete but in a usable state. + +In Mixer mode, knobs 1-4 control chain volumes 1-4 instead of acting as Zynpots. +In other modes, they function as regular Zynpots for the Zynthian GUI. + +Illuminated buttons indicate active functions. +Current driver modes are indicated by blinking mode buttons. + +Display usage: +- Top two rows are exclusively for mixer display +- Bottom two rows are for scales and sequencer modes +""" + +import logging +import traceback + +#### Local debug configuration +debug_mode = True +if debug_mode: + # Set up logger for this driver to isolate debug messages + logger = logging.getLogger("ABL-Push_1") + logger.setLevel(logging.DEBUG) + + # Create console handler with formatted output + handler = logging.StreamHandler() + formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) +### End of local debug configuration + +# Brumby's custom imports +from time import sleep # Pause between sysex events (Push 1 is slow) +import zyngine.ctrldev.ableton.push1_consts as ABL # Button definitions +from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony # Custom class for scales mode +from zyngine.ctrldev.zynthian_ctrldev_base_extended import RunTimer, KnobSpeedControl, ButtonTimer, CONST + +# Zynthian core modules +from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer +from zyncoder.zyncore import lib_zyncore +from zynlibs.zynseq import zynseq + +# ------------------------------------------------------------------------------------------------------------------ +# Ableton Push 1 Driver Class +# ------------------------------------------------------------------------------------------------------------------ + +# zynthian_ctrldev_zynpad - base class for sequencer pad control +# zynthian_ctrldev_zynmixer - base class for main mixer control + +# Push 1 MIDI mapping note values +# DO NOT DELETE - essential reference +ABL_PAD_START = 36 # First pad = pad_36 +ABL_PAD_END = 99 # Last pad = pad_99 + +class zynthian_ctrldev_ableton_push_1_v2(zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer): + """Driver class for Ableton Push 1 controller""" + + logging.info("Push 1 initializing class instance") + # Web log will show this message during initialization + + # Device IDs for recognition (obtained from zynthian_ctrldev_manager.load_driver()) + # dev_ids = ["Ableton Push IN 2", "Ableton Push IN 1"] + dev_ids = ["Ableton Push IN 2"] # Data only appears on Port 2 + + driver_name = "Ableton Push v1" # Optional - class name used if not specified + driver_description = "Interface Ableton Push v1 with zynpad and zynmixer functionality" + + ################################ + + # Colors for LED pads in Sequencer mode + # TODO: Palette needs adjustment + # Reference: https://pushmod.blogspot.com/p/pad-color-table.html + # ORIGINAL PAD_COLOURS = [71, 104, 76, 51, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] + PAD_COLOURS = [61, 36, 63, 54, 104, 41, 64, 12, 11, 71, 4, 67, 42, 9, 105, 15] # Current (non-functional) + STARTING_COLOUR = 123 # GREEN + STOPPING_COLOUR = 120 # RED + RUNNING_COLOR = 3 # WHITE + + # Event type constants (duplicates exist in base_extended) + # evtype = (ev[0] >> 4) & 0x0F + EV_NOTE_OFF = 0x8 # 3 bytes + EV_NOTE_ON = 0x9 # 3 bytes + EV_AFTERTOUCH = 0xA # 3 bytes (polyphonic per note) + EV_CC = 0xB # 3 bytes + EV_PC = 0xC # 2 bytes + EV_CHAN_PRESS = 0xD # 2 bytes + EV_PITCHBEND = 0xE # 3 bytes: ev[1] = LSB 0-127; ev[2] = MSB 0-127 + EV_SYSTEM = 0xF # Variable length (1+ bytes), system type = ev[0] & 0x0F + + # Device operation modes + DEV_MODE_NONE = None + DEV_MODE_PAD = 1 # Sequencer pad mode + DEV_MODE_SCALES = 2 # Keyboard/scales mode + DEV_MODE_MIXER = 3 # Mixer mode + + device_mode_active = DEV_MODE_NONE # Initial mode (no mode selected) + + # Initialize scales harmony system + scales = Harmony(8, 8) + scales.init_scale( + tonic=0, + mode_name="Major", + col_versatz=-5, + middle_c=48, + middle_pad_nr=4 + ) + + def __init__(self, state_manager, idev_in, idev_out=None): + """Initialize the Push 1 driver + + Args: + state_manager: Zynthian state manager instance + idev_in: Input device ID + idev_out: Output device ID (optional) + """ + logging.info("Ableton Push 1 detected on USB") + # TODO: Add confirmation message when correct USB device is found + + # Call parent constructor (saves state_manager, chainmanager, idev_in, idev_out) + super().__init__(state_manager, idev_in, idev_out) + + # Initialize knob easing/speed control + # TODO: Experiment with values during live operation + self._knobs_ease = KnobSpeedControl() + + # Initialize device feedback controllers + self._leds_mono = Feedback_Mono_LEDs(idev_out) # Mono LED buttons (right/left of pads) + self._leds_bi = Feedback_Bi_LEDs(idev_out) # Bi-color display buttons + self._leds_rgb = Feedback_RGB_LEDs(idev_out) # RGB pad LEDs + self._display = Feedback_Display(idev_out) # Text display + self.mixer_init() # Initialize mixer display + + # Required when sending translated MIDI events + self.unroute_from_chains = True + + # Device state variables + self.shift = False # SHIFT button pressed state + self.shift_note = 0 # Octave shift amount + + def init(self): + """Initialize the device - called from parent class""" + try: + logging.info("Initializing Ableton Push 1 - BRUMBY") + + # Set initial device mode + self.set_device_mode_new(self.DEV_MODE_MIXER) + + # Setup LED states for control buttons + # Illuminate monochrome buttons that should be lit + bright_buttons = [ + 36, 37, 38, 39, 40, 41, 42, 43, + ABL.BTN_START[1], ABL.BTN_OK[1], ABL.BTN_ESC[1], ABL.BTN_LEFT[1], + ABL.BTN_RIGHT[1], ABL.BTN_UP[1], ABL.BTN_DOWN[1], ABL.BTN_SCALES[1], + ABL.BTN_USER[1] + ] + + for btn in bright_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, btn, ABL.MONO_LED_LIT) + + # Set dim state for specific buttons + dim_buttons = [ABL.BTN_REC[1], ABL.BTN_SHIFT[1]] + for btn in dim_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, btn, ABL.MONO_LED_DIM) + + # Set bi-color LEDs to dim orange + bi_buttons = [ + ABL.BTN_R1_C1[1], ABL.BTN_R1_C2[1], ABL.BTN_R1_C3[1], ABL.BTN_R1_C4[1] + ] + for btn in bi_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, btn, ABL.BI_ORANGE_DIM) + + # Setup device pad array dimensions + self.cols = 8 + self.rows = 8 + super().init() # Activate parent initialization (required!) + + # Setup scales mode if active + if self.device_mode_active == self.DEV_MODE_SCALES: + self.scales_set_dev_to_scales_mode() + + except Exception as e: + logger.error("Exception during initialization: %s", e) + logger.error("Traceback: %s", traceback.format_exc()) + + def end(self): + """Clean up device state - called from parent class""" + logging.info("Shutting down Ableton Push 1 - BRUMBY") + super().end() + +################################################################################################################# +################## START of SCALES FUNCTIONS ########################################################## + + def scales_set_dev_to_scales_mode(self): + """Configure device for scales/keyboard mode""" + self.device_mode_active = self.DEV_MODE_SCALES + + # Visual feedback - blink Scales button + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) + + # Clear pads and set scale colors + self.pads_off() + self.scales_set_pad_colors() + + # Display scale information + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + scale_n_mode += " - knob 7+8" + self._display.write_xy_mem(scale_n_mode, 0, 2) + + # Setup display text for scales mode + btn_txt_row2 = "|modes here | | || G# | A | A# | B |" + btn_txt_row3 = "| C | C# | D | D# || E | F | F# | G |" + + self._display.write_xy_mem(btn_txt_row2, 0, 2) + self._display.write_xy_mem(scale_n_mode, 0, 2) # Overwrite with scale info + self._display.write_xy_mem(btn_txt_row3, 0, 3) + self._display.update_screen() + + # Setup pad LEDs and octave buttons + self.scale_update_leds(self.scales.tonic) + for btn in [ABL.BTN_OCTAVE_DOWN[1], ABL.BTN_OCTAVE_UP[1]]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, btn, ABL.MONO_LED_LIT) + + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) + + def scales_cleanup(self): + """Clean up scales mode - reset LEDs and display""" + # Clear display + clear_text = "| | | | || | | | |" + self._display.write_xy_mem(clear_text, 0, 2) + self._display.write_xy_mem(clear_text, 0, 3) + self._display.update_screen() + + # Turn off scale selection LEDs + scale_buttons = [ + ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], + ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], + ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] + ] + for btn in scale_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, btn, ABL.BI_LED_OFF) + + # Reset Scales button and octave buttons + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) + for btn in [ABL.BTN_OCTAVE_DOWN[1], ABL.BTN_OCTAVE_UP[1]]: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, btn, ABL.MONO_LED_OFF) + + def scale_update_leds(self, index_activated): + """Update scale selection LED indicators + + Args: + index_activated: Index of the currently active scale (0-11) + """ + scale_buttons = [ + ABL.BTN_R2_C1[1], ABL.BTN_R2_C2[1], ABL.BTN_R2_C3[1], ABL.BTN_R2_C4[1], + ABL.BTN_R2_C5[1], ABL.BTN_R2_C6[1], ABL.BTN_R2_C7[1], ABL.BTN_R2_C8[1], + ABL.BTN_R1_C5[1], ABL.BTN_R1_C6[1], ABL.BTN_R1_C7[1], ABL.BTN_R1_C8[1] + ] + + # Set all scale buttons to dim green + for btn in scale_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, btn, ABL.BI_GREEN_DIM) + + # Set active scale button to blinking + lib_zyncore.dev_send_ccontrol_change( + self.idev_out, 0, scale_buttons[index_activated], ABL.BI_GREEN_DIM_BLINK + ) + + def scales_set_tonic(self, step): + """Change scale tonic (root note) + + Args: + step: Relative change amount (positive or negative) + """ + if step > 63: + step -= 128 # Convert encoder wrap-around to negative + + # Slow down knob sensitivity + self.steps_tonic = getattr(self, 'steps_tonic', 0) + step + if abs(self.steps_tonic) <= 10: + return # Apply change only every 10 steps + self.steps_tonic = 0 + + # Calculate new tonic with bounds checking + new_tonic = self.scales.tonic + step + if new_tonic < 0: + new_tonic = 11 # Wrap to B + if new_tonic > 11: + new_tonic = 0 # Wrap to C + + self.scales.tonic = new_tonic + + # Update display and LEDs + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + scale_n_mode += " - knob 7+8" + self._display.write_xy_mem(scale_n_mode, 0, 2) + self._display.update_screen() + self.scale_update_leds(new_tonic) + + def scales_set_mode(self, step): + """Change scale mode (major, minor, etc.) + + Args: + step: Relative change amount (positive or negative) + """ + if step > 63: + step -= 128 # Convert encoder wrap-around to negative + + # Slow down knob sensitivity + self.steps_mode = getattr(self, 'steps_mode', 0) + step + if abs(self.steps_mode) <= 10: + return # Apply change only every 10 steps + self.steps_mode = 0 + + # Get available mode names and current mode index + modenames = self.scales.harmony_get_mode_names() + nr_of_modes = len(modenames) + current_index = 0 + + # Set default mode if not already set + if not self.scales.active_mode: + self.scales.active_mode = modenames[0] # "Chromatic" + + # Find current mode index + for i in range(nr_of_modes): + if modenames[i] == self.scales.active_mode: + current_index = i + break + + # Calculate new mode index with wrapping + new_index = current_index + step + if new_index >= nr_of_modes: + new_index = 0 + elif new_index < 0: + new_index = nr_of_modes - 1 + + # Apply new mode + new_mode = modenames[new_index] + self.scales.active_mode = new_mode + self.scales.init_scale(self.scales.tonic, self.scales.active_mode) + + # Update visual feedback + self.scales_set_pad_colors() + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + scale_n_mode += " - knob 7+8" + self._display.write_xy_mem(scale_n_mode, 0, 2) + self._display.update_screen() + + def scales_set_pad_colors(self): + """Set LED colors for all pads based on current scale""" + for pad_nr in range(64): + new_note = self.scales.harmony_get_target_note(pad_nr) + + # Color tonic notes differently + if self.scales.is_tonic_by_padnr(pad_nr): + r, g, b = 0, 0, 255 # Blue for tonic + else: + r, g, b = 200, 200, 200 # White for other notes + + self._leds_rgb.set_rgb(pad_nr, r, g, b, overlay=False) + + def process_scale_event(self, ev) -> bool: + """Process MIDI events in scales mode + + Args: + ev: MIDI event bytes + + Returns: + bool: True if event was processed, False otherwise + """ + if self.device_mode_active != self.DEV_MODE_SCALES: + return False # Not in scales mode + + note = ev[1] + + # Filter for pad events (sound-producing notes) + if ABL_PAD_START <= note <= ABL_PAD_END: + evtype = (ev[0] >> 4) & 0x0F + + # Handle pitch bend from ribbon controller + if evtype == self.EV_PITCHBEND: + self._forward_like_niels_did(ev) + + # Handle note events + if evtype in [self.EV_NOTE_ON, self.EV_NOTE_OFF, self.EV_AFTERTOUCH]: + pad_nr = note - 35 # Convert to hardware pad number + + # Translate note according to current scale + note_translated = self.scales.harmony_get_target_note(pad_nr - 1) + note_translated += self.shift_note # Apply octave shift + + # Adjust velocity (Push 1 is velocity-insensitive) + vel = ev[2] + if evtype == self.EV_NOTE_ON: + vel = min(ev[2] * 2, 255) # Boost velocity but cap at max + + # Forward translated event + new_ev = bytes([ev[0], note_translated, vel]) + self._forward_like_niels_did(new_ev) + return True + + # Process control buttons and knobs + def helper_set_new_tonic(tonic): + """Helper function to change scale tonic""" + if self.scales.set_new_tonic(tonic): + scale_n_mode = self.scales.harmony_get_scale_name_with_mode() + scale_n_mode += " - knob 7+8" + self._display.write_xy_mem(scale_n_mode, 0, 2) + self._display.update_screen() + self.scale_update_leds(tonic) + return True + + # Check for button press events + search_key = (ev[0], ev[1]) + if ev[2] > 0: # Button down events only + # Scale selection buttons + scale_mapping = { + ABL.BTN_R2_C1: 0, ABL.BTN_R2_C2: 1, ABL.BTN_R2_C3: 2, ABL.BTN_R2_C4: 3, + ABL.BTN_R2_C5: 4, ABL.BTN_R2_C6: 5, ABL.BTN_R2_C7: 6, ABL.BTN_R2_C8: 7, + ABL.BTN_R1_C5: 8, ABL.BTN_R1_C6: 9, ABL.BTN_R1_C7: 10, ABL.BTN_R1_C8: 11 + } + + if search_key in scale_mapping: + return helper_set_new_tonic(scale_mapping[search_key]) + + # Scale and mode knobs + if search_key == ABL.KNOB_7: + self.scales_set_tonic(ev[2]) + return True + if search_key == ABL.KNOB_8: + self.scales_set_mode(ev[2]) + return True + + # Octave buttons + if search_key == ABL.BTN_OCTAVE_UP: + self.shift_note += 12 + return True + if search_key == ABL.BTN_OCTAVE_DOWN: + self.shift_note -= 12 + return True + + return False + +################## END of SCALES FUNCTIONS ########################################################## +############################################################################################################### + +####################################################################################### +### MIXER FUNCTIONS FOR DISPLAY ACTION from zynmixer ### + + def mixer_init(self): + """Initialize mixer display functionality + + Mixer is the main functionality, so it must be setup in init function + """ + # Create constants for mixer display layout + self.MIXER_DISP_ROW_VOLUME = 1 + self.MIXER_DISP_ROW_BALANCE = 0 + + self.KNOB_VOLUME = ABL.KNOB_8 + self._mixer_chains_bank = 0 + + self._display_mixer = Feedback_Display(self.idev_out) + self.mixer_set_dev_to_mixermode() + + return + + def mixer_set_dev_to_mixermode(self): + """Configure device for mixer mode""" + # Create mixer display content + btn_txt_row0 = " Ch 1 Ch 2 Ch 3 Ch 4 Ch 5 Ch 6 Ch 7 Main " + btn_text_row2 = self._display_mixer.format_help + + self._display_mixer.write_xy_mem(btn_txt_row0, 0, 0) + self._display_mixer.update_screen() + + def mixer_cleanup(self): + """Clean up mixer mode - currently no specific cleanup needed""" + pass + + def update_mixer_active_chain(self, active_chain): + """Update hardware indicators for active chain + + Args: + active_chain: Index of the currently active audio chain + """ + try: + mix_state = self.zynmixer.get_state() + for c in mix_state.keys(): + if c[:5] == "chan_": + chan_nr = int(c[5:7]) + ch_level = self.zynmixer.zctrls[chan_nr]['level'].get_value() + self._display_mixer.write_to_knobx_mem(ch_level, chan_nr, self.MIXER_DISP_ROW_VOLUME, as_bar=True) + self._display_mixer.update_screen() + except Exception as e: + logging.error(f"Error in update_mixer_active_chain: {e}") + logging.exception(traceback.format_exc()) + + def update_mixer_strip(self, chan, symbol, value): + """Update hardware indicators for a mixer strip: mute, solo, level, balance, etc. + + Args: + chan: Mixer channel index + symbol: Control name ('level', 'balance', 'mute', 'solo', 'mono', 'm+s', 'phase') + value: Control value + """ + try: + match symbol: + case 'level': + if chan > 7: + chan = 7 # Main channel + self._display_mixer.write_to_knobx_mem(value, chan, self.MIXER_DISP_ROW_VOLUME, as_bar=True) + self._display_mixer.update_screen() + return True + + case 'balance' | 'mute' | 'solo' | 'mono' | 'm+s' | 'phase': + # These controls are recognized but not yet implemented for display + pass + + case _: + logging.debug(f"Update mixer strip: UNKNOWN SYMBOL! chan: {chan}; symbol: {symbol} value: {value}") + return False + + logging.debug(f"Update mixer strip: NOT IMPLEMENTED! chan: {chan}; symbol: {symbol} value: {value}") + + except Exception as e: + logging.error(f"Error in update_mixer_strip: {e}") + logging.exception(traceback.format_exc()) + + def process_mixer_event(self, ev) -> bool: + """Process MIDI events in mixer mode + + Args: + ev: MIDI event bytes + + Returns: + bool: True if event was processed, False otherwise + """ + if len(ev) != 3: + return False # Need 3-byte events + + # Handle mixer knob events (volume control) + search_knob = (ev[0], ev[1]) + if search_knob in [ABL.KNOB_1, ABL.KNOB_2, ABL.KNOB_3, ABL.KNOB_4, + ABL.KNOB_5, ABL.KNOB_6, ABL.KNOB_7, ABL.KNOB_8]: + return self._update_control("level", ev, 0, 100) + + return False + + def _update_control(self, type: str, ev: bytes, minv, maxv): + """Update mixer control value based on MIDI event + + Args: + type: Control type ('level' or 'balance') + ev: MIDI event bytes + minv: Minimum value + maxv: Maximum value + + Returns: + bool: True if control was updated, False otherwise + """ + ccnum = ev[1] & 0x7F + ccval = ev[2] & 0x7F + + # Determine which mixer channel to control + if ev[:2] == bytes(self.KNOB_VOLUME): # Main volume knob + mixer_chan = 255 + else: + # Calculate chain index based on knob position and bank + index = (ccnum - ABL.KNOB_1[1]) + self._mixer_chains_bank * 8 + chain = self.chain_manager.get_chain_by_index(index) + if chain is None or chain.chain_id == 0: + return False + mixer_chan = chain.mixer_chan + + # Get current value and set function based on control type + if type == "level": + value = self.zynmixer.get_level(mixer_chan) + set_value = self.zynmixer.set_level + elif type == "balance": + value = self.zynmixer.get_balance(mixer_chan) + set_value = self.zynmixer.set_balance + else: + return False + + # Apply relative change from encoder + value *= 100 # Convert 0.0-1.0 range to 0-100 + value += ccval if ccval < 64 else ccval - 128 # Handle encoder rotation + value = max(minv, min(value, maxv)) # Clamp to valid range + set_value(mixer_chan, value / 100) # Convert back to 0.0-1.0 range + return True + +### END of Mixer functions ### +######################################################################### + +######################################################################### +### Start of Sequencer / Pad Functions ### + def process_sequencer_event(self, ev) -> bool: + """Process MIDI events in sequencer mode + + Args: + ev: MIDI event bytes + + Returns: + bool: True if event was processed, False otherwise + """ + if not self.device_mode_active == self.DEV_MODE_PAD: + return False # Not in sequencer mode + + cc = ev[1] # Controller number for bank calculation + + # Handle scene selection buttons + search_key = [ev[0], ev[1]] + if ev[2] > 0: # Button down events only + scene_buttons = [ + ABL.BTN_BEAT_1_QUATER, ABL.BTN_BEAT_2_QUATER_T, ABL.BTN_BEAT_3_EIGHTH, + ABL.BTN_BEAT_4_EIGHTH_T, ABL.BTN_BEAT_5_SIXTEENTH, ABL.BTN_BEAT_6_SIXTEENTH_T, + ABL.BTN_BEAT_7_THIRTYSECOND, ABL.BTN_BEAT_8_THIRTYSECOND_T + ] + + if search_key in scene_buttons: + return self.sequencer_set_scene(cc) + + # Handle pad events for sequencer control + evtype = (ev[0] >> 4) & 0x0F + note = ev[1] & 0x7F + + # Filter program change events (Push 1 doesn't send these) + if evtype == self.EV_PC: + return True # Ignore program change events + + # Handle note events for sequencer pads + if evtype == self.EV_NOTE_ON: + try: + pad_nr = note - ABL_PAD_START # Convert to pad number (0-63) + col = pad_nr // 8 + row = pad_nr % 8 + col = 7 - col # Flip column orientation + + # Calculate sequencer pad index and toggle play state + pad = row * self.zynseq.col_in_bank + col + logging.debug(f"BRUMBY: row={row}; col={col}; pad={pad}") + if pad < self.zynseq.seq_in_bank: + self.zynseq.libseq.togglePlayState(self.zynseq.bank, pad) + return True + except Exception: + pass # Silently handle pad calculation errors + + return False + + def update_seq_state(self, bank, seq, state, mode, group): + """Update sequencer state indicators + + Args: + bank: Sequencer bank number + seq: Sequence number + state: Sequence state (playing, stopped, etc.) + mode: Sequence mode + group: Sequence group + """ + try: + # Only update if in sequencer mode and bank matches + if not self.device_mode_active == self.DEV_MODE_PAD: + return + if self.idev_out is None or bank != self.zynseq.bank: + return + + # Calculate pad position and MIDI note + col, row = self.zynseq.get_xy_from_pad(seq) + note = ABL_PAD_END + 1 - (row + 1) * 8 + col + + # Determine LED color based on sequence state + try: + if mode == 0 or group > 16: + chan = 0 + vel = 0 + elif state == zynseq.SEQ_STOPPED: + chan = 0 + vel = self.PAD_COLOURS[group] + elif state == zynseq.SEQ_PLAYING: + chan = 2 + vel = self.RUNNING_COLOR + elif state in [zynseq.SEQ_STOPPING, zynseq.SEQ_STOPPINGSYNC]: + chan = 1 + vel = self.STOPPING_COLOUR + elif state == zynseq.SEQ_STARTING: + chan = 1 + vel = self.STARTING_COLOUR + else: + chan = 0 + vel = 0 + except Exception: + chan = 0 + vel = 0 + + # Update pad LED + lib_zyncore.dev_send_note_on(self.idev_out, chan, note, vel) + except ValueError as e: + print(f"Error updating sequencer state: {e}") + + def refresh(self): + """Refresh LED states - called from parent class""" + if self.device_mode_active == self.DEV_MODE_PAD: + return super().refresh() + + def pad_off(self, col, row): + """Turn off specific pad LED + + Args: + col: Column index (0-7) + row: Row index (0-7) + """ + note = ABL_PAD_END + 1 - (row + 1) * 8 + col + lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0) + + def sequencer_set_scene(self, ccnum): + """Set active sequencer scene/bank + + Args: + ccnum: Controller number identifying the scene + + Returns: + bool: True if scene was changed, False otherwise + """ + self.zynseq.select_bank(8 - (ccnum - 36)) + + # Update scene button LEDs + scene_buttons = [36, 37, 38, 39, 40, 41, 42, 43] + for btn in scene_buttons: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, btn, ABL.MONO_LED_DIM) + + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ccnum, ABL.MONO_LED_LIT_BLINK_FAST) + return True + + def pads_off(self): + """Turn off all pad LEDs""" + for row in range(self.rows): + for col in range(self.cols): + self.pad_off(col, row) + + def _forward_like_niels_did(self, ev): + """Forward MIDI events to active chain (based on Niels' implementation) + + Args: + ev: MIDI event bytes + + Returns: + bool: True if event was forwarded, False otherwise + """ + chain = self.chain_manager.get_active_chain() + if chain.midi_chan is None: + return False + + status = (ev[0] & 0xF0) | chain.midi_chan + self.zynseq.libseq.sendMidiCommand(status, ev[1], ev[2]) + return True + + def set_device_mode_new(self, new_mode): + """Change device operation mode + + Args: + new_mode: New mode to activate (DEV_MODE_MIXER, DEV_MODE_PAD, DEV_MODE_SCALES) + + Returns: + bool: True if mode was changed successfully, False otherwise + """ + try: + if new_mode == self.device_mode_active: + return True # Already in requested mode + + # Clean up current mode + match self.device_mode_active: + case self.DEV_MODE_MIXER: + lib_zyncore.dev_send_ccontrol_change( + self.idev_out, 0, ABL.BTN_VOLUME[1], ABL.MONO_LED_LIT) + self.mixer_cleanup() + + case self.DEV_MODE_PAD: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_USER[1], ABL.MONO_LED_LIT) + self.pads_off() + + case self.DEV_MODE_SCALES: + self.scales_cleanup() + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT) + self.pads_off() + + # Set new mode + self.device_mode_active = new_mode + + # Initialize new mode + match new_mode: + case self.DEV_MODE_MIXER: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_VOLUME[1], ABL.MONO_LED_LIT_BLINK) + return self.mixer_set_dev_to_mixermode() + + case self.DEV_MODE_PAD: + self.refresh() + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_USER[1], ABL.MONO_LED_LIT_BLINK) + + case self.DEV_MODE_SCALES: + self.scales_set_dev_to_scales_mode() + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, ABL.BTN_SCALES[1], ABL.MONO_LED_LIT_BLINK) + + case _: + logging.error("DEVICE Mode not defined. Programming Error") + return False + + return True + + except Exception as e: + logger.error(f"Error in set_device_mode_new: {e}") + logger.exception(traceback.format_exc()) + return False + + def midi_event(self, ev): + """Main MIDI event handler - called for all incoming MIDI events + + Args: + ev: MIDI event bytes + + Returns: + bool: True if event was processed, False otherwise + """ + # Debug logging for button events + if len(ev) > 1 and debug_mode: + search_key = (ev[0], ev[1]) + btn_name = self.button_name_from_midi_event(search_key) + if btn_name != "": + logger.debug(f"\n - Button: {btn_name} on chan. {ev[0] & 0x0F} gives midi_event: {hex(ev[0])} {hex(ev[1])} {hex(ev[2])} = {int(ev[1])}, {int(ev[2])}") + + # Handle shift button (momentary modifier) + if len(ev) > 2: + val_or_vel = ev[2] + is_key_push = val_or_vel > 0 + + search_key = (ev[0], ev[1]) + if search_key == ABL.BTN_SHIFT: + self.shift = is_key_push + # Visual feedback for shift state + if self.shift: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_LIT_BLINK) + else: + lib_zyncore.dev_send_ccontrol_change(self.idev_out, 0, 49, ABL.MONO_LED_DIM) + return True + + # Handle mode change buttons + if len(ev) > 2 and ev[2] > 0: # Button down events + search_key = (ev[0], ev[1]) + if search_key == ABL.BTN_VOLUME: + return self.set_device_mode_new(self.DEV_MODE_MIXER) + elif search_key == ABL.BTN_SCALES: + return self.set_device_mode_new(self.DEV_MODE_SCALES) + elif search_key == ABL.BTN_USER: + return self.set_device_mode_new(self.DEV_MODE_PAD) + + # Route events to current mode handler + match self.device_mode_active: + case self.DEV_MODE_MIXER: + if self.process_mixer_event(ev): + return True + case self.DEV_MODE_PAD: + if self.process_sequencer_event(ev): + return True + case self.DEV_MODE_SCALES: + if self.process_scale_event(ev): + return True + + # Handle GUI control events + if self.process_gui_events(ev): + return True + + return False # Event not processed + + def process_gui_events(self, ev) -> bool: + """Process GUI control events (knobs and buttons) + + Args: + ev: MIDI event bytes + + Returns: + bool: True if event was processed, False otherwise + """ + if len(ev) < 3: + return False # Need 3-byte events + + search_key = (ev[0], ev[1]) + data_val = ev[2] & 0x7F + + # Handle knob events with easing + knob_mapping = { + ABL.KNOB_1: (0, False), + ABL.KNOB_2: (1, False), + ABL.KNOB_3: (2, False), + ABL.KNOB_4: (3, True) + } + + if search_key in knob_mapping: + knob_id, is_shifted = knob_mapping[search_key] + delta = self._knobs_ease.feed(bytes(search_key), data_val, is_shifted) + self.state_manager.send_cuia("ZYNPOT", [knob_id, delta]) + return True + + # Handle button press events + if data_val > 0: # Button down events only + button_mapping = { + ABL.BTN_OK: ("V5_ZYNPOT_SWITCH", [3, "S"]), + ABL.BTN_R1_C1: ("V5_ZYNPOT_SWITCH", [0, "S"]), + ABL.BTN_R1_C2: ("V5_ZYNPOT_SWITCH", [1, "S"]), + ABL.BTN_R1_C3: ("V5_ZYNPOT_SWITCH", [2, "S"]), + ABL.BTN_ESC: ("BACK", None), + ABL.BTN_RIGHT: ("ARROW_RIGHT", None), + ABL.BTN_LEFT: ("ARROW_LEFT", None), + ABL.BTN_UP: ("ARROW_UP", None), + ABL.BTN_DOWN: ("ARROW_DOWN", None), + ABL.BTN_START: ("TOGGLE_PLAY", "TOGGLE_MIDI_PLAY"), + ABL.BTN_REC: ("TOGGLE_RECORD", "TOGGLE_MIDI_RECORD") + } + + if search_key in button_mapping: + action, shift_action = button_mapping[search_key] + if self.shift and shift_action: + action = shift_action + + if isinstance(action, tuple): + self.state_manager.send_cuia(action[0], action[1]) + else: + self.state_manager.send_cuia(action) + return True + + return False + + def button_name_from_midi_event(self, ev): + """Get button name from MIDI event for debugging + + Args: + ev: MIDI event bytes or search key + + Returns: + str: Button name or empty string if not found + """ + if len(ev) < 2: + return None + + if len(ev) > 2: + data = ev[2] + search_key = (ev[0] & 0xF0, ev[1]) + + # Search for button definition in ABL module + for name in dir(ABL): + if not name.startswith('__') and name.isupper(): + attr = getattr(ABL, name) + if attr == search_key: + return name + + return "" # Button not found in config + +# ------------------------------------------------------------------------------ +# Special Classes for Ableton Push 1 Display and Feedback +##################################################################################### + +class Feedback_Display: + """Class for controlling Ableton Push 1 text display""" + + # Display character constants (Akai-specific character set) + DISP_ARROW_UP = 0 + DISP_ARROW_DOWN = 1 + DISP_ARROW_RIGHT = 30 + DISP_ARROW_LEFT = 31 + DISP_HORIZONTAL_LINES_THREE_STACKED = 2 + DISP_HORIZONTAL_LINE_LOW = 95 + DISP_HOIZONTAL_LINE_SPLIT = 6 + DISP_VERTICAL_LINE_AND_HORIZONTAL_LINE = 3 + DISP_HORIZONTAL_LINE_AND_VERTICAL_LINE = 4 + DISP_VERTICAL_LINES_TWO = 5 + DISP_VERTICAL_LINE_MID = 174 + DISP_SPLIT_VERTICAL_LINES = 8 + DISP_FOLDER_SYMBOL = 7 + DISP_FLAT_SYMBOLS = 27 + DISP_THREE_SIDE_BY_SIDE_DOTS = 28 + DISP_FULL_BLOCK = 29 + DISP_LITTLE_BOX_SHIFTED_HIGH_MIDDLE = 9 + DISP_AE_UC = 10 + DISP_CEDILLE_UC = 11 + DISP_OE_UC = 12 + DISP_UE_UC = 13 + DISP_SZ = 14 + DISP_A_GRAVE = 15 + DISP_AE_LC = 16 + DISP_CEDILE = 17 + DISP_E_LC_GRAVE = 18 + DISP_E_LC_EGUT = 19 + DISP_E_LC_CIRCUM = 20 + DISP_I_LC_TREMA = 21 + DISP_N_LC_WITH_TILDE = 22 + DISP_OE_LC = 23 + DISP_DIV_STROKE = 24 + DISP_CIRC_WITH_DIV_STROKE = 25 + DISP_UE_LC = 26 + + # Mapping from Akai character codes to Unicode + akai_to_unicode = { + 0: "↑", 1: "↓", 30: "→", 31: "←", 2: "≡", 6: "╌", 95: "_", + 3: "┤", 4: "├", 5: "║", 8: "⫼", 174: "|", 7: "📁", 27: "♭", + 28: "⋮", 29: "█", 9: "▫", 10: "Ä", 11: "Ç", 12: "Ö", 13: "Ü", + 14: "ß", 15: "à", 16: "ä", 17: "ç", 18: "è", 19: "é", 20: "ê", + 21: "ï", 22: "ñ", 23: "ö", 24: "⁄", 25: "Ø", 26: "ü" + } + + format_help = b'123456789A123456789B123456789C123456789D123456789E123456789F123456789' + + def __init__(self, idev_out): + """Initialize display controller + + Args: + idev_out: Output device ID + """ + self.idev_out = idev_out + self.display_mem = [[32] * 68 for _ in range(4)] # 4 rows, 68 columns + self._disp_line_dirty = [False, False, False, False] + + def clear(self): + """Clear entire display (fill with spaces)""" + self.display_mem = [[32] * 68 for _ in range(4)] + + # Send SYSEX commands to clear each display line + sysex_commands = [ + bytes([240, 71, 127, 21, 28, 0, 0, 247]), # Line 0 + bytes([240, 71, 127, 21, 29, 0, 0, 247]), # Line 1 + bytes([240, 71, 127, 21, 30, 0, 0, 247]), # Line 2 + bytes([240, 71, 127, 21, 31, 0, 0, 247]) # Line 3 + ] + + for cmd in sysex_commands: + lib_zyncore.dev_send_midi_event(self.idev_out, cmd, len(cmd)) + sleep(0.01) + + self._disp_line_dirty = [False, False, False, False] + + def update_screen(self): + """Update display with changed content""" + for row in range(4): + if self._disp_line_dirty[row]: + text = bytes(self.display_mem[row]) + text_len = len(text) + col = 0 + + # Construct SYSEX message for line update + msg = bytes([240, 71, 127, 21, row + 24, 0, text_len + 1, col]) + text + bytes([247]) + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + sleep(0.01) + self._disp_line_dirty[row] = False + + def write_xy_mem(self, text, col_in, row_in): + """Write text to display memory at specified position + + Args: + text: Text to display (str, bytes, or number) + col_in: Column position (0-67) + row_in: Row position (0-3) + """ + # Convert input to bytes + if isinstance(text, str): + text = text.encode() + elif isinstance(text, bytes): + pass # Already bytes + elif isinstance(text, (int, float)): + text = str(text).encode() + else: + text = "Type error".encode() + + # Validate coordinates + row_in = max(0, min(3, row_in)) + col_in = max(0, min(63, col_in)) + + # Truncate text if it exceeds display bounds + text_len = len(text) + if text_len + col_in > 68: + text = text[:68 - col_in] + text_len = len(text) + + # Update display memory and mark as dirty + self.display_mem[row_in][col_in:col_in + text_len] = list(text) + self._disp_line_dirty[row_in] = True + + def write_to_knobx_mem(self, text, knob_x, row_y, as_bar=False): + """Write text below specified knob position + + Args: + text: Text to display + knob_x: Knob index (0-7) + row_y: Row position + as_bar: Whether to display as volume bar + """ + if knob_x > 7: + knob_x = 7 + logging.error("Knob index >7 not implemented. Using main channel.") + + # Convert numbers to text and handle bar display + if isinstance(text, (int, float)): + if as_bar: + fieldlen = 8 + if float(text) == 0.0: + text = "".ljust(fieldlen) + else: + int_val = int(text * fieldlen) + if int_val == 0: + text = ".".ljust(fieldlen) + else: + text = "".ljust(int_val, chr(self.DISP_VERTICAL_LINES_TWO)).ljust(fieldlen) + else: + text = str(text).ljust(8) + + # Calculate display position and write text + fields_start_knobs = [0, 9, 17, 26, 34, 43, 51, 60] + knobx_start = fields_start_knobs[knob_x] + text = text.ljust(8)[:8] + self.write_xy_mem(text, knobx_start, row_y) + + def contrast(self, i=None): + """Set or get display contrast + + Args: + i: Contrast value (0-63) or None to read current value + + Returns: + int: Current contrast value or None if reading not supported + """ + if i is not None: + i = max(0, min(63, i)) # Clamp to valid range + msg = bytes([240, 71, 127, 21, 122, 0, 1, i, 247]) + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + return i + + # Reading contrast not currently implemented + return None + + def brightnes(self, i=None): + """Set or get display brightness + + Args: + i: Brightness value (0-63) or None to read current value + + Returns: + int: Current brightness value or None if reading not supported + """ + if i is not None: + i = max(0, min(63, i)) # Clamp to valid range + msg = bytes([240, 71, 127, 21, 124, 0, 1, i, 247]) + lib_zyncore.dev_send_midi_event(self.idev_out, msg, len(msg)) + return i + + # Reading brightness not currently implemented + return None + + def first_screen(self): + """Display welcome/splash screen""" + self.clear() + sleep(0.1) + self.write_xy_mem(b'** Zynthian Push1Driver 0.1 **', 17, 2) + self.write_xy_mem(b'++ Make MusicNot War ++', 20, 3) + self.update_screen() + +# -------------------------------------------------------------------------- +# Feedback LED controller classes +# -------------------------------------------------------------------------- + +class Feedback_Mono_LEDs: + """Controller for monochrome LEDs""" + + _all_mono = [3, 9, 28, 29, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 61, 62, 63, + 85, 86, 87, 88, 89, 90, 110, 111, 11, 113, 114, 115, 116, 117, 118, 119] + + def __init__(self, idev): + self._idev = idev + self._state = {} + self._timer = RunTimer() + + def all_off(self, overlay=False): + """Turn off all monochrome LEDs""" + for note in self._all_mono: + lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, 0) + if not overlay: + self._state[note] = 0 + + def set_mono(self, note, grey_val, overlay=False): + """Set monochrome LED state + + Args: + note: LED note number + grey_val: Brightness value + overlay: Whether to update internal state + """ + lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, grey_val) + if not overlay: + self._state[note] = grey_val + + def refresh_one(self, note): + """Refresh single LED state from memory""" + if note in self._state: + lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, self._state[note]) + + def refresh(self): + """Refresh all LED states from memory""" + for note in self._all_mono: + if note in self._state: + lib_zyncore.dev_send_ccontrol_change(self._idev, 0, note, self._state[note]) + +class Feedback_Bi_LEDs: + """Controller for bi-color LEDs (not fully implemented)""" + def __init__(self, idev): + self._idev = idev + self._state = {} + self._timer = RunTimer() + + def all_off(self): + """Turn off all bi-color LEDs""" + pass # Implementation needed + +class Feedback_RGB_LEDs: + """Controller for RGB pad LEDs""" + + def __init__(self, idev): + self._idev = idev + self._state = {} + self._timer = RunTimer() + + def all_off(self, overlay=False): + """Turn off all RGB LEDs""" + for pad_nr in range(ABL_PAD_END + 1 - ABL_PAD_START): + self.set_rgb(pad_nr, 0, 0, 0, overlay) + + def refresh(self): + """Refresh all RGB LED states from memory""" + for pad_nr in range(ABL_PAD_END + 1 - ABL_PAD_START): + self.refresh_one(pad_nr) + + def refresh_one(self, pad_nr): + """Refresh single RGB LED state from memory""" + if pad_nr in self._state: + r, g, b = self._state[pad_nr] + self.set_rgb(pad_nr, r, g, b) + + def off_col_row(self, col, row): + """Turn off LED at specific column/row position + + Args: + col: Column index (0-7) + row: Row index (0-7) + """ + note = ABL_PAD_END + 1 - (row + 1) * 8 + col + lib_zyncore.dev_send_note_on(self._idev, 0, note, 0) + + def set_rgb(self, pad_nr, r, g, b, overlay=False): + """Set RGB LED color + + Args: + pad_nr: Pad number (0-63) + r: Red component (0-255) + g: Green component (0-255) + b: Blue component (0-255) + overlay: Whether to update internal state + """ + # Clamp color values + r = max(0, min(255, r)) + g = max(0, min(255, g)) + b = max(0, min(255, b)) + + if not 0 <= pad_nr <= 63: + logging.error(f"Invalid pad number: {pad_nr}") + return False + + # Save state if not overlay + if not overlay: + self._state[pad_nr] = [r, g, b] + + # Convert RGB to Push format (4-bit components) + r1, r2 = r // 16, r % 16 + g1, g2 = g // 16, g % 16 + b1, b2 = b // 16, b % 16 + + # Send SYSEX message + sysex = bytes([240, 71, 127, 21, 4, 0, 8, pad_nr, 0, r1, r2, g1, g2, b1, b2, 247]) + lib_zyncore.dev_send_midi_event(self._idev, sysex, len(sysex)) \ No newline at end of file From be012e85ddcaf45e9be4b355df688a06fb1d7fc5 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 01:30:16 +0200 Subject: [PATCH 47/57] ableton: changed all consts to tuples --- zyngine/ctrldev/ableton/push1_consts.py | 373 ++++++++++++------------ 1 file changed, 182 insertions(+), 191 deletions(-) diff --git a/zyngine/ctrldev/ableton/push1_consts.py b/zyngine/ctrldev/ableton/push1_consts.py index 6e8a34e32..e992800d8 100644 --- a/zyngine/ctrldev/ableton/push1_consts.py +++ b/zyngine/ctrldev/ableton/push1_consts.py @@ -1,218 +1,211 @@ -# Ableteton Push 1 +# Ableton Push 1 ### Definition of all buttons, pads and knobs -#knobs and Buttons are CC-Events -#knobs also have touch function with midi note event +# knobs and Buttons are CC-Events +# knobs also have touch function with midi note event # -#ribbon is type modwheel !!! Just one byte ev[0] -#ribbon has also touch function with midi note even +# ribbon is type modwheel !!! Just one byte ev[0] +# ribbon has also touch function with midi note even # # pad has note event - ##### -# Buttons are defined with ther action Message. Noteon, Control Change - +# Buttons are defined with their action Message. Noteon, Control Change ### SYSEX -#SYSEX_PREAMBLE = [] -#SYSEX_END = [] +# SYSEX_PREAMBLE = [] +# SYSEX_END = [] # Display # Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 # Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 -#SYSEX_INST_WRITE_LINE_0 = bytes([24]) -#SYSEX_INST_WRITE_LINE_1 = bytes([25]) -#SYSEX_INST_WRITE_LINE_2 = bytes([26]) -#SYSEX_INST_WRITE_LINE_3 = bytes([27]) - +# SYSEX_INST_WRITE_LINE_0 = bytes([24]) +# SYSEX_INST_WRITE_LINE_1 = bytes([25]) +# SYSEX_INST_WRITE_LINE_2 = bytes([26]) +# SYSEX_INST_WRITE_LINE_3 = bytes([27]) # Knobs 1-9 -KNOB_1 = [0xB0, 71] # CC71 -KNOB_2 = [0xB0, 72] # CC72 -KNOB_3 = [0xB0, 73] -KNOB_4 = [0xB0, 74] -KNOB_5 = [0xB0, 75] # CC75 -KNOB_6 = [0xB0, 76] # CC76 -KNOB_7 = [0xB0, 77] # CC77 -KNOB_8 = [0xB0, 78] # CC78 -KNOB_9 = [0xB0, 79] # CC79 -KNOB_10 = [0xB0, 14] # CC79 -KNOB_11 = [0xB0, 15] # CC79 - +KNOB_1 = (0xB0, 71) # CC71 +KNOB_2 = (0xB0, 72) # CC72 +KNOB_3 = (0xB0, 73) +KNOB_4 = (0xB0, 74) +KNOB_5 = (0xB0, 75) # CC75 +KNOB_6 = (0xB0, 76) # CC76 +KNOB_7 = (0xB0, 77) # CC77 +KNOB_8 = (0xB0, 78) # CC78 +KNOB_9 = (0xB0, 79) # CC79 +KNOB_10 = (0xB0, 14) # CC79 +KNOB_11 = (0xB0, 15) # CC79 # Touch -KNOB_1_T = [0x90,0] # "C-1" sic! -KNOB_2_T = [0x90,1] # "C#-1" -KNOB_3_T = [0x90,2] # "D-1" -KNOB_4_T = [0x90,3] # "D#-1" -KNOB_5_T = [0x90,4] # Note 4 -KNOB_6_T = [0x90,5] # Note 5 -KNOB_7_T = [0x90,6] # note 6 -KNOB_8_T = [0x90,7] # note 7 -KNOB_9_T = [0x90,8] # Note 8 -KNOB_10_T = [0x90,9] -KNOB_11_T = [0x90,10] - -RIBBON_TOUCH_T = [0x90,12] # "C0" note 12 -RIBBON_PITCH = [0xE0] # Mod-wheel ?? ### Achtung einziger Identifier, der nur 1 byte hat!!! ### +KNOB_1_T = (0x90, 0) # "C-1" sic! +KNOB_2_T = (0x90, 1) # "C#-1" +KNOB_3_T = (0x90, 2) # "D-1" +KNOB_4_T = (0x90, 3) # "D#-1" +KNOB_5_T = (0x90, 4) # Note 4 +KNOB_6_T = (0x90, 5) # Note 5 +KNOB_7_T = (0x90, 6) # note 6 +KNOB_8_T = (0x90, 7) # note 7 +KNOB_9_T = (0x90, 8) # Note 8 +KNOB_10_T = (0x90, 9) +KNOB_11_T = (0x90, 10) + +RIBBON_TOUCH_T = (0x90, 12) # "C0" note 12 +RIBBON_PITCH = (0xE0,) # Mod-wheel - Tupel mit einem Element! # Monochromatic Buttons # Alle Button sind CC / Alle PAD sind Noteon -BTN_TAP_TEMPO = [0xB0, 3] -BTN_METRONOME = [0xB0, 9] +BTN_TAP_TEMPO = (0xB0, 3) +BTN_METRONOME = (0xB0, 9) +BTN_FIXED_LENGTH = (0xB0, 90) +BTN_AUTOMATION = (0xB0, 89) +BTN_DUPLICATE = (0xB0, 88) -BTN_FIXED_LENGTH = [0xB0, 90] -BTN_AUTOMATION = [0xB0, 89] -BTN_DUPLICATE = [0xB0, 88] - -BTN_NEW = [0xB0, 87] -BTN_REC = [0xB0, 86] -BTN_START = [0xB0, 85] +BTN_NEW = (0xB0, 87) +BTN_REC = (0xB0, 86) +BTN_START = (0xB0, 85) ######### RECHTS ############ -BTN_PAN = [0xB0, 115] # CC115 -BTN_VOLUME = [0xB0, 114] # CC114 - -BTN_CLIP = [0xB0, 113] -BTN_TRACK = [0xB0, 112] - -BTN_BROWSE = [0xB0, 111] -BTN_DEVICE = [0xB0, 110] - - -BTN_ESC = [0xB0, 63] -BTN_OK = [0xB0, 62] -BTN_SOLO = [0xB0, 61] -BTN_MUTE = [0xB0, 60] -BTN_USER = [0xB0, 59] -BTN_SCALES = [0xB0, 58] -BTN_ACCENT = [0xB0, 57] -BTN_REPEAT = [0xB0, 56] -BTN_OCTAVE_UP = [0xB0, 55] -BTN_OCTAVE_DOWN = [0xB0, 54] - -BTN_ADD_TRACK = [0xB0, 53] -BTN_ADD_EFFECT = [0xB0, 52] -BTN_SESSION = [0xB0, 51] -BTN_NOTE = [0xB0, 50] -BTN_SHIFT = [0xB0, 49] -BTN_SELECT = [0xB0, 48] - -BTN_UP = [0xB0, 46] -BTN_DOWN = [0xB0, 47] -BTN_LEFT = [0xB0, 44] -BTN_RIGHT = [0xB0, 45] +BTN_PAN = (0xB0, 115) # CC115 +BTN_VOLUME = (0xB0, 114) # CC114 + +BTN_CLIP = (0xB0, 113) +BTN_TRACK = (0xB0, 112) + +BTN_BROWSE = (0xB0, 111) +BTN_DEVICE = (0xB0, 110) + +BTN_ESC = (0xB0, 63) +BTN_OK = (0xB0, 62) +BTN_SOLO = (0xB0, 61) +BTN_MUTE = (0xB0, 60) +BTN_USER = (0xB0, 59) +BTN_SCALES = (0xB0, 58) +BTN_ACCENT = (0xB0, 57) +BTN_REPEAT = (0xB0, 56) +BTN_OCTAVE_UP = (0xB0, 55) +BTN_OCTAVE_DOWN = (0xB0, 54) + +BTN_ADD_TRACK = (0xB0, 53) +BTN_ADD_EFFECT = (0xB0, 52) +BTN_SESSION = (0xB0, 51) +BTN_NOTE = (0xB0, 50) +BTN_SHIFT = (0xB0, 49) +BTN_SELECT = (0xB0, 48) + +BTN_UP = (0xB0, 46) +BTN_DOWN = (0xB0, 47) +BTN_LEFT = (0xB0, 44) +BTN_RIGHT = (0xB0, 45) # bottom up -BTN_BEAT_1_QUATER = [0xB0, 36] -BTN_BEAT_2_QUATER_T = [0xB0, 37] -BTN_BEAT_3_EIGHTH = [0xB0, 38] -BTN_BEAT_4_EIGHTH_T = [0xB0, 39] -BTN_BEAT_5_SIXTEENTH = [0xB0, 40] -BTN_BEAT_6_SIXTEENTH_T = [0xB0, 41] -BTN_BEAT_7_THIRTYSECOND = [0xB0, 42] -BTN_BEAT_8_THIRTYSECOND_T = [0xB0, 43] - -BTN_MASTER = [0xB0, 28] -BTN_STOP = [0xB0, 29] +BTN_BEAT_1_QUATER = (0xB0, 36) +BTN_BEAT_2_QUATER_T = (0xB0, 37) +BTN_BEAT_3_EIGHTH = (0xB0, 38) +BTN_BEAT_4_EIGHTH_T = (0xB0, 39) +BTN_BEAT_5_SIXTEENTH = (0xB0, 40) +BTN_BEAT_6_SIXTEENTH_T = (0xB0, 41) +BTN_BEAT_7_THIRTYSECOND = (0xB0, 42) +BTN_BEAT_8_THIRTYSECOND_T = (0xB0, 43) + +BTN_MASTER = (0xB0, 28) +BTN_STOP = (0xB0, 29) # Bicolor Buttons in the middle, below the display # They have two colors, red and green. -BTN_R1_C1 = [0xB0, 20] -BTN_R1_C2 = [0xB0, 21] -BTN_R1_C3 = [0xB0, 22] -BTN_R1_C4 = [0xB0, 23] -BTN_R1_C5 = [0xB0, 24] -BTN_R1_C6 = [0xB0, 25] -BTN_R1_C7 = [0xB0, 26] -BTN_R1_C8 = [0xB0, 27] - -BTN_R2_C1 = [0xB0, 102] -BTN_R2_C2 = [0xB0, 103] -BTN_R2_C3 = [0xB0, 104] -BTN_R2_C4 = [0xB0, 105] -BTN_R2_C5 = [0xB0, 106] -BTN_R2_C6 = [0xB0, 107] -BTN_R2_C7 = [0xB0, 108] -BTN_R2_C8 = [0xB0, 109] +BTN_R1_C1 = (0xB0, 20) +BTN_R1_C2 = (0xB0, 21) +BTN_R1_C3 = (0xB0, 22) +BTN_R1_C4 = (0xB0, 23) +BTN_R1_C5 = (0xB0, 24) +BTN_R1_C6 = (0xB0, 25) +BTN_R1_C7 = (0xB0, 26) +BTN_R1_C8 = (0xB0, 27) + +BTN_R2_C1 = (0xB0, 102) +BTN_R2_C2 = (0xB0, 103) +BTN_R2_C3 = (0xB0, 104) +BTN_R2_C4 = (0xB0, 105) +BTN_R2_C5 = (0xB0, 106) +BTN_R2_C6 = (0xB0, 107) +BTN_R2_C7 = (0xB0, 108) +BTN_R2_C8 = (0xB0, 109) # Have RGB-LED -PAD_36 = [0x90, 36] # note -PAD_37 = [0x90, 37] # note -PAD_38 = [0x90, 38] # note -PAD_39 = [0x90, 39] # note -PAD_40 = [0x90, 40] # note -PAD_41 = [0x90, 41] # note -PAD_42 = [0x90, 42] # note -PAD_43 = [0x90, 43] # note - -PAD_44 = [0x90, 44] # note -PAD_45 = [0x90, 45] # note -PAD_46 = [0x90, 46] # note -PAD_47 = [0x90, 47] # note -PAD_48 = [0x90, 48] # note -PAD_49 = [0x90, 49] # note -PAD_50 = [0x90, 50] # note -PAD_51 = [0x90, 51] # note - -PAD_52 = [0x90, 52] # note -PAD_53 = [0x90, 53] # note -PAD_54 = [0x90, 54] # note -PAD_55 = [0x90, 55] # note -PAD_56 = [0x90, 56] # note -PAD_57 = [0x90, 57] # note -PAD_58 = [0x90, 58] # note -PAD_59 = [0x90, 59] # note - -PAD_60 = [0x90, 60] # note -PAD_61 = [0x90, 61] # note -PAD_62 = [0x90, 62] # note -PAD_63 = [0x90, 63] # note -PAD_64 = [0x90, 64] # note -PAD_65 = [0x90, 65] # note -PAD_66 = [0x90, 66] # note -PAD_67 = [0x90, 67] # note - -PAD_68 = [0x90, 68] # note -PAD_69 = [0x90, 69] # note -PAD_70 = [0x90, 70] # note -PAD_71 = [0x90, 71] # note -PAD_72 = [0x90, 72] # note -PAD_73 = [0x90, 73] # note -PAD_74 = [0x90, 74] # note -PAD_75 = [0x90, 75] # not - -PAD_76 = [0x90, 76] # note -PAD_77 = [0x90, 77] # note -PAD_78 = [0x90, 78] # note -PAD_79 = [0x90, 79] # note -PAD_80 = [0x90, 80] # note -PAD_81 = [0x90, 81] # note -PAD_82 = [0x90, 82] # note -PAD_83 = [0x90, 83] # note - -PAD_84 = [0x90, 84] # note -PAD_85 = [0x90, 85] # note -PAD_86 = [0x90, 86] # note -PAD_87 = [0x90, 87] # note -PAD_88 = [0x90, 88] # note -PAD_89 = [0x90, 89] # note -PAD_90 = [0x90, 90] # note -PAD_91 = [0x90, 91] # note - -PAD_92 = [0x90, 92] # note -PAD_93 = [0x90, 96] # note -PAD_94 = [0x90, 94] # note -PAD_95 = [0x90, 95] # note -PAD_96 = [0x90, 96] # note -PAD_97 = [0x90, 97] # note -PAD_98 = [0x90, 98] # note -PAD_99 = [0x90, 99] # note - - -## from pushmod.blosgpot.com +PAD_36 = (0x90, 36) # note +PAD_37 = (0x90, 37) # note +PAD_38 = (0x90, 38) # note +PAD_39 = (0x90, 39) # note +PAD_40 = (0x90, 40) # note +PAD_41 = (0x90, 41) # note +PAD_42 = (0x90, 42) # note +PAD_43 = (0x90, 43) # note + +PAD_44 = (0x90, 44) # note +PAD_45 = (0x90, 45) # note +PAD_46 = (0x90, 46) # note +PAD_47 = (0x90, 47) # note +PAD_48 = (0x90, 48) # note +PAD_49 = (0x90, 49) # note +PAD_50 = (0x90, 50) # note +PAD_51 = (0x90, 51) # note + +PAD_52 = (0x90, 52) # note +PAD_53 = (0x90, 53) # note +PAD_54 = (0x90, 54) # note +PAD_55 = (0x90, 55) # note +PAD_56 = (0x90, 56) # note +PAD_57 = (0x90, 57) # note +PAD_58 = (0x90, 58) # note +PAD_59 = (0x90, 59) # note + +PAD_60 = (0x90, 60) # note +PAD_61 = (0x90, 61) # note +PAD_62 = (0x90, 62) # note +PAD_63 = (0x90, 63) # note +PAD_64 = (0x90, 64) # note +PAD_65 = (0x90, 65) # note +PAD_66 = (0x90, 66) # note +PAD_67 = (0x90, 67) # note + +PAD_68 = (0x90, 68) # note +PAD_69 = (0x90, 69) # note +PAD_70 = (0x90, 70) # note +PAD_71 = (0x90, 71) # note +PAD_72 = (0x90, 72) # note +PAD_73 = (0x90, 73) # note +PAD_74 = (0x90, 74) # note +PAD_75 = (0x90, 75) # note + +PAD_76 = (0x90, 76) # note +PAD_77 = (0x90, 77) # note +PAD_78 = (0x90, 78) # note +PAD_79 = (0x90, 79) # note +PAD_80 = (0x90, 80) # note +PAD_81 = (0x90, 81) # note +PAD_82 = (0x90, 82) # note +PAD_83 = (0x90, 83) # note + +PAD_84 = (0x90, 84) # note +PAD_85 = (0x90, 85) # note +PAD_86 = (0x90, 86) # note +PAD_87 = (0x90, 87) # note +PAD_88 = (0x90, 88) # note +PAD_89 = (0x90, 89) # note +PAD_90 = (0x90, 90) # note +PAD_91 = (0x90, 91) # note + +PAD_92 = (0x90, 92) # note +PAD_93 = (0x90, 96) # note +PAD_94 = (0x90, 94) # note +PAD_95 = (0x90, 95) # note +PAD_96 = (0x90, 96) # note +PAD_97 = (0x90, 97) # note +PAD_98 = (0x90, 98) # note +PAD_99 = (0x90, 99) # note + +## from pushmod.blogspot.com #### PUSH 1 SYSEX ####################################### # 71 is the manufacturer ID (Akai Electric Co. Ltd.) @@ -228,10 +221,10 @@ # Set channel aftertouch 240,71,127,21,92,0,1,1,247 # Set Live version 240,71,127,21,96,0,4,65,,,,247 # Set Live mode 240,71,127,21,98,0,1,0,247 -SYSEX_DATA_SET_LIVE_MODE= [240,71,127,21,98,0,1,0,247] +SYSEX_DATA_SET_LIVE_MODE = (240, 71, 127, 21, 98, 0, 1, 0, 247) # Set User mode 240,71,127,21,98,0,1,1,247 -SYSEX_DATA_SET_USER_MODE= [240,71,127,21,98,0,1,1,247] +SYSEX_DATA_SET_USER_MODE = (240, 71, 127, 21, 98, 0, 1, 1, 247) # Set touch strip mode 240,71,127,21,99,0,1,,247 # Request white calibration information 240,71,127,21,107,0,0,247 @@ -252,8 +245,8 @@ # 7 -> 127 - Lit ######### END MONOCHROMATIC LED ################## -#Bi-color LED table -#These are the colors which will be set on the bi-color (red/green) buttons below display +# Bi-color LED table +# These are the colors which will be set on the bi-color (red/green) buttons below display BI_LED_OFF = 0 # 0 - Off (Black) BI_RED_DIM = 1 # 1 - Red Dim @@ -280,6 +273,4 @@ BI_GREEN = 22 # 22 - Green BI_GREEN_BLINK = 23 # 23 - Green Blink BI_GREEN_BLINK_FAST = 24 # 24 - Green Blink Fast -#25 -> 127 - Green - - +# 25 -> 127 - Green \ No newline at end of file From 73fda46f4f85a962448152e7cd1e86707e5d1962 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 01:44:35 +0200 Subject: [PATCH 48/57] Hello World! display in startup --- zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py index ef5c82c15..394900546 100644 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py @@ -165,6 +165,7 @@ def __init__(self, state_manager, idev_in, idev_out=None): self._leds_bi = Feedback_Bi_LEDs(idev_out) # Bi-color display buttons self._leds_rgb = Feedback_RGB_LEDs(idev_out) # RGB pad LEDs self._display = Feedback_Display(idev_out) # Text display + self._display.first_screen() self.mixer_init() # Initialize mixer display # Required when sending translated MIDI events @@ -1191,7 +1192,7 @@ def first_screen(self): """Display welcome/splash screen""" self.clear() sleep(0.1) - self.write_xy_mem(b'** Zynthian Push1Driver 0.1 **', 17, 2) + self.write_xy_mem(b'** Zynthian Push1Driver 0.2 **', 17, 2) self.write_xy_mem(b'++ Make MusicNot War ++', 20, 3) self.update_screen() From 3ba994736691982eead7c3fbf8964d3b64ff4284 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 11:15:49 +0200 Subject: [PATCH 49/57] Push 1 Driver now working with internal key definitions --- .../zynthian_ctrldev_ableton_push_1_v2.py | 288 +++++++++++++++++- 1 file changed, 286 insertions(+), 2 deletions(-) diff --git a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py index 394900546..2783a9fad 100644 --- a/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py +++ b/zyngine/ctrldev/zynthian_ctrldev_ableton_push_1_v2.py @@ -68,10 +68,14 @@ # Brumby's custom imports from time import sleep # Pause between sysex events (Push 1 is slow) -import zyngine.ctrldev.ableton.push1_consts as ABL # Button definitions from zyngine.ctrldev.zynthian_ctrldev_base_scale import Harmony # Custom class for scales mode from zyngine.ctrldev.zynthian_ctrldev_base_extended import RunTimer, KnobSpeedControl, ButtonTimer, CONST +# Push 1 event definitions +# import zyngine.ctrldev.ableton.push1_consts as ABL # external Button definitions +# ABL is now defined as class at end of ile. + + # Zynthian core modules from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_zynpad, zynthian_ctrldev_zynmixer from zyncoder.zyncore import lib_zyncore @@ -1317,4 +1321,284 @@ def set_rgb(self, pad_nr, r, g, b, overlay=False): # Send SYSEX message sysex = bytes([240, 71, 127, 21, 4, 0, 8, pad_nr, 0, r1, r2, g1, g2, b1, b2, 247]) - lib_zyncore.dev_send_midi_event(self._idev, sysex, len(sysex)) \ No newline at end of file + lib_zyncore.dev_send_midi_event(self._idev, sysex, len(sysex)) + + + +class ABL: + # Ableton Push 1 consts + + ### Definition of all buttons, pads and knobs + # knobs and Buttons are CC-Events + # knobs also have touch function with midi note event + # + # ribbon is type modwheel !!! Just one byte ev[0] + # ribbon has also touch function with midi note even + # + # pad have note event + + # Definitions contain full event[:2] Data. You must not distinguish between different Midi Message types + # (Buttons are defined with their action Message. Noteon, Control Change) + + ### SYSEX + # SYSEX_PREAMBLE = [] + # SYSEX_END = [] + + # Display + # Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 + # Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 + # SYSEX_INST_WRITE_LINE_0 = bytes([24]) + # SYSEX_INST_WRITE_LINE_1 = bytes([25]) + # SYSEX_INST_WRITE_LINE_2 = bytes([26]) + # SYSEX_INST_WRITE_LINE_3 = bytes([27]) + + # Knobs 1-9 + KNOB_1 = (0xB0, 71) # CC71 + KNOB_2 = (0xB0, 72) # CC72 + KNOB_3 = (0xB0, 73) + KNOB_4 = (0xB0, 74) + KNOB_5 = (0xB0, 75) # CC75 + KNOB_6 = (0xB0, 76) # CC76 + KNOB_7 = (0xB0, 77) # CC77 + KNOB_8 = (0xB0, 78) # CC78 + KNOB_9 = (0xB0, 79) # CC79 + KNOB_10 = (0xB0, 14) # CC79 + KNOB_11 = (0xB0, 15) # CC79 + + # Touch + KNOB_1_T = (0x90, 0) # "C-1" sic! + KNOB_2_T = (0x90, 1) # "C#-1" + KNOB_3_T = (0x90, 2) # "D-1" + KNOB_4_T = (0x90, 3) # "D#-1" + KNOB_5_T = (0x90, 4) # Note 4 + KNOB_6_T = (0x90, 5) # Note 5 + KNOB_7_T = (0x90, 6) # note 6 + KNOB_8_T = (0x90, 7) # note 7 + KNOB_9_T = (0x90, 8) # Note 8 + KNOB_10_T = (0x90, 9) + KNOB_11_T = (0x90, 10) + + RIBBON_TOUCH_T = (0x90, 12) # "C0" note 12 + RIBBON_PITCH = (0xE0,) # Mod-wheel - Tupel mit einem Element! + + # Monochromatic Buttons + # Alle Button sind CC / Alle PAD sind Noteon + BTN_TAP_TEMPO = (0xB0, 3) + BTN_METRONOME = (0xB0, 9) + + BTN_FIXED_LENGTH = (0xB0, 90) + BTN_AUTOMATION = (0xB0, 89) + BTN_DUPLICATE = (0xB0, 88) + + BTN_NEW = (0xB0, 87) + BTN_REC = (0xB0, 86) + BTN_START = (0xB0, 85) + + ######### RECHTS ############ + BTN_PAN = (0xB0, 115) # CC115 + BTN_VOLUME = (0xB0, 114) # CC114 + + BTN_CLIP = (0xB0, 113) + BTN_TRACK = (0xB0, 112) + + BTN_BROWSE = (0xB0, 111) + BTN_DEVICE = (0xB0, 110) + + BTN_ESC = (0xB0, 63) + BTN_OK = (0xB0, 62) + BTN_SOLO = (0xB0, 61) + BTN_MUTE = (0xB0, 60) + BTN_USER = (0xB0, 59) + BTN_SCALES = (0xB0, 58) + BTN_ACCENT = (0xB0, 57) + BTN_REPEAT = (0xB0, 56) + BTN_OCTAVE_UP = (0xB0, 55) + BTN_OCTAVE_DOWN = (0xB0, 54) + + BTN_ADD_TRACK = (0xB0, 53) + BTN_ADD_EFFECT = (0xB0, 52) + BTN_SESSION = (0xB0, 51) + BTN_NOTE = (0xB0, 50) + BTN_SHIFT = (0xB0, 49) + BTN_SELECT = (0xB0, 48) + + BTN_UP = (0xB0, 46) + BTN_DOWN = (0xB0, 47) + BTN_LEFT = (0xB0, 44) + BTN_RIGHT = (0xB0, 45) + + # bottom up + BTN_BEAT_1_QUATER = (0xB0, 36) + BTN_BEAT_2_QUATER_T = (0xB0, 37) + BTN_BEAT_3_EIGHTH = (0xB0, 38) + BTN_BEAT_4_EIGHTH_T = (0xB0, 39) + BTN_BEAT_5_SIXTEENTH = (0xB0, 40) + BTN_BEAT_6_SIXTEENTH_T = (0xB0, 41) + BTN_BEAT_7_THIRTYSECOND = (0xB0, 42) + BTN_BEAT_8_THIRTYSECOND_T = (0xB0, 43) + + BTN_MASTER = (0xB0, 28) + BTN_STOP = (0xB0, 29) + + # Bicolor Buttons in the middle, below the display + # They have two colors, red and green. + BTN_R1_C1 = (0xB0, 20) + BTN_R1_C2 = (0xB0, 21) + BTN_R1_C3 = (0xB0, 22) + BTN_R1_C4 = (0xB0, 23) + BTN_R1_C5 = (0xB0, 24) + BTN_R1_C6 = (0xB0, 25) + BTN_R1_C7 = (0xB0, 26) + BTN_R1_C8 = (0xB0, 27) + + BTN_R2_C1 = (0xB0, 102) + BTN_R2_C2 = (0xB0, 103) + BTN_R2_C3 = (0xB0, 104) + BTN_R2_C4 = (0xB0, 105) + BTN_R2_C5 = (0xB0, 106) + BTN_R2_C6 = (0xB0, 107) + BTN_R2_C7 = (0xB0, 108) + BTN_R2_C8 = (0xB0, 109) + + # Have RGB-LED + PAD_36 = (0x90, 36) # note + PAD_37 = (0x90, 37) # note + PAD_38 = (0x90, 38) # note + PAD_39 = (0x90, 39) # note + PAD_40 = (0x90, 40) # note + PAD_41 = (0x90, 41) # note + PAD_42 = (0x90, 42) # note + PAD_43 = (0x90, 43) # note + + PAD_44 = (0x90, 44) # note + PAD_45 = (0x90, 45) # note + PAD_46 = (0x90, 46) # note + PAD_47 = (0x90, 47) # note + PAD_48 = (0x90, 48) # note + PAD_49 = (0x90, 49) # note + PAD_50 = (0x90, 50) # note + PAD_51 = (0x90, 51) # note + + PAD_52 = (0x90, 52) # note + PAD_53 = (0x90, 53) # note + PAD_54 = (0x90, 54) # note + PAD_55 = (0x90, 55) # note + PAD_56 = (0x90, 56) # note + PAD_57 = (0x90, 57) # note + PAD_58 = (0x90, 58) # note + PAD_59 = (0x90, 59) # note + + PAD_60 = (0x90, 60) # note + PAD_61 = (0x90, 61) # note + PAD_62 = (0x90, 62) # note + PAD_63 = (0x90, 63) # note + PAD_64 = (0x90, 64) # note + PAD_65 = (0x90, 65) # note + PAD_66 = (0x90, 66) # note + PAD_67 = (0x90, 67) # note + + PAD_68 = (0x90, 68) # note + PAD_69 = (0x90, 69) # note + PAD_70 = (0x90, 70) # note + PAD_71 = (0x90, 71) # note + PAD_72 = (0x90, 72) # note + PAD_73 = (0x90, 73) # note + PAD_74 = (0x90, 74) # note + PAD_75 = (0x90, 75) # note + + PAD_76 = (0x90, 76) # note + PAD_77 = (0x90, 77) # note + PAD_78 = (0x90, 78) # note + PAD_79 = (0x90, 79) # note + PAD_80 = (0x90, 80) # note + PAD_81 = (0x90, 81) # note + PAD_82 = (0x90, 82) # note + PAD_83 = (0x90, 83) # note + + PAD_84 = (0x90, 84) # note + PAD_85 = (0x90, 85) # note + PAD_86 = (0x90, 86) # note + PAD_87 = (0x90, 87) # note + PAD_88 = (0x90, 88) # note + PAD_89 = (0x90, 89) # note + PAD_90 = (0x90, 90) # note + PAD_91 = (0x90, 91) # note + + PAD_92 = (0x90, 92) # note + PAD_93 = (0x90, 96) # note + PAD_94 = (0x90, 94) # note + PAD_95 = (0x90, 95) # note + PAD_96 = (0x90, 96) # note + PAD_97 = (0x90, 97) # note + PAD_98 = (0x90, 98) # note + PAD_99 = (0x90, 99) # note + + ## from pushmod.blogspot.com + #### PUSH 1 SYSEX ####################################### + + # 71 is the manufacturer ID (Akai Electric Co. Ltd.) + # 127 is the device ID (default it 127 - All Devices) + # 21 is the product ID (Push) + # The Device ID can be sent as 0 as well. + + # Identity request 240,126,0,6,1,247 + # Set pad color (RGB) 240,71,127,21,4,0,8,,0,,,,,,,247 + # Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 + # Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 + # Set key aftertouch 240,71,127,21,92,0,1,0,247 + # Set channel aftertouch 240,71,127,21,92,0,1,1,247 + # Set Live version 240,71,127,21,96,0,4,65,,,,247 + # Set Live mode 240,71,127,21,98,0,1,0,247 + SYSEX_DATA_SET_LIVE_MODE = (240, 71, 127, 21, 98, 0, 1, 0, 247) + + # Set User mode 240,71,127,21,98,0,1,1,247 + SYSEX_DATA_SET_USER_MODE = (240, 71, 127, 21, 98, 0, 1, 1, 247) + + # Set touch strip mode 240,71,127,21,99,0,1,,247 + # Request white calibration information 240,71,127,21,107,0,0,247 + # Contrast request 240,71,127,21,122,0,0,247 + # Contrast set 240,71,127,21,122,0,1,, 247 + # Brightness request 240,71,127,21,124,0,0,247 + # Brightness set 240,71,127,21,124,0,1,,247 + ######### END PUSH 1 SYSEX ############################ + + ### Monochromatic Keys/Pads ####################### + MONO_LED_OFF = 0 # 0 - Off + MONO_LED_DIM = 1 # 1 - Dim + MONO_LED_DIM_BLINK = 2 # 2 - Dim Blink + MONO_LED_DIM_BLINK_FAST = 3 # 3 - Dim Blink Fast + MONO_LED_LIT = 4 # 4 - Lit + MONO_LED_LIT_BLINK = 5 # 5 - Lit Blink + MONO_LED_LIT_BLINK_FAST = 6 # 6 - Lit Blink Fast + # 7 -> 127 - Lit + ######### END MONOCHROMATIC LED ################## + + # Bi-color LED table + # These are the colors which will be set on the bi-color (red/green) buttons below display + + BI_LED_OFF = 0 # 0 - Off (Black) + BI_RED_DIM = 1 # 1 - Red Dim + BI_RED_DIM_BLINK = 2 # 2 - Red Dim Blink + BI_RED_DIM_BLINK_FAST = 3 # 3 - Red Dim Blink Fast + BI_RED = 4 # 4 - Red + BI_RED_BLINK = 5 # 5 - Red Blink + BI_RED_BLINK_FAST = 6 # 6 - Red Blink Fast + BI_ORANGE_DIM = 7 # 7 - Orange Dim + BI_ORANGE_DIM_BLINK = 8 # 8 - Orange Dim Blink + BI_ORANGE_DIM_BLINK_FAST = 9 # 9 - Orange Dim Blink Fast + BI_ORANGE = 10 # 10 - Orange + BI_ORANGE_BLINK = 11 # 11 - Orange Blink + BI_ORANGE_BLINK_FAST = 12 # 12 - Orange Blink Fast + BI_YELLOW_DIM = 13 # 13 - Yellow (Lime) Dim + BI_YELLOW_DIM_BLINK = 14 # 14 - Yellow Dim Blink + BI_YELLOW_DIM_BLINK_FAST = 15 # 15 - Yellow Dim Blink Fast + BI_YELLOW = 16 # 16 - Yellow (Lime) + BI_YELLOW_BLINK = 17 # 17 - Yellow Blink + BI_YELLOW_BLINK_FAST = 18 # 18 - Yellow Blink Fast + BI_GREEN_DIM = 19 # 19 - Green Dim + BI_GREEN_DIM_BLINK = 20 # 20 - Green Dim Blink + BI_GREEN_DIM_BLINK_FAST = 21 # 21 - Green Dim Blink Fast + BI_GREEN = 22 # 22 - Green + BI_GREEN_BLINK = 23 # 23 - Green Blink + BI_GREEN_BLINK_FAST = 24 # 24 - Green Blink Fast + # 25 -> 127 - Green \ No newline at end of file From 71be75d91c1f1fd4b74cdf03a2f92c8b73208f28 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 11:18:14 +0200 Subject: [PATCH 50/57] removed supbdir with const def file --- zyngine/ctrldev/ableton/push1_consts.py | 276 ------------------------ 1 file changed, 276 deletions(-) delete mode 100644 zyngine/ctrldev/ableton/push1_consts.py diff --git a/zyngine/ctrldev/ableton/push1_consts.py b/zyngine/ctrldev/ableton/push1_consts.py deleted file mode 100644 index e992800d8..000000000 --- a/zyngine/ctrldev/ableton/push1_consts.py +++ /dev/null @@ -1,276 +0,0 @@ -# Ableton Push 1 - -### Definition of all buttons, pads and knobs -# knobs and Buttons are CC-Events -# knobs also have touch function with midi note event -# -# ribbon is type modwheel !!! Just one byte ev[0] -# ribbon has also touch function with midi note even -# -# pad has note event - -##### -# Buttons are defined with their action Message. Noteon, Control Change - -### SYSEX -# SYSEX_PREAMBLE = [] -# SYSEX_END = [] - -# Display -# Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 -# Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 -# SYSEX_INST_WRITE_LINE_0 = bytes([24]) -# SYSEX_INST_WRITE_LINE_1 = bytes([25]) -# SYSEX_INST_WRITE_LINE_2 = bytes([26]) -# SYSEX_INST_WRITE_LINE_3 = bytes([27]) - -# Knobs 1-9 -KNOB_1 = (0xB0, 71) # CC71 -KNOB_2 = (0xB0, 72) # CC72 -KNOB_3 = (0xB0, 73) -KNOB_4 = (0xB0, 74) -KNOB_5 = (0xB0, 75) # CC75 -KNOB_6 = (0xB0, 76) # CC76 -KNOB_7 = (0xB0, 77) # CC77 -KNOB_8 = (0xB0, 78) # CC78 -KNOB_9 = (0xB0, 79) # CC79 -KNOB_10 = (0xB0, 14) # CC79 -KNOB_11 = (0xB0, 15) # CC79 - -# Touch -KNOB_1_T = (0x90, 0) # "C-1" sic! -KNOB_2_T = (0x90, 1) # "C#-1" -KNOB_3_T = (0x90, 2) # "D-1" -KNOB_4_T = (0x90, 3) # "D#-1" -KNOB_5_T = (0x90, 4) # Note 4 -KNOB_6_T = (0x90, 5) # Note 5 -KNOB_7_T = (0x90, 6) # note 6 -KNOB_8_T = (0x90, 7) # note 7 -KNOB_9_T = (0x90, 8) # Note 8 -KNOB_10_T = (0x90, 9) -KNOB_11_T = (0x90, 10) - -RIBBON_TOUCH_T = (0x90, 12) # "C0" note 12 -RIBBON_PITCH = (0xE0,) # Mod-wheel - Tupel mit einem Element! - -# Monochromatic Buttons -# Alle Button sind CC / Alle PAD sind Noteon -BTN_TAP_TEMPO = (0xB0, 3) -BTN_METRONOME = (0xB0, 9) - -BTN_FIXED_LENGTH = (0xB0, 90) -BTN_AUTOMATION = (0xB0, 89) -BTN_DUPLICATE = (0xB0, 88) - -BTN_NEW = (0xB0, 87) -BTN_REC = (0xB0, 86) -BTN_START = (0xB0, 85) - -######### RECHTS ############ -BTN_PAN = (0xB0, 115) # CC115 -BTN_VOLUME = (0xB0, 114) # CC114 - -BTN_CLIP = (0xB0, 113) -BTN_TRACK = (0xB0, 112) - -BTN_BROWSE = (0xB0, 111) -BTN_DEVICE = (0xB0, 110) - -BTN_ESC = (0xB0, 63) -BTN_OK = (0xB0, 62) -BTN_SOLO = (0xB0, 61) -BTN_MUTE = (0xB0, 60) -BTN_USER = (0xB0, 59) -BTN_SCALES = (0xB0, 58) -BTN_ACCENT = (0xB0, 57) -BTN_REPEAT = (0xB0, 56) -BTN_OCTAVE_UP = (0xB0, 55) -BTN_OCTAVE_DOWN = (0xB0, 54) - -BTN_ADD_TRACK = (0xB0, 53) -BTN_ADD_EFFECT = (0xB0, 52) -BTN_SESSION = (0xB0, 51) -BTN_NOTE = (0xB0, 50) -BTN_SHIFT = (0xB0, 49) -BTN_SELECT = (0xB0, 48) - -BTN_UP = (0xB0, 46) -BTN_DOWN = (0xB0, 47) -BTN_LEFT = (0xB0, 44) -BTN_RIGHT = (0xB0, 45) - -# bottom up -BTN_BEAT_1_QUATER = (0xB0, 36) -BTN_BEAT_2_QUATER_T = (0xB0, 37) -BTN_BEAT_3_EIGHTH = (0xB0, 38) -BTN_BEAT_4_EIGHTH_T = (0xB0, 39) -BTN_BEAT_5_SIXTEENTH = (0xB0, 40) -BTN_BEAT_6_SIXTEENTH_T = (0xB0, 41) -BTN_BEAT_7_THIRTYSECOND = (0xB0, 42) -BTN_BEAT_8_THIRTYSECOND_T = (0xB0, 43) - -BTN_MASTER = (0xB0, 28) -BTN_STOP = (0xB0, 29) - -# Bicolor Buttons in the middle, below the display -# They have two colors, red and green. -BTN_R1_C1 = (0xB0, 20) -BTN_R1_C2 = (0xB0, 21) -BTN_R1_C3 = (0xB0, 22) -BTN_R1_C4 = (0xB0, 23) -BTN_R1_C5 = (0xB0, 24) -BTN_R1_C6 = (0xB0, 25) -BTN_R1_C7 = (0xB0, 26) -BTN_R1_C8 = (0xB0, 27) - -BTN_R2_C1 = (0xB0, 102) -BTN_R2_C2 = (0xB0, 103) -BTN_R2_C3 = (0xB0, 104) -BTN_R2_C4 = (0xB0, 105) -BTN_R2_C5 = (0xB0, 106) -BTN_R2_C6 = (0xB0, 107) -BTN_R2_C7 = (0xB0, 108) -BTN_R2_C8 = (0xB0, 109) - -# Have RGB-LED -PAD_36 = (0x90, 36) # note -PAD_37 = (0x90, 37) # note -PAD_38 = (0x90, 38) # note -PAD_39 = (0x90, 39) # note -PAD_40 = (0x90, 40) # note -PAD_41 = (0x90, 41) # note -PAD_42 = (0x90, 42) # note -PAD_43 = (0x90, 43) # note - -PAD_44 = (0x90, 44) # note -PAD_45 = (0x90, 45) # note -PAD_46 = (0x90, 46) # note -PAD_47 = (0x90, 47) # note -PAD_48 = (0x90, 48) # note -PAD_49 = (0x90, 49) # note -PAD_50 = (0x90, 50) # note -PAD_51 = (0x90, 51) # note - -PAD_52 = (0x90, 52) # note -PAD_53 = (0x90, 53) # note -PAD_54 = (0x90, 54) # note -PAD_55 = (0x90, 55) # note -PAD_56 = (0x90, 56) # note -PAD_57 = (0x90, 57) # note -PAD_58 = (0x90, 58) # note -PAD_59 = (0x90, 59) # note - -PAD_60 = (0x90, 60) # note -PAD_61 = (0x90, 61) # note -PAD_62 = (0x90, 62) # note -PAD_63 = (0x90, 63) # note -PAD_64 = (0x90, 64) # note -PAD_65 = (0x90, 65) # note -PAD_66 = (0x90, 66) # note -PAD_67 = (0x90, 67) # note - -PAD_68 = (0x90, 68) # note -PAD_69 = (0x90, 69) # note -PAD_70 = (0x90, 70) # note -PAD_71 = (0x90, 71) # note -PAD_72 = (0x90, 72) # note -PAD_73 = (0x90, 73) # note -PAD_74 = (0x90, 74) # note -PAD_75 = (0x90, 75) # note - -PAD_76 = (0x90, 76) # note -PAD_77 = (0x90, 77) # note -PAD_78 = (0x90, 78) # note -PAD_79 = (0x90, 79) # note -PAD_80 = (0x90, 80) # note -PAD_81 = (0x90, 81) # note -PAD_82 = (0x90, 82) # note -PAD_83 = (0x90, 83) # note - -PAD_84 = (0x90, 84) # note -PAD_85 = (0x90, 85) # note -PAD_86 = (0x90, 86) # note -PAD_87 = (0x90, 87) # note -PAD_88 = (0x90, 88) # note -PAD_89 = (0x90, 89) # note -PAD_90 = (0x90, 90) # note -PAD_91 = (0x90, 91) # note - -PAD_92 = (0x90, 92) # note -PAD_93 = (0x90, 96) # note -PAD_94 = (0x90, 94) # note -PAD_95 = (0x90, 95) # note -PAD_96 = (0x90, 96) # note -PAD_97 = (0x90, 97) # note -PAD_98 = (0x90, 98) # note -PAD_99 = (0x90, 99) # note - -## from pushmod.blogspot.com -#### PUSH 1 SYSEX ####################################### - -# 71 is the manufacturer ID (Akai Electric Co. Ltd.) -# 127 is the device ID (default it 127 - All Devices) -# 21 is the product ID (Push) -# The Device ID can be sent as 0 as well. - -# Identity request 240,126,0,6,1,247 -# Set pad color (RGB) 240,71,127,21,4,0,8,,0,,,,,,,247 -# Write text to display 240,71,127,21,<24+line(0-3)>,0,,,,247 -# Clear display line 240,71,127,21,<28+line(0-3)>,0,0,247 -# Set key aftertouch 240,71,127,21,92,0,1,0,247 -# Set channel aftertouch 240,71,127,21,92,0,1,1,247 -# Set Live version 240,71,127,21,96,0,4,65,,,,247 -# Set Live mode 240,71,127,21,98,0,1,0,247 -SYSEX_DATA_SET_LIVE_MODE = (240, 71, 127, 21, 98, 0, 1, 0, 247) - -# Set User mode 240,71,127,21,98,0,1,1,247 -SYSEX_DATA_SET_USER_MODE = (240, 71, 127, 21, 98, 0, 1, 1, 247) - -# Set touch strip mode 240,71,127,21,99,0,1,,247 -# Request white calibration information 240,71,127,21,107,0,0,247 -# Contrast request 240,71,127,21,122,0,0,247 -# Contrast set 240,71,127,21,122,0,1,, 247 -# Brightness request 240,71,127,21,124,0,0,247 -# Brightness set 240,71,127,21,124,0,1,,247 -######### END PUSH 1 SYSEX ############################ - -### Monochromatic Keys/Pads ####################### -MONO_LED_OFF = 0 # 0 - Off -MONO_LED_DIM = 1 # 1 - Dim -MONO_LED_DIM_BLINK = 2 # 2 - Dim Blink -MONO_LED_DIM_BLINK_FAST = 3 # 3 - Dim Blink Fast -MONO_LED_LIT = 4 # 4 - Lit -MONO_LED_LIT_BLINK = 5 # 5 - Lit Blink -MONO_LED_LIT_BLINK_FAST = 6 # 6 - Lit Blink Fast -# 7 -> 127 - Lit -######### END MONOCHROMATIC LED ################## - -# Bi-color LED table -# These are the colors which will be set on the bi-color (red/green) buttons below display - -BI_LED_OFF = 0 # 0 - Off (Black) -BI_RED_DIM = 1 # 1 - Red Dim -BI_RED_DIM_BLINK = 2 # 2 - Red Dim Blink -BI_RED_DIM_BLINK_FAST = 3 # 3 - Red Dim Blink Fast -BI_RED = 4 # 4 - Red -BI_RED_BLINK = 5 # 5 - Red Blink -BI_RED_BLINK_FAST = 6 # 6 - Red Blink Fast -BI_ORANGE_DIM = 7 # 7 - Orange Dim -BI_ORANGE_DIM_BLINK = 8 # 8 - Orange Dim Blink -BI_ORANGE_DIM_BLINK_FAST = 9 # 9 - Orange Dim Blink Fast -BI_ORANGE = 10 # 10 - Orange -BI_ORANGE_BLINK = 11 # 11 - Orange Blink -BI_ORANGE_BLINK_FAST = 12 # 12 - Orange Blink Fast -BI_YELLOW_DIM = 13 # 13 - Yellow (Lime) Dim -BI_YELLOW_DIM_BLINK = 14 # 14 - Yellow Dim Blink -BI_YELLOW_DIM_BLINK_FAST = 15 # 15 - Yellow Dim Blink Fast -BI_YELLOW = 16 # 16 - Yellow (Lime) -BI_YELLOW_BLINK = 17 # 17 - Yellow Blink -BI_YELLOW_BLINK_FAST = 18 # 18 - Yellow Blink Fast -BI_GREEN_DIM = 19 # 19 - Green Dim -BI_GREEN_DIM_BLINK = 20 # 20 - Green Dim Blink -BI_GREEN_DIM_BLINK_FAST = 21 # 21 - Green Dim Blink Fast -BI_GREEN = 22 # 22 - Green -BI_GREEN_BLINK = 23 # 23 - Green Blink -BI_GREEN_BLINK_FAST = 24 # 24 - Green Blink Fast -# 25 -> 127 - Green \ No newline at end of file From 29402db5250861f70012b1fd2503057a031fd7e5 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 17:56:01 +0200 Subject: [PATCH 51/57] Delete zyngine/zynthian_state_manager.py Should not be changed --- zyngine/zynthian_state_manager.py | 2822 ----------------------------- 1 file changed, 2822 deletions(-) delete mode 100644 zyngine/zynthian_state_manager.py diff --git a/zyngine/zynthian_state_manager.py b/zyngine/zynthian_state_manager.py deleted file mode 100644 index ab3c5c5d1..000000000 --- a/zyngine/zynthian_state_manager.py +++ /dev/null @@ -1,2822 +0,0 @@ -# -*- coding: utf-8 -*- -# **************************************************************************** -# ZYNTHIAN PROJECT: Zynthian State Manager (zynthian_state_manager) -# -# zynthian state manager -# -# Copyright (C) 2015-2024 Fernando Moyano -# Brian Walton -# -# **************************************************************************** -# -# 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 base64 -import ctypes -import logging -import traceback -from glob import glob -from threading import Thread -from queue import SimpleQueue -from datetime import datetime -from time import sleep, monotonic -from json import JSONEncoder, JSONDecoder -from subprocess import check_output, Popen, STDOUT, PIPE -from os.path import basename, isdir, isfile, join, dirname, splitext - -# Zynthian specific modules -import zynconf -import zynautoconnect - -from zyncoder.zyncore import lib_zyncore -from zynlibs.zynaudioplayer import * -from zynlibs.zynseq import zynseq -# Python wrapper for zynsmf (ensures initialised and wraps load() function) -from zynlibs.zynsmf import zynsmf -from zynlibs.zynsmf.zynsmf import libsmf # Direct access to shared library - -from zyngine.zynthian_chain_manager import * -from zyngine.zynthian_processor import zynthian_processor -from zyngine.zynthian_audio_recorder import zynthian_audio_recorder -from zyngine.zynthian_signal_manager import zynsigman -from zyngine.zynthian_legacy_snapshot import zynthian_legacy_snapshot, SNAPSHOT_SCHEMA_VERSION -from zyngine import zynthian_engine_audio_mixer -from zyngine import zynthian_midi_filter - -from zyngui import zynthian_gui_config -from zyngine.zynthian_ctrldev_manager import zynthian_ctrldev_manager - -# ---------------------------------------------------------------------------- -# Zynthian State Manager Class -# ---------------------------------------------------------------------------- - -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") - - -class zynthian_state_manager: - - # Subsignals are defined inside each module. Here we define state manager subsignals: - SS_LOAD_SNAPSHOT = 1 - SS_MIDI_PLAYER_STATE = 2 - SS_MIDI_RECORDER_STATE = 3 - SS_LOAD_ZS3 = 4 - SS_SAVE_ZS3 = 5 - SS_ALL_NOTES_OFF = 6 - - # Subsignals from other modules. Just to simplify access. - # From S_AUDIO_PLAYER - SS_AUDIO_PLAYER_STATE = 1 - # From S_AUDIO_RECORDER - SS_AUDIO_RECORDER_STATE = 1 - SS_AUDIO_RECORDER_ARM = 2 - - def __init__(self): - """ Create an instance of a state manager - - Manages full Zynthian state, i.e. snapshot - """ - - logging.info("Creating state manager") - - self.busy = set() # Set of clients indicating they are busy doing something (may be used by UI to show progress) - self.busy_message = None - self.busy_error = None - self.busy_warning = None - self.busy_success = None - 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.default_snapshot_fpath = join(self.snapshot_dir, "default.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 = "" - self.snapshot_bank = None # Name of snapshot bank (without path) - self.snapshot_program = 0 - self.zs3 = {} # Dictionary or zs3 configs indexed by "ch/pc" - self.last_zs3_id = None - - # Power saving - self.power_save_mode = False - self.last_event_flag = False - self.last_event_ts = monotonic() - - # Status - self.status_xrun = False - self.status_undervoltage = False - self.overtemp_warning = 75 # Temperature limit before warning overtemperature - self.status_overtemp = False - self.status_cpu_load = 0 # 0..100 - self.status_audio_player = False # True if playing - self.status_midi_recorder = False - self.status_midi_player = False - self.last_midi_file = None - self.status_midi = False - self.status_midi_clock = False - self.update_available = False # True when updates available from repositories - self.checking_for_updates = False # True whilst checking for updates - - self.midi_filter_script = None - self.midi_learn_state = False - # When ZS3 Program Change MIDI learning is enabled, the name used for creating new ZS3, empty string for auto-generating a name. None when disabled. - self.midi_learn_pc = None - self.midi_learn_zctrl = None # zctrl currently being learned - self.sync = False # True to request file system sync - self.zctrl_x = None - self.zctrl_y = None - - self.cuia_queue = SimpleQueue() # Queue for CUIA calls - - self.get_throttled_file = None - self.hwmon_thermal_file = None - self.hwmon_undervolt_file = None - - self.zynmixer = zynthian_engine_audio_mixer.zynmixer() - 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.refresh_controllers() - - self.audio_recorder = zynthian_audio_recorder(self) - self.zynseq = zynseq.zynseq(self) - self.ctrldev_manager = None - self.audio_player = None - self.aubio_in = [1, 2] # List of aubio inputs - - # List of lists [rate, cb, schedule] for registered regularly repeating callbacks - self.slow_update_callbacks = [] - - # Initialize SMF MIDI recorder and player - try: - self.smf_player = libsmf.addSmf() - libsmf.attachPlayer(self.smf_player) - except Exception as e: - logging.error(e) - - try: - self.smf_recorder = libsmf.addSmf() - libsmf.attachRecorder(self.smf_recorder) - except Exception as e: - logging.error(e) - - # Initialize internal MIDI sender - self.zynmidi = zynthian_zcmidi() - - self.exit_flag = False - self.slow_thread = None - self.fast_thread = None - self.start() - - self.end_busy("zynthian_state_manager") - - def start(self): - """Start state manager""" - - self.start_busy("start state") - # Initialize SOC sensors monitoring - - # Sysfs->hwmon monitoring interface - try: - sfpath = '/sys/class/hwmon/hwmon0/temp1_input' - self.hwmon_thermal_file = open(sfpath) - logging.debug(f"Opened temperature sensor '{sfpath}'") - except: - self.hwmon_thermal_file = None - logging.error("Can't access temperature sensor.") - - try: - result = glob("/sys/class/hwmon/**/in0_lcrit_alarm") - self.hwmon_undervolt_file = open(result[0]) - logging.debug(f"Opened undervoltage sensor '{result[0]}'") - except: - try: - 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: - self.hwmon_undervolt_file = None - logging.error("Can't access undervoltage sensor.") - - # 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') - except: - self.get_throttled_file = None - - # Start VNC as configured - self.default_vncserver() - - self.ctrldev_manager = zynthian_ctrldev_manager(self) - zynautoconnect.start(self) - self.jack_period = self.get_jackd_blocksize() / self.get_jackd_samplerate() - self.zynmixer.reset_state() - self.reload_midi_config() - self.create_audio_player() - self.chain_manager.add_chain(0) - - self.exit_flag = False - self.slow_thread = Thread(target=self.slow_thread_task) - self.slow_thread.name = "Status Manager Slow" - self.slow_thread.daemon = True # thread dies with the program - self.slow_thread.start() - - self.fast_thread = Thread(target=self.fast_thread_task) - self.fast_thread.name = "Status Manager Fast" - 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) - - self.end_busy("start state") - - def stop(self): - """Stop state manager""" - - self.start_busy("stop state") - - 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(): - self.fast_thread.join() - self.fast_thread = None - if self.slow_thread and self.slow_thread.is_alive(): - self.slow_thread.join() - self.slow_thread = None - - self.last_snapshot_fpath = "" - self.zynseq.transport_stop("ALL") - zynautoconnect.pause() - self.chain_manager.remove_all_chains(True) - self.reset_zs3() - self.zynseq.load("") - self.ctrldev_manager.unload_all_drivers() - self.destroy_audio_player() - zynautoconnect.stop() - - if self.hwmon_thermal_file: - self.hwmon_thermal_file.close() - self.hwmon_thermal_file = None - if self.hwmon_undervolt_file: - self.hwmon_undervolt_file.close() - self.hwmon_undervolt_file = None - if self.get_throttled_file: - self.get_throttled_file.close() - self.get_throttled_file = None - - self.end_busy("stop state") - - def reset(self): - """Reset state manager to clean initial start-up state""" - - self.start_busy("reset state") - self.stop() - sleep(0.2) - self.clear_busy() # TODO Is this needed? - self.start() - self.end_busy("reset state") - - def clean(self, chains=True, zynseq=True): - """Remove Chains & Sequences. - chains : True for cleaning all chains - sequences : True for cleaning zynseq state (sequences) - """ - - self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, 1) - # self.zynseq.transport_stop("ALL") - self.zynseq.libseq.stop() - if zynseq: - self.zynseq.load("") - if chains: - zynautoconnect.pause() - self.chain_manager.remove_all_chains(True) - self.reset_zs3() - self.zynmixer.reset_state() - self.reload_midi_config() - zynautoconnect.request_midi_connect(True) - zynautoconnect.request_audio_connect(True) - zynautoconnect.resume() - self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, 0) - - def clean_all(self): - """Remove ALL Chains & Sequences.""" - - self.start_busy("clean all", "cleaning all...") - self.clean(chains=True, zynseq=True) - self.last_snapshot_fpath = "" - self.end_busy("clean all") - self.busy.clear() # Sometimes it's needed, why?? - - def clean_chains(self): - """Remove ALL chains while keeping sequences.""" - - self.start_busy("clean chains", "cleaning chains...") - self.clean(chains=True, zynseq=False) - self.end_busy("clean chains") - self.busy.clear() # Sometimes it's needed, why?? - - def clean_sequences(self): - """Remove ALL sequences while keeping chains.""" - - self.start_busy("clean sequences", "cleaning sequences...") - self.clean(chains=False, zynseq=True) - self.end_busy("clean sequences") - self.busy.clear() # Sometimes it's needed, why?? - - # ------------------------------------------------------------------------- - # Internal parameters and core limits - # ------------------------------------------------------------------------- - - def get_max_num_mixer_chans(self): - return MAX_NUM_MIXER_CHANS - - def get_num_zmop_chains(self): - return NUM_ZMOP_CHAINS - - def get_max_num_zmops(self): - return MAX_NUM_ZMOPS - - def get_num_midi_devs_in(self): - return NUM_MIDI_DEVS_IN - - def get_num_midi_devs_out(self): - return NUM_MIDI_DEVS_OUT - - def get_max_num_midi_devs(self): - return MAX_NUM_MIDI_DEVS - - def get_zmip_seq_index(self): - return ZMIP_SEQ_INDEX - - def get_zmip_step_index(self): - return ZMIP_STEP_INDEX - - def get_zmip_int_index(self): - return ZMIP_INT_INDEX - - def get_zmip_ctrl_index(self): - return ZMIP_CTRL_INDEX - - # ------------------------------------------------------------------------- - # Busy state management - # ------------------------------------------------------------------------- - - def start_busy(self, clid, message=None, details=None): - """Add client to list of busy clients - clid : Client id - """ - - self.busy.add(clid) - if message: - self.busy_message = message - if details: - self.busy_details = details - - # logging.debug(f"Start busy for {clid}. Message: '{message}', Details: '{details}', Current clients: {self.busy})") - - def end_busy(self, clid): - """Remove client from list of busy clients - clid : Client id - """ - - try: - self.busy.remove(clid) - except: - pass - if len(self.busy) == 0: - self.busy_message = None - self.busy_error = None - self.busy_warning = None - self.busy_success = None - self.busy_details = None - - # logging.debug(f"End busy for {clid}. Remaining clients: {self.busy}") - - def clear_busy(self): - self.busy.clear() - self.busy_message = None - self.busy_error = None - self.busy_warning = None - self.busy_success = None - self.busy_details = None - - def is_busy(self, client=None): - """Check if clients are busy - client : Name of client to check (Default: all clients) - Returns : True if any clients are busy - """ - - if client: - return client in self.busy - return len(self.busy) > 0 - - def set_busy_message(self, message, details=None): - """Set busy message - message : message text - """ - - if len(self.busy) > 0: - self.busy_message = message - if details: - self.details = details - - def get_busy_message(self): - """Returns busy message and clean it - return message text - """ - - res = self.busy_message - self.busy_message = None - return res - - def set_busy_error(self, message, details=None): - """Set busy error message - message : message text - """ - - if len(self.busy) > 0: - self.busy_error = message - if details: - self.details = details - - def get_busy_error(self): - """Returns busy error message and clean it - return message text - """ - - res = self.busy_error - self.busy_error = None - return res - - def set_busy_warning(self, message, details=None): - """Set busy warning message - message : message text - """ - - if len(self.busy) > 0: - self.busy_warning = message - if details: - self.details = details - - def get_busy_warning(self): - """Returns busy warning message and clean it - return message text - """ - - res = self.busy_warning - self.busy_warning = None - return res - - def set_busy_success(self, message, details=None): - """Set busy success message text - details : details text - """ - - if len(self.busy) > 0: - self.busy_success = message - if details: - self.details = details - - def get_busy_success(self): - """Returns busy success message and clean it - return message text - """ - - res = self.busy_success - self.busy_success = None - return res - - def set_busy_details(self, details): - """Set busy details text - details : details text - """ - - if len(self.busy) > 0: - self.busy_details = details - - def get_busy_details(self): - """Returns busy details and clean it - return details text - """ - - res = self.busy_details - self.busy_details = None - return res - - # ---------------------------------------------------------------------------- - # CUIA Queue - # ---------------------------------------------------------------------------- - - def send_cuia(self, cuia, params=None): - self.cuia_queue.put_nowait((cuia, params)) - - def parse_cuia_params(self, params_str): - params = [] - for i, p in enumerate(params_str.split(",")): - try: - params.append(int(p)) - except: - params.append(p.strip()) - return params - - # ------------------------------------------------------------------ - # Background task threads - # ------------------------------------------------------------------ - - def slow_thread_task(self): - """Perform slow / low priority background tasks""" - - status_counter = 0 - xruns_status = self.status_xrun - midi_status = self.status_midi - midi_clock_status = self.status_midi_clock - # Short delay after startup before first slow update - next_second_check = monotonic() + 2 - self.add_slow_update_callback(3600, self.check_for_updates) - - while not self.exit_flag: - # Get CPU Load - # self.status_cpu_load = max(psutil.cpu_percent(None, True)) - self.status_cpu_load = zynautoconnect.get_jackd_cpu_load() - now = monotonic() - - try: - # Get SOC sensors (once each 5 refreshes) - if status_counter > 5: - status_counter = 0 - - self.status_overtemp = False - self.status_undervoltage = False - - # RBPi native sensors interface - if self.get_throttled_file: - try: - self.get_throttled_file.seek(0) - thr = int('0x%s' % self.get_throttled_file.read(), 16) - if thr & 0x1: - self.status_undervoltage = True - elif thr & (0x4 | 0x2): - self.status_overtemp = True - except Exception as e: - logging.error(e) - - # Alternate sensor interface - elif self.hwmon_thermal_file and self.hwmon_undervolt_file: - try: - self.hwmon_thermal_file.seek(0) - res = int(self.hwmon_thermal_file.read())/1000 - # logging.debug(f"CPU Temperature => {res}") - if res > self.overtemp_warning: - self.status_overtemp = True - except Exception as e: - logging.error(e) - - try: - self.hwmon_undervolt_file.seek(0) - res = self.hwmon_undervolt_file.read() - if res == "1": - self.status_undervoltage = True - except Exception as e: - logging.error(e) - - else: - self.status_overtemp = True - self.status_undervoltage = True - - else: - status_counter += 1 - - # MIDI Player - # TODO: Add callback from MIDI player to avoid polling (and regular access to c-lib) - 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) - - # 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) - - # Sequencer Status => It must be improved using callbacks - self.zynseq.update_state() - - # Clean some status flags - if xruns_status: - self.status_xrun = False - xruns_status = False - if self.status_xrun: - xruns_status = True - - if midi_status: - self.status_midi = False - midi_status = False - if self.status_midi: - midi_status = True - - if midi_clock_status: - self.status_midi_clock = False - midi_clock_status = False - if self.status_midi_clock: - midi_clock_status = True - - if self.sync: - self.sync = False - os.sync() - - if now > next_second_check: - for cb in self.slow_update_callbacks: - if now > cb[2]: - try: - cb[1]() - cb[2] = now + cb[0] - except Exception as e: - logging.error(e) - next_second_check = now + 1 - - except Exception as e: - logging.exception(e) - - sleep(0.2) - - def cb_status_audio_player(self, handle, state): - if handle == self.audio_player.handle: - self.status_audio_player = state - - def fast_thread_task(self): - """Perform fast / high priority background tasks""" - - while not self.exit_flag: - # Process MIDI events - self.zynmidi_read() - sleep(0.01) - - def add_slow_update_callback(self, rate, cb): - """Add a callback to be called every "rate" seconds - - rate - time in seconds between callbacks - cb - Callback function - """ - - self.remove_slow_update_callback(cb) - self.slow_update_callbacks.append([rate, cb, 0]) - - def remove_slow_update_callback(self, cb): - """Add a callback to be called every "rate" seconds - - rate - time in seconds between callbacks - cb - Callback function - """ - - for cb in self.slow_update_callbacks: - if cb[1] == cb: - cb.remove(cb) - break - - # ------------------------------------------------------------------ - # MIDI processing - # ------------------------------------------------------------------ - - def zynmidi_read(self): - try: - n = lib_zyncore.get_zynmidi_num_pending() - if n <= 0: - return - midi_events = (ctypes.c_uint32 * n)() - n = lib_zyncore.read_zynmidi_buffer(midi_events, n) - i = 0 - while i < n: - ev = midi_events[i].to_bytes(4, 'big') - i += 1 - izmip = ev[0] - evhead = ev[1] - ev = ev[1:] - - # Process SysEx - if evhead == 0xF0: - # logging.debug(f"RECEIVED SYSEX FROM {izmip}...") - sysex_data = bytearray(ev) - while i < n: - chunk = midi_events[i].to_bytes(4, 'big') - sysex_data.extend(chunk) - if 0xF7 in chunk: - break - i += 1 - # This is probably not correct and we should continue reading in the next period - if i == n: - logging.error(f"SysEx message from device {izmip} is not terminated") - continue - # Crop data until find the 0xF7 mark - while sysex_data[-1] != 0xF7: - del sysex_data[-1] - - # logging.debug(f" SYSEX DATA => {sysex_data}") - ev = bytes(sysex_data) - - # Try to manage with a control device driver - if self.ctrldev_manager.midi_event(izmip, ev): - self.status_midi = True - self.last_event_flag = True - continue - - """ # brumby 250906-2230 - ret_value = self.ctrldev_manager.midi_event(izmip, ev) - if isinstance(ret_value, bool): - # logging.info(f"returns boolean: {ret_value}") - if ret_value: - # if true, the driver self processed the event. if false, the event must be processed as usual - self.status_midi = True - self.last_event_flag = True - continue # process new event from midi_in - elif isinstance(ret_value, bytes) or isinstance(ret_value, bytearray): - # driver modified the event and returns it - logging.info(f"returns event: {ret_value.hex()}") - evhead = ret_value[0] # update in Queue used varable. - ev = ret_value # now process returned new event down the line - else: - logging.error("wrong return type. not boolean, not midi_event") - """ - - evtype = (evhead >> 4) & 0x0F - chan = evhead & 0x0F - - # logging.info(f"MIDI EVENT: IZMIP={izmip}, TYPE={evtype}, CHAN={chan}") - - # System Messages (Common & RT) - if evtype == 0xF: - # SysEx - if chan == 0x0: - # Handle SysEx from external devices only - if izmip < self.get_max_num_midi_devs(): - zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_SYSEX, izmip=izmip, data=ev) - # Clock - elif chan == 0x8: - self.status_midi_clock = True - continue - # Tick - elif chan == 0x9: - continue - # Active Sense - elif chan == 0xE: - continue - # Reset - elif chan == 0xF: - pass - - # Master MIDI Channel... - elif chan == zynthian_gui_config.master_midi_channel: - logging.info(f"MASTER MIDI MESSAGE: {ev.hex()}") - # Webconf configured messages for Snapshot Control... - if ev == zynthian_gui_config.master_midi_program_change_up: - logging.debug("PROGRAM CHANGE UP!") - self.load_snapshot_by_prog(self.snapshot_program + 1) - elif ev == zynthian_gui_config.master_midi_program_change_down: - logging.debug("PROGRAM CHANGE DOWN!") - self.load_snapshot_by_prog(self.snapshot_program - 1) - elif ev == zynthian_gui_config.master_midi_bank_change_up: - logging.debug("BANK CHANGE UP!") - self.set_snapshot_midi_bank(self.snapshot_bank + 1) - elif ev == zynthian_gui_config.master_midi_bank_change_down: - logging.debug("BANK CHANGE DOWN!") - self.set_snapshot_midi_bank(self.snapshot_bank - 1) - # Program Change => Snapshot Load - elif evtype == 0xC: - pgm = ev[1] & 0x7F - logging.debug("PROGRAM CHANGE %d" % pgm) - self.start_busy("load_snapshot", "loading snapshot") - self.load_snapshot_by_prog(pgm) - self.end_busy("load_snapshot") - # Control Change... - elif evtype == 0xB: - ccnum = ev[1] & 0x7F - ccval = ev[2] & 0x7F - if ccnum == zynthian_gui_config.master_midi_bank_change_ccnum: - logging.debug(f"BANK CHANGE {ccval}") - self.set_snapshot_midi_bank(ccval) - elif ccnum == 120: - self.all_sounds_off() - elif ccnum == 123: - self.all_notes_off() - else: - if self.midi_learn_zctrl: - self.chain_manager.add_midi_learn(chan, ccnum, self.midi_learn_zctrl, izmip) - else: - 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) - vel = ev[2] & 0x7F - if note in zynthian_gui_config.master_midi_note_cuia: - cuia_str = zynthian_gui_config.master_midi_note_cuia[note] - parts = cuia_str.split(" ", 2) - cuia = parts[0].lower() - if len(parts) > 1: - params = self.parse_cuia_params(parts[1]) - else: - params = None - # Emulate Zynswitch Push/Release with Note On/Off - if cuia == "zynswitch" and len(params) == 1: - if evtype == 0x8 or vel == 0: - params.append('R') - else: - params.append('P') - self.cuia_queue.put_nowait((cuia, params)) - # Or normal CUIA - elif evtype == 0x9 and vel > 0: - self.cuia_queue.put_nowait((cuia, params)) - - # Control Change... - elif evtype == 0xB: - ccnum = ev[1] & 0x7F - ccval = ev[2] & 0x7F - # 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) - # Special CCs >= Channel Mode - elif ccnum == 120: - self.all_sounds_off_chan(chan) - elif ccnum == 123: - self.all_notes_off_chan(chan) - - # Program Change... - elif evtype == 0xC: - pgm = ev[1] & 0x7F - logging.info(f"MIDI PROGRAM CHANGE: CH#{chan}, PRG#{pgm}") - # MIDI learn SubSnapShot (ZS3) - if self.midi_learn_pc is not None: - # When using internal PC, ignore MIDI channel - if izmip == 0xFF: - self.save_zs3(f"*/{pgm}") - else: - self.save_zs3(f"{chan}/{pgm}") - send_signal = True - else: - # select SubSnapShot (ZS3) - if zynthian_gui_config.midi_prog_change_zs3: - # When using internal PC, ignore MIDI channel - if izmip == 0xFF: - send_signal = self.load_zs3(f"*/{pgm}") - else: - send_signal = self.load_zs3(f"{chan}/{pgm}") - # or select preset - else: - # 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) - if send_signal: - zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_PC, - izmip=izmip, chan=chan, num=pgm) - - # Note Off - elif evtype == 0x8: - # Handle external devices only - if izmip < self.get_max_num_midi_devs(): - zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_OFF, - izmip=izmip, chan=chan, note=ev[1] & 0x7f, vel=ev[2] & 0x7f) - - # Note On - elif evtype == 0x9: - # Handle external devices only - if izmip < self.get_max_num_midi_devs(): - zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_ON, - izmip=izmip, chan=chan, note=ev[1] & 0x7f, vel=ev[2] & 0x7f) - - # Flag MIDI event - self.status_midi = True - self.last_event_flag = True - - except Exception as err: - logging.exception(err) - - # --------------------------------------------------------------------------- - # Power Saving - # --------------------------------------------------------------------------- - - def power_save_check(self): - if zynthian_gui_config.power_save_secs <= 0: - return - if self.last_event_flag: - self.last_event_ts = monotonic() - self.last_event_flag = False - if self.power_save_mode: - self.set_power_save_mode(False) - elif not self.power_save_mode and (monotonic() - self.last_event_ts) > zynthian_gui_config.power_save_secs: - self.set_power_save_mode(True) - - def set_power_save_mode(self, psm=True): - self.power_save_mode = psm - if psm: - logging.info("Power Save Mode: ON") - self.ctrldev_manager.sleep_on() - check_output("powersave_control.sh on", shell=True) - else: - logging.info("Power Save Mode: OFF") - check_output("powersave_control.sh off", shell=True) - self.ctrldev_manager.sleep_off() - - def set_event_flag(self): - self.last_event_flag = True - - def reset_event_flag(self): - self.last_event_flag = False - - # ---------------------------------------------------------------------------- - # Snapshot Save & Load - # ---------------------------------------------------------------------------- - - def get_state(self): - """Get a dictionary describing the full state model""" - - self.save_zs3("zs3-0", "Last state") - self.purge_zs3() - state = { - 'schema_version': SNAPSHOT_SCHEMA_VERSION, - 'last_snapshot_fpath': self.last_snapshot_fpath, - 'midi_profile_state': self.get_midi_profile_state(), - 'chains': self.chain_manager.get_state(), - 'zs3': self.zs3, - 'last_zs3_id': self.last_zs3_id - } - - engine_states = {} - for eid, engine in self.chain_manager.zyngines.items(): - engine_state = engine.get_extended_config() - if engine_state: - engine_states[eid] = engine_state - if engine_states: - state["engine_config"] = engine_states - - # Add ALSA-Mixer setting - if zynthian_gui_config.snapshot_mixer_settings and self.alsa_mixer_processor: - state['alsa_mixer'] = self.alsa_mixer_processor.get_state() - - # Audio Recorder Armed - armed_state = [] - for midi_chan in range(self.zynmixer.MAX_NUM_CHANNELS): - if self.audio_recorder.is_armed(midi_chan): - armed_state.append(midi_chan) - if armed_state: - state['audio_recorder_armed'] = armed_state - - # Zynseq RIFF data - binary_riff_data = self.zynseq.get_riff_data() - b64_data = base64.b64encode(binary_riff_data) - state['zynseq_riff_b64'] = b64_data.decode('utf-8') - - return state - - def export_chain(self, fpath, chain_id): - """Save just a single chain to a snapshot file - - fpath: Full filename and path - chain_id: Chain to export - """ - self.start_busy("export chain", "exporting chain") - try: - # Get state - state = self.get_state() - procs = [] - for id in list(state["chains"]): - if id != chain_id: - del state["chains"][id] - else: - for slot in state["chains"][id]["slots"]: - for proc in slot.keys(): - procs.append(proc) - for zs3 in list(state["zs3"]): - if zs3 != "zs3-0": - del state["zs3"][zs3] - else: - if "processors" in state["zs3"][zs3]: - for proc in list(state["zs3"][zs3]["processors"]): - if proc not in procs: - del state["zs3"][zs3]["processors"][proc] - for id in list(state["zs3"][zs3]["chains"]): - if id != chain_id: - del state["zs3"][zs3]["chains"][id] - for key in ["global", "midi_capture", "active_chain"]: - try: - del state["zs3"][zs3][key] - 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] - except: - pass - - # JSON Encode - json = JSONEncoder().encode(state) - with open(fpath, "w") as fh: - logging.info(f"Saving snapshot {fpath} ...") - # logging.debug(f"Snapshot JSON Data =>\n{json}") - fh.write(json) - fh.flush() - os.fsync(fh.fileno()) - except Exception as e: - logging.exception(traceback.format_exc()) - logging.error("Can't export chain file '%s': %s" % (fpath, e)) - self.set_busy_error("ERROR saving snapshot", e) - sleep(2) - self.end_busy("export chain") - return False - - self.end_busy("export chain") - return True - - def save_snapshot(self, fpath, extra_data=None): - """Save current state model to file - - fpath : Full filename and path - extra_data : Dictionary to add to snapshot, e.g. UI specific config - Returns : True on success - """ - - self.start_busy("save snapshot", "saving snapshot") - try: - # Get state - state = self.get_state() - if isinstance(extra_data, dict): - state = {**state, **extra_data} - # JSON Encode - json = JSONEncoder().encode(state) - with open(fpath, "w") as fh: - logging.info(f"Saving snapshot {fpath} ...") - # logging.debug(f"Snapshot JSON Data =>\n{json}") - fh.write(json) - fh.flush() - os.fsync(fh.fileno()) - except Exception as e: - logging.exception(traceback.format_exc()) - logging.error("Can't save snapshot file '%s': %s" % (fpath, e)) - self.set_busy_error("ERROR saving snapshot", e) - sleep(2) - self.end_busy("save snapshot") - return False - - self.last_snapshot_fpath = fpath - self.end_busy("save snapshot") - return True - - def load_snapshot(self, fpath, load_chains=True, load_sequences=True, merge=False): - """Loads a snapshot from file - - fpath : Full path and filename of snapshot file - load_chains : True to load chains - load_sequences : True to load sequences into step sequencer - Returns : State dictionary or None on failure - """ - - self.start_busy("load snapshot", "loading snapshot") - try: - with open(fpath, "r") as fh: - json = fh.read() - logging.info(f"Loading snapshot '{fpath}' ...") - # logging.debug(f"Snapshot JSON Data =>\n{json}") - except Exception as e: - logging.error("Can't load snapshot '%s': %s" % (fpath, e)) - self.end_busy("load snapshot") - return None - - mute = self.zynmixer.get_mute(self.zynmixer.MAX_NUM_CHANNELS - 1) - try: - snapshot = JSONDecoder().decode(json) - self.set_busy_details("fixing legacy snapshot") - converter = zynthian_legacy_snapshot(self) - state = converter.convert_state(snapshot) - - if load_chains: - # Mute output to avoid unwanted noises - self.zynmixer.set_mute( - self.zynmixer.MAX_NUM_CHANNELS - 1, True) - - zynautoconnect.pause() - if "chains" in state: - if "engine_config" in state: - engine_config = state["engine_config"] - else: - engine_config = None - - if merge: - # Remove elements that are not to be merged - for key in ["last_snapshot_fpath", "last_zs3_id", "midi_profile_state", "audio_recorder_armed", "zynseq_riff_b64", "alsa_mixer", "zyngui"]: - try: - del state[key] - except: - pass - # Need to reassign chains and processor ids - chain_map = {} # Map of new chain id indexed by old id - proc_map = {} # Map of new processor id indexed by old id - mixer_map = {} # Map of new mixer chan idx indexed by old idx - # Don't import main chain - try: - del state["chains"]["0"] - except: - pass - new_proc_id = 0 - for id in self.chain_manager.processors: - if new_proc_id <= id: - new_proc_id = id + 1 - - mixer_chan = 0 - for chain_id, chain_state in state["chains"].items(): - # Fix mixer channel - mixer_chan = self.chain_manager.get_next_free_mixer_chan(mixer_chan) - mixer_map[int(chain_state["mixer_chan"])] = mixer_chan - chain_state["mixer_chan"] = mixer_chan - mixer_chan += 1 - new_chain_id = 1 - while new_chain_id in self.chain_manager.chains: - new_chain_id += 1 - chain_map[chain_id] = new_chain_id - for slot, procs in enumerate(chain_state["slots"]): - new_procs = {} - for old_proc_id, proc in procs.items(): - new_procs[new_proc_id] = proc - proc_map[old_proc_id] = new_proc_id - new_proc_id += 1 - chain_state["slots"][slot] = new_procs - # Fix zs3 - procs = {} - for proc_id, proc_config in state["zs3"]["zs3-0"]["processors"].items(): - if proc_id in proc_map: - procs[proc_map[proc_id]] = proc_config - state["zs3"]["zs3-0"]["processors"] = procs - chains = {} - for chain_id, chain_config in state["zs3"]["zs3-0"]["chains"].items(): - if chain_id == '0': - continue - chains[chain_map[chain_id]] = chain_config - - if "midi_cc" in chain_config: - for cc, map in chain_config["midi_cc"].items(): - for ctrl_cfg in map: - if str(ctrl_cfg[0]) in proc_map: - ctrl_cfg[0] = proc_map[str(ctrl_cfg[0])] - state["zs3"]["zs3-0"]["chains"] = chains - mixer_chans = {} - for old_mixer_chan, new_mixer_chan in mixer_map.items(): - try: - mixer_chans[f"chan_{new_mixer_chan:02d}"] = state["zs3"]["zs3-0"]["mixer"][f"chan_{old_mixer_chan:02d}"] - except: - pass - state["zs3"]["zs3-0"]["mixer"] = mixer_chans - # We don't want to merge MIDI binding to mixer - try: - del state["zs3"]["zs3-0"]["mixer"]["midi_learn"] - except: - pass - # We don't want to merge MIDI capture - try: - del state["zs3"]["zs3-0"]["midi_capture"] - except: - pass - - self.chain_manager.set_state(state['chains'], engine_config, merge) - self.chain_manager.stop_unused_engines() - zynautoconnect.resume() - - if "last_zs3_id" in state: - self.last_zs3_id = state["last_zs3_id"] - else: - self.last_zs3_id = None - zs3 = self.sanitize_zs3_from_json(state["zs3"]) - if not merge: - self.zs3 = zs3 - self.load_zs3(zs3["zs3-0"], autoconnect=False) - try: - mute |= self.zs3["zs3-0"]["mixer"]["chan_16"]["mute"] - except: - pass - - if "alsa_mixer" in state: - self.alsa_mixer_processor.set_state(state["alsa_mixer"]) - - if "audio_recorder_armed" in state: - for midi_chan in range(self.zynmixer.MAX_NUM_CHANNELS): - if midi_chan in state["audio_recorder_armed"]: - self.audio_recorder.arm(midi_chan) - else: - self.audio_recorder.unarm(midi_chan) - - if "midi_profile_state" in state: - self.set_midi_profile_state(state["midi_profile_state"]) - - if load_sequences and "zynseq_riff_b64" in state: - b64_bytes = state["zynseq_riff_b64"].encode("utf-8") - binary_riff_data = base64.decodebytes(b64_bytes) - self.zynseq.restore_riff_data(binary_riff_data) - - if fpath == self.last_snapshot_fpath and "last_state_fpath" in state: - self.last_snapshot_fpath = state["last_snapshot_fpath"] - else: - self.last_snapshot_fpath = fpath - - self.last_snapshot_count += 1 - try: - self.snapshot_program = int(basename(fpath[:3])) - except: - pass - - except Exception as e: - state = None - logging.exception("Invalid snapshot: %s" % e) - self.set_busy_error("ERROR: Invalid snapshot", e) - sleep(2) - - zynautoconnect.request_midi_connect() - zynautoconnect.request_audio_connect(True) - - # Restore mute state - self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, mute) - - # Signal snapshot loading - zynsigman.send_queued(zynsigman.S_STATE_MAN, self.SS_LOAD_SNAPSHOT) - - self.end_busy("load snapshot") - return state - - def set_snapshot_midi_bank(self, bank): - """Set the current snapshot bank - - bank: Snapshot bank (0..127) - """ - - for bank in glob(f"{self.snapshot_dir}/{bank:03d}*"): - if isdir(bank): - self.snapshot_bank = basename(bank) - return - - def load_snapshot_by_prog(self, program, bank=None): - """Loads a snapshot from its MIDI program and bank - - program : MIDI program number - bank : MIDI bank number (Default: Use last selected bank) - Returns : True on success - """ - - if bank is None: - bank = self.snapshot_bank - if bank is None: - return # Don't load snapshot if invalid bank selected - files = glob(f"{self.snapshot_dir}/{bank}/{program:03d}-*.zss") - if files: - self.load_snapshot(files[0]) - return True - return False - - def backup_snapshot(self, path): - """Make a backup copy of a snapshot file""" - - if isfile(path): - dpath = dirname(path) - fbase, fext = splitext(basename(path)) - ts_str = datetime.now().strftime("%Y%m%d%H%M%S") - budir = dpath + "/.backup" - if not isdir(budir): - os.mkdir(budir) - os.rename(path, "{}/{}.{}{}".format(budir, fbase, ts_str, fext)) - - def save_default_snapshot(self): - self.save_snapshot(self.default_snapshot_fpath) - - def load_default_snapshot(self): - if isfile(self.default_snapshot_fpath): - return self.load_snapshot(self.default_snapshot_fpath) - - def save_last_state_snapshot(self): - self.save_snapshot(self.last_state_snapshot_fpath) - - def load_last_state_snapshot(self): - if isfile(self.last_state_snapshot_fpath): - return self.load_snapshot(self.last_state_snapshot_fpath) - - def delete_last_state_snapshot(self): - try: - os.remove(self.last_state_snapshot_fpath) - except: - pass - - # ---------------------------------------------------------------------------- - # ZS3 management - # ---------------------------------------------------------------------------- - - def get_zs3_title(self, zs3_id=None): - """Get ZS3 title - - zs3_id : ZS3 ID (default: Use last loaded zs3) - Returns : Title as string - """ - - try: - if zs3_id is None: - zs3_id = self.last_zs3_id - return self.zs3[zs3_id]["title"] - except: - return zs3_id - - def set_zs3_title(self, zs3_id, title): - self.zs3[zs3_id]["title"] = title - - def toggle_zs3_chain_restore_flag(self, zs3_id, chain_id): - zs3_state = self.zs3[zs3_id] - if chain_id == "mixer": - tstate = zs3_state["mixer"] - else: - tstate = zs3_state["chains"][chain_id] - try: - tstate["restore"] = not tstate["restore"] - except: - tstate["restore"] = False - - def load_zs3(self, zs3_id, autoconnect=True): - """Restore a ZS3 - - zs3_id : ID of ZS3 to restore or zs3 dict - Returns : True on success - """ - - if isinstance(zs3_id, str): - # Try loading exact match - try: - zs3_state = self.zs3[zs3_id] - except: - # else ignore MIDI channel => try loading "program change" match - try: - zs3_id = f"*/{zs3_id.split('/')[1]}" - zs3_state = self.zs3[zs3_id] - except: - logging.info(f"Not found ZS3 matching '{zs3_id}'") - return False - else: - try: - zs3_state = zs3_id - zs3_id = self.last_zs3_id - if zs3_id is None: - zs3_id = "zs3-0" - except: - zs3_id = "zs3-0" - - restored_chains = [] - restored_cc_mapping = [] - mute_pause = False - if "chains" in zs3_state: - self.set_busy_details("restoring chains state") - for chain_id, chain_state in zs3_state["chains"].items(): - chain_id = int(chain_id) - - try: - restore_flag = chain_state["restore"] - except: - restore_flag = True - - if not restore_flag: - continue - - chain = self.chain_manager.get_chain(chain_id) - if chain: - restored_chains.append(chain_id) - else: - continue - - try: - if zs3_state["mixer"][f"chan_{chain.mixer_chan:02}"]["mute"]: - # Avoid subsequent config changes from being heard on muted chains - self.zynmixer.set_mute(chain.mixer_chan, 1) - mute_pause = True - except: - pass - - if "midi_chan" in chain_state: - if chain.midi_chan is not None and chain.midi_chan != chain_state['midi_chan']: - self.chain_manager.set_midi_chan(chain_id, chain_state['midi_chan']) - - if chain.zmop_index is not None: - if "note_low" in chain_state: - lib_zyncore.zmop_set_note_low(chain.zmop_index, chain_state["note_low"]) - else: - lib_zyncore.zmop_set_note_low(chain.zmop_index, 0) - if "note_high" in chain_state: - lib_zyncore.zmop_set_note_high(chain.zmop_index, chain_state["note_high"]) - else: - lib_zyncore.zmop_set_note_high(chain.zmop_index, 127) - if "transpose_octave" in chain_state: - lib_zyncore.zmop_set_transpose_octave(chain.zmop_index, chain_state["transpose_octave"]) - else: - lib_zyncore.zmop_set_transpose_octave(chain.zmop_index, 0) - 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) - if "midi_in" in chain_state: - chain.midi_in = chain_state["midi_in"] - if "midi_out" in chain_state: - chain.midi_out = chain_state["midi_out"] - if "midi_thru" in chain_state: - chain.midi_thru = chain_state["midi_thru"] - if "audio_in" in chain_state: - chain.audio_in = chain_state["audio_in"] - chain.audio_out = [] - if "audio_out" in chain_state: - for out in chain_state["audio_out"]: - if isinstance(out, list): - chain.audio_out.append(f"{self.chain_manager.processors[out[0]].jackname}:{out[1]}") - 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: - chain.audio_thru = chain_state["audio_thru"] - chain.rebuild_graph() - if "midi_cc" in chain_state: - for cc, cfg in chain_state["midi_cc"].items(): - for proc_id, symbol in cfg: - if proc_id in self.chain_manager.processors: - restored_cc_mapping.append((proc_id, int(cc), symbol)) - if mute_pause: - # Wait for soft mutes to apply before changing settings - sleep(self.jack_period) - - if "processors" in zs3_state: - for proc_id, proc_state in zs3_state["processors"].items(): - try: - processor = self.chain_manager.processors[int(proc_id)] - if processor.chain_id in restored_chains: - self.set_busy_details(f"restoring {processor.get_basepath()} state") - processor.set_state(proc_state) - except Exception as e: - logging.error(f"Failed to restore processor {proc_id} state => {e}") - - for cc_map in restored_cc_mapping: - processor = self.chain_manager.processors[cc_map[0]] - try: - zctrl = processor.controllers_dict[cc_map[2]] - self.chain_manager.add_midi_learn(processor.midi_chan, cc_map[1], zctrl) - except: - logging.warning(f"Failed to restore MIDI learning {cc_map[1]} => {cc_map[2]}") - - if "active_chain" in zs3_state: - self.chain_manager.set_active_chain_by_id(zs3_state["active_chain"]) - - if "mixer" in zs3_state: - try: - restore_flag = zs3_state["mixer"]["restore"] - except: - restore_flag = True - if restore_flag: - self.set_busy_details("restoring mixer state") - self.zynmixer.set_state(zs3_state["mixer"]) - - if "midi_capture" in zs3_state: - self.set_busy_details("restoring midi capture state") - self.set_midi_capture_state(zs3_state['midi_capture']) - - if "global" in zs3_state: - if "midi_transpose" in zs3_state["global"]: - lib_zyncore.set_global_transpose(int(zs3_state["global"]["midi_transpose"])) - if "zctrl_x" in zs3_state["global"]: - try: - processor = self.chain_manager.processors[zs3_state["global"]["zctrl_x"][0]] - self.zctrl_x = processor.controllers_dict[zs3_state["global"]["zctrl_x"][1]] - except: - self.zctrl_x = None - if "zctrl_y" in zs3_state["global"]: - try: - processor = self.chain_manager.processors[zs3_state["global"]["zctrl_y"][0]] - self.zctrl_y = processor.controllers_dict[zs3_state["global"]["zctrl_y"][1]] - except: - self.zctrl_y = None - if "zynaptik" in zs3_state["global"]: - try: - zynaptik_config = zs3_state["global"]["zynaptik"] - lib_zyncore.zynaptik_cvin_set_volts_octave(ctypes.c_float(zynaptik_config["cvin_volts_octave"])) - lib_zyncore.zynaptik_cvin_set_note0(zynaptik_config["cvin_note0"]) - lib_zyncore.zynaptik_cvout_set_volts_octave(ctypes.c_float(zynaptik_config["cvout_volts_octave"])) - lib_zyncore.zynaptik_cvout_set_note0(zynaptik_config["cvout_note0"]) - except: - pass - - if zs3_id != 'zs3-0': - self.last_zs3_id = zs3_id - #self.zs3['zs3-0'] = self.zs3[zs3_id].copy() - zynsigman.send(zynsigman.S_STATE_MAN, self.SS_LOAD_ZS3, zs3_id=zs3_id) - - if autoconnect: - zynautoconnect.request_midi_connect(True) - zynautoconnect.request_audio_connect(True) - return True - - def save_zs3(self, zs3_id=None, title=None): - """Store current state as ZS3 - - zs3_id : ID of zs3 to save / overwrite (Default: Create new id) - title : ZS3 title (Default: Create new title) - """ - - if zs3_id is None: - # Get next id and name - used_ids = [] - for zid in self.zs3: - if zid.startswith("zs3-"): - try: - used_ids.append(int(zid.split('-')[1])) - except: - pass - used_ids.sort() - # Get next free zs3 id - for index in range(1, len(used_ids) + 2): - if index not in used_ids: - zs3_id = f"zs3-{index}" - break - - if title is None: - title = self.midi_learn_pc - - if not title: - if zs3_id in self.zs3: - title = self.zs3[zs3_id]['title'] - else: - title = zs3_id.upper() - - # Initialise zs3 - self.zs3[zs3_id] = { - "title": title, - "active_chain": self.chain_manager.active_chain_id, - "global": {} - } - chain_states = {} - for chain_id, chain in self.chain_manager.chains.items(): - chain_state = { - "midi_chan": chain.midi_chan - } - if chain.is_midi(): - note_low = lib_zyncore.zmop_get_note_low(chain.zmop_index) - if note_low > 0: - chain_state["note_low"] = note_low - 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) - if transpose_octave: - chain_state["transpose_octave"] = transpose_octave - transpose_semitone = lib_zyncore.zmop_get_transpose_semitone(chain.zmop_index) - if transpose_semitone: - chain_state["transpose_semitone"] = transpose_semitone - if chain.midi_in: - chain_state["midi_in"] = chain.midi_in.copy() - if chain.midi_out: - chain_state["midi_out"] = chain.midi_out.copy() - if chain.midi_thru: - chain_state["midi_thru"] = chain.midi_thru - chain_state["audio_in"] = chain.audio_in.copy() - chain_state["audio_out"] = [] - for out in chain.audio_out: - if out in zynautoconnect.get_sidechain_portnames(): - client_name, port_name = out.split(":", 1) - for i, proc in self.chain_manager.processors.items(): - if proc.jackname == client_name: - out = [i, port_name] - break - chain_state["audio_out"].append(out) - if chain.audio_thru: - chain_state["audio_thru"] = chain.audio_thru - # Add chain MIDI mapping - for key, zctrls in self.chain_manager.chain_midi_cc_binding.items(): - if chain_id == (key >> 16) & 0xff: - cc = (key >> 8) & 0x7f - # TODO: Do not save default engine mapping - if "midi_cc" not in chain_state: - chain_state["midi_cc"] = {} - chain_state["midi_cc"][cc] = [] - for zctrl in zctrls: - chain_state["midi_cc"][cc].append([zctrl.processor.id, zctrl.symbol]) - if chain_state: - chain_states[chain_id] = chain_state - if chain_states: - self.zs3[zs3_id]["chains"] = chain_states - - # Add processors - processor_states = {} - for id, processor in self.chain_manager.processors.items(): - processor_state = { - "bank_info": processor.bank_info, - "preset_info": processor.preset_info, - "controllers": {} - } - # Add controllers - for symbol, zctrl in processor.controllers_dict.items(): - processor_state["controllers"][symbol] = zctrl.get_state() - processor_states[id] = processor_state - if processor_states: - self.zs3[zs3_id]["processors"] = processor_states - - # Add mixer state - mixer_state = self.zynmixer.get_state(False) - if mixer_state: - self.zs3[zs3_id]["mixer"] = mixer_state - - # Add MIDI capture state - mcstate = self.get_midi_capture_state() - if mcstate: - self.zs3[zs3_id]["midi_capture"] = mcstate - - # Add global parameters - self.zs3[zs3_id]["global"]["midi_transpose"] = lib_zyncore.get_global_transpose() - try: - processor_id = self.zctrl_x.processor.id - symbol = self.zctrl_x.symbol - self.zs3[zs3_id]["global"]["zctrl_x"] = [processor_id, symbol] - except: - pass - try: - processor_id = self.zctrl_y.processor.id - symbol = self.zctrl_y.symbol - self.zs3[zs3_id]["global"]["zctrl_y"] = [processor_id, symbol] - except: - pass - try: - if callable(lib_zyncore.init_zynaptik): - lib_zyncore.zynaptik_cvin_get_volts_octave.restype = ctypes.c_float - lib_zyncore.zynaptik_cvout_get_volts_octave.restype = ctypes.c_float - zynaptik_config = { - "cvin_volts_octave": lib_zyncore.zynaptik_cvin_get_volts_octave(), - "cvin_note0": lib_zyncore.zynaptik_cvin_get_note0(), - "cvout_volts_octave": lib_zyncore.zynaptik_cvout_get_volts_octave(), - "cvout_note0": lib_zyncore.zynaptik_cvout_get_note0() - } - self.zs3[zs3_id]["global"]["zynaptik"] = zynaptik_config - except: - pass - - if zs3_id != 'zs3-0': - self.last_zs3_id = zs3_id - # Jofemodo: this has not sense from my POV - #self.zs3['zs3-0'] = self.zs3[zs3_id].copy() - zynsigman.send(zynsigman.S_STATE_MAN, self.SS_SAVE_ZS3, zs3_id=zs3_id) - - def delete_zs3(self, zs3_id): - """Remove a ZS3 - - zs3_id : Index of ZS3 to remove - """ - try: - del (self.zs3[zs3_id]) - if self.last_zs3_id == zs3_id: - self.last_zs3_id = None - - except: - logging.info("Tried to remove non-existant ZS3") - - def reset_zs3(self): - """Remove all ZS3""" - - # ZS3 list (subsnapshots) - self.zs3 = {} - - def sanitize_zs3_from_json(self, zs3_state): - """Fix chain & processor ID keys in ZS3 data decoded from JSON""" - - # TODO: Temporal compatibility fix with older vangelis => To remove!! - if 'last_zs3' in zs3_state: - if self.last_zs3_id is None: - self.last_zs3_id = zs3_state['last_zs3'] - del zs3_state['last_zs3'] - - for zs3_key, state in zs3_state.items(): - if 'chains' in state: - fixed_chains = {} - for chain_id, chain_state in state['chains'].items(): - try: - chain_id = int(chain_id) - except: - logging.error( - f"Chain in ZS3 {zs3_key} has an invalid ID: {chain_id}") - continue - fixed_chains[chain_id] = chain_state - state['chains'] = fixed_chains - if 'processors' in state: - fixed_processors = {} - for processor_id, processor_state in state['processors'].items(): - try: - processor_id = int(processor_id) - except: - logging.error( - f"Processor in ZS3 {zs3_key} has an invalid ID: {processor_id}") - continue - fixed_processors[processor_id] = processor_state - state['processors'] = fixed_processors - - return zs3_state - - def purge_zs3(self): - """Remove non-existant chains and processors from ZS3 state""" - - for key, state in self.zs3.items(): - if state["active_chain"] not in self.chain_manager.chains: - state["active_chain"] = self.chain_manager.active_chain_id - if "processors" in state: - for processor_id in list(state["processors"]): - if int(processor_id) not in self.chain_manager.processors: - logging.debug( - f"Purging processor {processor_id} from ZS3 {key}") - del state["processors"][processor_id] - if "chains" in state: - for chain_id in list(state["chains"]): - if int(chain_id) not in self.chain_manager.chains: - logging.debug( - f"Purging chain {chain_id} from ZS3 {key}") - del state["chains"][chain_id] - - def get_last_zs3_index(self): - return list(self.zs3.keys()).index(self.last_zs3_id) - - def load_zs3_by_index(self, index): - try: - zs3_id = list(self.zs3.keys())[index] - except: - logging.warning(f"Can't find ZS3 with index {index}") - return - return self.load_zs3(zs3_id) - - def load_next_zs3(self): - try: - index = self.get_last_zs3_index() + 1 - except: - return False - return self.load_zs3_by_index(index) - - def load_prev_zs3(self): - try: - index = self.get_last_zs3_index() - 1 - except: - return False - return self.load_zs3_by_index(index) - - # ------------------------------------------------------------------ - # Jackd Info - # ------------------------------------------------------------------ - - def get_jackd_samplerate(self): - """Get the samplerate that jackd is running""" - - return zynautoconnect.get_jackd_samplerate() - - def get_jackd_blocksize(self): - """Get the block size used by jackd""" - - return zynautoconnect.get_jackd_blocksize() - - # ------------------------------------------------------------------ - # All Notes/Sounds Off => PANIC! - # ------------------------------------------------------------------ - - def all_sounds_off(self): - logging.info("All Sounds Off!") - for chan in range(16): - lib_zyncore.ui_send_ccontrol_change(chan, 120, 0) - - def all_notes_off(self): - logging.info("All Notes Off!") - self.zynseq.libseq.stop() - for chan in range(16): - lib_zyncore.ui_send_ccontrol_change(chan, 123, 0) - try: - lib_zyncore.zynaptik_all_gates_off() - except: - pass - zynsigman.send_queued(zynsigman.S_STATE_MAN, self.SS_ALL_NOTES_OFF, chan=None) - - def raw_all_notes_off(self): - logging.info("Raw All Notes Off!") - lib_zyncore.ui_send_all_notes_off() - - def all_sounds_off_chan(self, chan): - logging.info(f"All Sounds Off for channel {chan}!") - lib_zyncore.ui_send_ccontrol_change(chan, 120, 0) - - def all_notes_off_chan(self, chan): - logging.info(f"All Notes Off for channel {chan}!") - lib_zyncore.ui_send_ccontrol_change(chan, 123, 0) - zynsigman.send_queued(zynsigman.S_STATE_MAN, self.SS_ALL_NOTES_OFF, chan=chan) - - def raw_all_notes_off_chan(self, chan): - logging.info(f"Raw All Notes Off for channel {chan}!") - lib_zyncore.ui_send_all_notes_off_chan(chan) - - # ------------------------------------------------------------------ - # MPE initialization - # ------------------------------------------------------------------ - - def init_mpe_zones(self, lower_n_chans, upper_n_chans): - # Configure Lower Zone - if not isinstance(lower_n_chans, int) or lower_n_chans < 0 or lower_n_chans > 0xF: - logging.error( - f"Can't initialize MPE Lower Zone. Incorrect num of channels ({lower_n_chans})") - else: - lib_zyncore.ctrlfb_send_ccontrol_change(0x0, 0x79, 0x0) - lib_zyncore.ctrlfb_send_ccontrol_change(0x0, 0x64, 0x6) - lib_zyncore.ctrlfb_send_ccontrol_change(0x0, 0x65, 0x0) - lib_zyncore.ctrlfb_send_ccontrol_change(0x0, 0x06, lower_n_chans) - - # Configure Upper Zone - if not isinstance(upper_n_chans, int) or upper_n_chans < 0 or upper_n_chans > 0xF: - logging.error( - f"Can't initialize MPE Upper Zone. Incorrect num of channels ({upper_n_chans})") - else: - lib_zyncore.ctrlfb_send_ccontrol_change(0xF, 0x79, 0x0) - lib_zyncore.ctrlfb_send_ccontrol_change(0xF, 0x64, 0x6) - lib_zyncore.ctrlfb_send_ccontrol_change(0xF, 0x65, 0x0) - lib_zyncore.ctrlfb_send_ccontrol_change(0xF, 0x06, upper_n_chans) - - # ---------------------------------------------------------------------------- - # MIDI Capture State - # ---------------------------------------------------------------------------- - - def get_midi_capture_state(self): - """Get state related to midi input (capture): flags, chain routing, etc. - - Returns : dictionary with state - """ - mcstate = {} - ctrldev_state_drivers = self.ctrldev_manager.get_state_drivers() - for izmip in range(NUM_MIDI_DEVS_IN): - if zynautoconnect.devices_in[izmip] is None: - continue - try: - uid = zynautoconnect.devices_in[izmip].aliases[0] - except: - logging.error(f"No aliases for device connected to {izmip} => Skipping!") - continue - routed_chains = [] - for ch in range(MAX_NUM_ZMOPS): - if lib_zyncore.zmop_get_route_from(ch, izmip): - routed_chains.append(ch) - mcstate[uid] = { - "zmip_input_mode": bool(lib_zyncore.zmip_get_flag_active_chain(izmip)), - "disable_ctrldev": self.ctrldev_manager.get_disabled_driver(uid), - "ctrldev_driver": self.ctrldev_manager.get_driver_class_name(izmip), - "routed_chains": routed_chains - } - # Ctrldev driver state - if uid in ctrldev_state_drivers: - mcstate[uid]["ctrldev_state"] = ctrldev_state_drivers[uid] - # Aubio state - if uid == "AUBIO:in": - mcstate[uid]["audio_in"] = self.aubio_in - # Add global / absolute MIDI mapping - for key, zctrls in self.chain_manager.absolute_midi_cc_binding.items(): - if izmip == (key >> 24) & 0xff: - chan_cc = (key >> 8) & 0x7f7f - if "midi_cc" not in mcstate[uid]: - mcstate[uid]["midi_cc"] = {} - mcstate[uid]["midi_cc"][chan_cc] = [] - for zctrl in zctrls: - mcstate[uid]["midi_cc"][chan_cc].append([zctrl.processor.id, zctrl.symbol]) - - return mcstate - - def set_midi_capture_state(self, mcstate=None): - """Set midi input (capture) state: flags, chain routing, etc. - - mcstate : dictionary with state. None for reset state to defaults. - """ - if mcstate: - ctrldev_state_drivers = {} - for uid, state in mcstate.items(): - #logging.debug(f"MCSTATE {uid} => {state}") - izmip = zynautoconnect.get_midi_in_devid_by_uid(uid, zynthian_gui_config.midi_usb_by_port) - if izmip is None: - continue - try: - lib_zyncore.zmip_set_flag_active_chain(izmip, bool(state["zmip_input_mode"])) - except: - pass - try: - self.aubio_in = state["audio_in"] - except: - pass - zynautoconnect.update_midi_in_dev_mode(izmip) - try: - #TODO: Use ctrldev_driver=None to disable driver - if state["disable_ctrldev"]: - self.ctrldev_manager.unload_driver(izmip, True) - else: - self.ctrldev_manager.load_driver(izmip, state["ctrldev_driver"]) - except: - pass - try: - ctrldev_state_drivers[uid] = state["ctrldev_state"] - except: - pass - # Route chain zmops - try: - routed_chains = state["routed_chains"] - for ch in range(0, 16): - lib_zyncore.zmop_set_route_from(ch, izmip, ch in routed_chains) - except: - pass - - if "midi_cc" in state: - for chan_cc, cfg in state["midi_cc"].items(): - for proc_id, symbol in cfg: - try: - processor = self.chain_manager.processors[proc_id] - except: - continue - try: - zctrl = processor.controllers_dict[symbol] - except: - logging.warning(f"Can't MIDI learn '{symbol}'. Controller not found in processor {proc_id}.") - continue - chan = (chan_cc >> 8) & 0xff - cc = chan_cc & 0x7f - self.chain_manager.add_midi_learn(chan, cc, zctrl, izmip) - - self.ctrldev_manager.set_state_drivers(ctrldev_state_drivers) - - else: - zynautoconnect.reset_midi_in_dev_all() - - # ------------------------------------------------------------------ - # MIDI learning - # ------------------------------------------------------------------ - - def set_midi_learn(self, state): - """Enable / disable MIDI learn in MIDI router - - state : True to enable MIDI learn - """ - - lib_zyncore.set_midi_learning_mode(state) - self.midi_learn_state = state - - def enable_learn_cc(self, zctrl): - """Enable MIDI CC learning - - zctrl : zctrl to learn to - """ - - self.disable_learn_pc() - self.midi_learn_zctrl = zctrl - self.midi_learn_zctrl.midi_cc_mode_reset() - self.set_midi_learn(True) - - def disable_learn_cc(self): - """Disables MIDI CC learning""" - - self.midi_learn_zctrl = None - self.set_midi_learn(False) - - def get_midi_learn_zctrl(self): - try: - return self.midi_learn_zctrl - except: - return None - - def enable_learn_pc(self, zs3_name=""): - self.disable_learn_cc() - self.midi_learn_pc = zs3_name - self.set_midi_learn(True) - - def disable_learn_pc(self): - self.midi_learn_pc = None - self.set_midi_learn(False) - - # --------------------------------------------------------------------------- - # MIDI Router Init & Config - # --------------------------------------------------------------------------- - - def init_midi(self): - """Initialise MIDI configuration""" - try: - # Set 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) - # Set MIDI System Messages flag - 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) - except Exception as e: - logging.error(f"ERROR initializing MIDI : {e}") - - def reload_midi_config(self): - """Reload MIDI configuration from saved state""" - - zynconf.load_config() - midi_profile_fpath = zynconf.get_midi_config_fpath() - if midi_profile_fpath: - zynconf.load_config(True, midi_profile_fpath) - zynthian_gui_config.set_midi_config() - self.init_midi() - self.init_midi_services() - zynautoconnect.request_midi_connect() - - def init_midi_services(self): - """Start/Stop MIDI aux. services""" - - self.default_rtpmidi() - self.default_qmidinet() - self.default_touchosc() - self.default_bluetooth() - self.default_aubionotes() - - # ------------------------------------------------------------------- - # MIDI transport & clock settings - # ------------------------------------------------------------------- - - def get_transport_clock_source(self): - val = self.zynseq.libseq.getClockSource() - if val == 5: - return 3 - elif val == 2: - return 2 - elif self.zynseq.libseq.getMidiClockOutput(): - return 1 - else: - return 0 - - def set_transport_clock_source(self, val=None, save_config=False): - if val is None: - val = zynthian_gui_config.transport_clock_source - - if val == 2: - self.zynseq.libseq.setClockSource(2) - elif val == 3: - self.zynseq.libseq.setClockSource(1 | 4) - else: - self.zynseq.libseq.setClockSource(1) - - self.zynseq.libseq.setMidiClockOutput(val == 1) - - if val > 0: - lib_zyncore.set_midi_system_events(1) - else: - lib_zyncore.set_midi_system_events(zynthian_gui_config.midi_sys_enabled) - - # Save config - if save_config: - zynthian_gui_config.transport_clock_source = val - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_TRANSPORT_CLOCK_SOURCE": str(int(val)) - }) - - # ------------------------------------------------------------------- - # MIDI profile - # ------------------------------------------------------------------- - - def get_midi_profile_state(self): - """Get MIDI profile state as an ordered dictionary""" - - midi_profile_state = OrderedDict() - for key in os.environ.keys(): - if key.startswith("ZYNTHIAN_MIDI_"): - midi_profile_state[key[14:]] = os.environ[key] - midi_profile_state["port_names"] = zynautoconnect.get_port_friendly_names() - return midi_profile_state - - def set_midi_profile_state(self, state): - """Set MIDI profile from state - - state : MIDI profile state dictionary - """ - - if state is not None: - for key in state: - if key == "port_names": - zynautoconnect.set_midi_port_names(state[key]) - # Drop Master Channel config, as it's global - elif not key.startswith("MASTER_"): - os.environ["ZYNTHIAN_MIDI_" + key] = state[key] - zynthian_gui_config.set_midi_config() - self.init_midi() - self.init_midi_services() - self.set_transport_clock_source() - zynautoconnect.request_midi_connect() - return True - - def reset_midi_profile(self): - """Clear MIDI profiles""" - - self.reload_midi_config() - - # --------------------------------------------------------------------------- - # Global Audio Player - # --------------------------------------------------------------------------- - - def create_audio_player(self): - if not self.audio_player: - try: - 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( - f"Can't create global Audio Player instance => {e}\n{traceback.format_exc()}") - - def destroy_audio_player(self): - if self.audio_player: - self.audio_player.engine.remove_processor(self.audio_player) - self.audio_player = None - self.status_audio_player = False - - def start_audio_player(self): - if (self.audio_player.preset_name and os.path.exists(self.audio_player.preset_info[0])) or zynaudioplayer.get_filename(self.audio_player.handle): - zynaudioplayer.start_playback(self.audio_player.handle) - else: - self.audio_player.engine.load_latest(self.audio_player) - zynaudioplayer.start_playback(self.audio_player.handle) - - def stop_audio_player(self, reset_pos=False): - zynaudioplayer.stop_playback(self.audio_player.handle) - if reset_pos: - zynaudioplayer.set_position(self.audio_player.handle, 0.0) - - def toggle_audio_player(self): - """Toggle playback of global audio player""" - - if zynaudioplayer.get_playback_state(self.audio_player.handle): - self.stop_audio_player() - else: - self.start_audio_player() - - # --------------------------------------------------------------------------- - # Global MIDI Player - # --------------------------------------------------------------------------- - - def get_new_midi_record_fpath(self): - exdirs = zynthian_gui_config.get_external_storage_dirs(ex_data_dir) - if exdirs: - path = exdirs[0] - else: - path = capture_dir_sdc - filename = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") - if self.last_snapshot_fpath and len(self.last_snapshot_fpath) > 4: - filename += "_" + os.path.basename(self.last_snapshot_fpath[:-4]) - - filename = filename.replace( - "/", ";").replace(">", ";").replace(" ; ", ";") - # Append index to file to make unique - index = 1 - while "{}.{:03d}.mid".format(filename, index) in os.listdir(path): - index += 1 - return "{}/{}.{:03d}.mid".format(path, filename, index) - - 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) - return True - else: - return False - - def stop_midi_record(self): - result = False - if libsmf.isRecording(): - logging.info("STOPPING MIDI RECORDING ...") - libsmf.stopRecording() - - fpath = self.get_new_midi_record_fpath() - if zynsmf.save(self.smf_recorder, fpath): - self.sync = True - self.last_midi_file = fpath - result = True - - zynsigman.send(zynsigman.S_STATE_MAN, self.SS_MIDI_RECORDER_STATE, state=False) - - return result - - def toggle_midi_record(self): - if libsmf.isRecording(): - self.stop_midi_record() - else: - self.start_midi_record() - - def set_tempo(self, tempo): - self.zynseq.set_tempo(tempo) - zynaudioplayer.set_tempo(tempo) - - def start_midi_playback(self, fpath): - self.stop_midi_playback() - if fpath is None: - if self.last_midi_file: - fpath = self.last_midi_file - else: - # Get latest file - latest_mtime = 0 - for dir in [capture_dir_sdc] + zynthian_gui_config.get_external_storage_dirs(ex_data_dir): - for fn in glob(f"{dir}//*.mid"): - mtime = os.path.getmtime(fn) - if mtime > latest_mtime: - fpath = fn - latest_mtime = mtime - - if fpath is None: - logging.info("No track to play!") - return self.status_midi_player - - try: - zynsmf.load(self.smf_player, fpath) - tempo = libsmf.getTempo(self.smf_player, 0) - logging.info(f"STARTING MIDI PLAY '{fpath}' => {tempo}BPM") - self.set_tempo(tempo) - libsmf.startPlayback() - 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) - self.status_midi_player = False - self.last_midi_file = fpath - # self.zynseq.libseq.transportLocate(0) - except Exception as e: - logging.error(f"ERROR STARTING MIDI PLAY: {e}") - return False - return self.status_midi_player - - 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) - return self.status_midi_player - - def toggle_midi_playback(self, fname=None): - if libsmf.getPlayState() == zynsmf.PLAY_STATE_STOPPED: - return self.start_midi_playback(fname) - else: - return self.stop_midi_playback() - - # --------------------------------------------------------------------------- - # Core Network Services - # --------------------------------------------------------------------------- - - def start_vncserver(self, save_config=True): - # Start VNC for Zynthian-UI - self.start_busy("start_vncserver", "starting VNC") - - if not zynconf.is_service_active("vncserver0"): - try: - logging.info("STARTING VNC-UI SERVICE") - self.set_busy_details("starting VNC-UI service") - check_output("systemctl start novnc0", shell=True) - zynthian_gui_config.vncserver_enabled = 1 - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STARTING VNC-UI", e) - sleep(2.0) - - # Start VNC for Engine's native GUIs - if not zynconf.is_service_active("vncserver1"): - # Save state and stop engines - if self.chain_manager.get_chain_count() > 0: - self.save_last_state_snapshot() - restore_state = True - else: - restore_state = False - # Start VNC for Engines - try: - logging.info("STARTING VNC-ENGINES SERVICE") - self.set_busy_details("starting VNC-ENGINES service") - check_output("systemctl start novnc1", shell=True) - zynthian_gui_config.vncserver_enabled = 1 - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STARTING VNC-ENGINES", e) - sleep(2.0) - # Restore state - if restore_state: - self.load_last_state_snapshot() - - # Update Config - if save_config: - zynconf.save_config({ - "ZYNTHIAN_VNCSERVER_ENABLED": str(zynthian_gui_config.vncserver_enabled) - }) - - self.end_busy("start_vncserver") - - def stop_vncserver(self, save_config=True): - self.start_busy("stop_vncserver", "stopping VNC") - - # Stop VNC for Zynthian-UI - if zynconf.is_service_active("vncserver0"): - try: - logging.info("STOPPING VNC-UI SERVICE") - self.set_busy_details("stopping VNC-UI service") - check_output("systemctl stop vncserver0", shell=True) - zynthian_gui_config.vncserver_enabled = 0 - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STOPPING VNC-UI", e) - sleep(2.0) - - # Start VNC for Engine's native GUIs - if zynconf.is_service_active("vncserver1"): - # Save state and stop engines - if len(self.chain_manager.processors) > 0: - self.save_last_state_snapshot() - restore_state = True - else: - restore_state = False - # Stop VNC for engiens - try: - logging.info("STOPPING VNC-ENGINES SERVICE") - self.set_busy_details("stopping VNC-ENGINES service") - check_output("systemctl stop vncserver1", shell=True) - zynthian_gui_config.vncserver_enabled = 0 - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STOPPING VNC-ENGINES", e) - sleep(2.0) - # Restore state - if restore_state: - self.load_last_state_snapshot() - - # Update Config - if save_config: - zynconf.save_config({ - "ZYNTHIAN_VNCSERVER_ENABLED": str(zynthian_gui_config.vncserver_enabled) - }) - - self.end_busy("stop_vncserver") - - # Start/Stop VNC Server depending on configuration - def default_vncserver(self): - if zynthian_gui_config.vncserver_enabled: - self.start_vncserver(False) - else: - self.stop_vncserver(False) - - # --------------------------------------------------------------------------- - # MIDI Network Services - # --------------------------------------------------------------------------- - - # Start/Stop NetUMP-MIDI-2.0 depending on configuration - def default_netump(self): - if zynthian_gui_config.midi_netump_enabled: - self.start_netump(False) - else: - self.stop_netump(False) - - def start_netump(self, save_config=True, wait=0): - service = "jacknetumpd" - if zynconf.is_service_active(service): - zynthian_gui_config.midi_netump_enabled = 1 - return - self.start_busy("start_netump", "starting NetUMP MIDI 2.0") - logging.info("STARTING NetUMP MIDI 2.0") - try: - check_output(f"systemctl start {service}", shell=True) - zynthian_gui_config.midi_netump_enabled = 1 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_NETUMP_ENABLED": str(zynthian_gui_config.midi_netump_enabled) - }) - # Call autoconnect after a little time - sleep(wait) - zynautoconnect.request_midi_connect(True) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STARTING NetUMP MIDI 2.0", e) - sleep(2.0) - - self.end_busy("start_netump") - - def stop_netump(self, save_config=True, wait=0): - service = "jacknetumpd" - if not zynconf.is_service_active(service): - zynthian_gui_config.midi_netump_enabled = 0 - return - self.start_busy("stop_netump", "stopping NetUMP MIDI 2.0") - logging.info("STOPPING NetUMP MIDI 2.0") - try: - check_output(f"systemctl stop {service}", shell=True) - zynthian_gui_config.midi_netump_enabled = 0 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_NETUMP_ENABLED": str(zynthian_gui_config.midi_netump_enabled) - }) - sleep(wait) - - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STOPPING NetUMP MIDI 2.0", e) - sleep(2.0) - - self.end_busy("stop_netump") - - # Start/Stop RTP-MIDI depending on configuration - def default_rtpmidi(self): - if zynthian_gui_config.midi_rtpmidi_enabled: - self.start_rtpmidi(False) - else: - self.stop_rtpmidi(False) - - def start_rtpmidi(self, save_config=True, wait=0): - service = "jackrtpmidid" - if zynconf.is_service_active(service): - zynthian_gui_config.midi_rtpmidi_enabled = 1 - return - self.start_busy("start_rtpmidi", "starting RTP-MIDI") - logging.info("STARTING RTP-MIDI") - try: - check_output(f"systemctl start {service}", shell=True) - zynthian_gui_config.midi_rtpmidi_enabled = 1 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_RTPMIDI_ENABLED": str(zynthian_gui_config.midi_rtpmidi_enabled) - }) - # Call autoconnect after a little time - sleep(wait) - zynautoconnect.request_midi_connect(True) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STARTING RTP-MIDI", e) - sleep(2.0) - - self.end_busy("start_rtpmidi") - - def stop_rtpmidi(self, save_config=True, wait=0): - service = "jackrtpmidid" - if not zynconf.is_service_active(service): - zynthian_gui_config.midi_rtpmidi_enabled = 0 - return - self.start_busy("stop_rtpmidi", "stopping RTP-MIDI") - logging.info("STOPPING RTP-MIDI") - try: - check_output(f"systemctl stop {service}", shell=True) - zynthian_gui_config.midi_rtpmidi_enabled = 0 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_RTPMIDI_ENABLED": str(zynthian_gui_config.midi_rtpmidi_enabled) - }) - sleep(wait) - - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STOPPING RTP-MIDI", e) - sleep(2.0) - - self.end_busy("stop_rtpmidi") - - def start_qmidinet(self, save_config=True, wait=0): - service = "qmidinet" - if zynconf.is_service_active(service): - zynthian_gui_config.midi_network_enabled = 1 - return - self.start_busy("start_qmidinet", "starting QMidiNet") - logging.info("STARTING QMidiNet") - try: - check_output(f"systemctl start {service}", shell=True) - zynthian_gui_config.midi_network_enabled = 1 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_NETWORK_ENABLED": str(zynthian_gui_config.midi_network_enabled) - }) - # Call autoconnect after a little time - sleep(wait) - zynautoconnect.request_midi_connect(True) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STARTING QMidiNet", e) - sleep(2.0) - - self.end_busy("start_qmidinet") - - def stop_qmidinet(self, save_config=True, wait=0): - service = "qmidinet" - if not zynconf.is_service_active(service): - zynthian_gui_config.midi_network_enabled = 0 - return - self.start_busy("stop_qmidinet", "stopping QMidiNet") - logging.info("STOPPING QMidiNet") - try: - check_output(f"systemctl stop {service}", shell=True) - zynthian_gui_config.midi_network_enabled = 0 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_NETWORK_ENABLED": str(zynthian_gui_config.midi_network_enabled) - }) - sleep(wait) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STOPPING QMidiNet", e) - sleep(2.0) - - self.end_busy("stop_qmidinet") - - # Start/Stop QMidiNet depending on configuration - def default_qmidinet(self): - if zynthian_gui_config.midi_network_enabled: - self.start_qmidinet(False) - else: - self.stop_qmidinet(False) - - def start_touchosc2midi(self, save_config=True, wait=0): - service = "touchosc2midi" - if zynconf.is_service_active(service): - zynthian_gui_config.midi_touchosc_enabled = 1 - return - self.start_busy("start_touchosc2midi", "starting Touch-OSC") - logging.info("STARTING touchosc2midi") - try: - check_output(f"systemctl start {service}", shell=True) - zynthian_gui_config.midi_touchosc_enabled = 1 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_TOUCHOSC_ENABLED": str(zynthian_gui_config.midi_touchosc_enabled) - }) - # Call autoconnect after a little time - zynautoconnect.request_midi_connect(True) - sleep(wait) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STARTING Touch-OSC", e) - sleep(2.0) - - self.end_busy("start_touchosc2midi") - - def stop_touchosc2midi(self, save_config=True, wait=0): - service = "touchosc2midi" - if not zynconf.is_service_active(service): - zynthian_gui_config.midi_touchosc_enabled = 0 - return - self.start_busy("stop_touchosc2midi", "stopping Touch-OSC") - logging.info("STOPPING touchosc2midi") - try: - check_output(f"systemctl stop {service}", shell=True) - zynthian_gui_config.midi_touchosc_enabled = 0 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_TOUCHOSC_ENABLED": str(zynthian_gui_config.midi_touchosc_enabled) - }) - sleep(wait) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STOPPING Touch-OSC", e) - sleep(2.0) - - self.end_busy("stop_touchosc2midi") - - # Start/Stop TouchOSC depending on configuration - def default_touchosc(self): - if zynthian_gui_config.midi_touchosc_enabled: - self.start_touchosc2midi(False) - else: - self.stop_touchosc2midi(False) - - def select_bluetooth_controller(self, controller): - if controller.count(":") != 5: - return - proc = Popen('bluetoothctl', stdin=PIPE, stdout=PIPE, - stderr=PIPE, encoding='utf-8') - for addr in check_output("bluetoothctl list", shell=True, timeout=1, encoding="utf-8").split(): - if addr.count(":") == 5: - proc.stdin.write(f"select {addr}\n") - if controller == addr: - proc.stdin.write(f"power on\n") - else: - proc.stdin.write(f"power off\n") - proc.stdin.flush() - proc.stdin.write(f"exit\n") - proc.stdin.flush() - zynthian_gui_config.ble_controller = controller - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_BLE_CONTROLLER": zynthian_gui_config.ble_controller - }) - - def start_bluetooth(self, save_config=True, wait=0): - service = "bluetooth" - if zynconf.is_service_active(service): - zynthian_gui_config.bluetooth_enabled = 1 - self.select_bluetooth_controller( - zynthian_gui_config.ble_controller) - return - self.start_busy("start_bluetooth", "starting Bluetooth") - logging.info("STARTING Bluetooth") - try: - check_output(f"systemctl start {service}", shell=True, timeout=2) - sleep(wait) - zynthian_gui_config.bluetooth_enabled = 1 - self.select_bluetooth_controller( - zynthian_gui_config.ble_controller) - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_BLE_ENABLED": str(zynthian_gui_config.bluetooth_enabled) - }) - # Call autoconnect after a little time - zynautoconnect.request_midi_connect(True) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STARTING Bluetooth", e) - sleep(2.0) - - self.end_busy("start_bluetooth") - - def stop_bluetooth(self, save_config=True, wait=0): - service = "bluetooth" - if not zynconf.is_service_active(service): - zynthian_gui_config.bluetooth_enabled = 0 - return - self.start_busy("stop_bluetooth", "stopping Bluetooth") - logging.info("STOPPING bluetooth") - try: - check_output(f"systemctl stop {service}", shell=True, timeout=1) - sleep(wait) - zynthian_gui_config.bluetooth_enabled = 0 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_BLE_ENABLED": str(zynthian_gui_config.bluetooth_enabled) - }) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STOPPING Bluetooth", e) - sleep(2.0) - - self.end_busy("stop_bluetooth") - - # Start/Stop Bluetooth depending on configuration - def default_bluetooth(self): - if zynthian_gui_config.bluetooth_enabled: - self.start_bluetooth(False) - else: - self.stop_bluetooth(False) - - def start_aubionotes(self, save_config=True, wait=0): - service = "aubionotes" - if zynconf.is_service_active(service): - zynthian_gui_config.midi_aubionotes_enabled = 1 - return - self.start_busy("start_aubionotes", "starting AubioNotes") - logging.info("STARTING aubionotes") - try: - check_output(f"systemctl start {service}", shell=True) - zynthian_gui_config.midi_aubionotes_enabled = 1 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_AUBIONOTES_ENABLED": str(zynthian_gui_config.midi_aubionotes_enabled) - }) - # Call autoconnect after a little time - sleep(wait) - zynautoconnect.request_midi_connect(True) - zynautoconnect.request_audio_connect() - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STARTING AubioNotes", e) - sleep(2.0) - - self.end_busy("start_aubionotes") - - def stop_aubionotes(self, save_config=True, wait=0): - service = "aubionotes" - if not zynconf.is_service_active(service): - zynthian_gui_config.midi_aubionotes_enabled = 0 - return - - self.start_busy("stop_aubionotes", "stopping AubioNotes") - logging.info("STOPPING aubionotes") - try: - check_output(f"systemctl stop {service}", shell=True) - zynthian_gui_config.midi_aubionotes_enabled = 0 - # Update MIDI profile - if save_config: - zynconf.update_midi_profile({ - "ZYNTHIAN_MIDI_AUBIONOTES_ENABLED": str(zynthian_gui_config.midi_aubionotes_enabled) - }) - sleep(wait) - except Exception as e: - logging.error(e) - self.set_busy_error("ERROR STOPPING AubioNotes", e) - sleep(2.0) - - self.end_busy("stop_aubionotes") - - # Start/Stop AubioNotes depending on configuration - def default_aubionotes(self): - if zynthian_gui_config.midi_aubionotes_enabled: - self.start_aubionotes(False) - else: - self.stop_aubionotes(False) - - # --------------------------------------------------------------------------- - # Zynthian Config Info - # --------------------------------------------------------------------------- - - def get_zynthian_config(self, varname): - try: - return eval("zynthian_gui_config.{}".format(varname)) - except: - return None - - def allow_rbpi_headphones(self): - try: - return self.alsa_mixer_processor.engine.allow_rbpi_headphones() - except: - return False - - def check_for_updates(self): - if self.checking_for_updates: - return - self.checking_for_updates = True - - def update_thread(): - logging.debug("************ CHECKING FOR UPDATES ... ************") - try: - repos = ["zynthian-ui", "zynthian-sys", "zynthian-webconf", "zynthian-data", "zyncoder"] - # If attached to last stable => Detect if new tag relase available - if os.environ.get('ZYNTHIAN_STABLE_TAG', "") == "last": - stable_branch = os.environ.get('ZYNTHIAN_STABLE_BRANCH', "oram") - for repo in repos: - 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") - last_stag = stags[-1].strip() - #logging.debug(f"STABLE TAG RELEASES => {stags}") - if branch != last_stag: - #logging.info(f"For reposiroty '{repo}', current branch ({branch}) != last tag release ({last_stag})!") - self.update_available = True - break - # else => Check for commits to pull - else: - 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] - #logging.debug(f"*********** BRANCH {branch} => local hash {local_hash}, remote hash {remote_hash} ****************") - if local_hash != remote_hash: - self.update_available = True - break - except Exception as e: - logging.warning(e) - self.checking_for_updates = False - - def get_repo_branch(path): - res = check_output(["git", "-C", path, "rev-parse", "--abbrev-ref", "HEAD"], - encoding="utf-8", stderr=STDOUT).strip() - parts = res.split("/", 1) - if len(parts) > 1 and parts[0] == 'heads': - return parts[1] - else: - return res - - thread = Thread(target=update_thread, args=()) - thread.name = "Check update" - thread.daemon = True # thread dies with the program - thread.start() - - # --------------------------------------------------------------------------- From de5fe7f8c30ce2b96edc8b160ae1b57659ffeaf4 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 17:56:59 +0200 Subject: [PATCH 52/57] Delete zynthian.sh Just my own --- zynthian.sh | 303 ---------------------------------------------------- 1 file changed, 303 deletions(-) delete mode 100755 zynthian.sh diff --git a/zynthian.sh b/zynthian.sh deleted file mode 100755 index 9abf2d166..000000000 --- a/zynthian.sh +++ /dev/null @@ -1,303 +0,0 @@ -#!/bin/bash -#****************************************************************************** -# ZYNTHIAN PROJECT: Zynthian Start Script -# -# Start all services needed by zynthian and the zynthian UI -# -# Copyright (C) 2015-2023 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. -# -#****************************************************************************** - - -#export ZYNTHIAN_LOG_LEVEL=10 # 10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICAL -#export ZYNTHIAN_RAISE_EXCEPTIONS=0 - -#------------------------------------------------------------------------------ -# Some Functions -#------------------------------------------------------------------------------ - -function load_config_env() { - source "$ZYNTHIAN_SYS_DIR/scripts/zynthian_envars_extended.sh" - - if [ -z "$ZYNTHIAN_SCRIPT_MIDI_PROFILE" ]; then - source "$ZYNTHIAN_MY_DATA_DIR/midi-profiles/default.sh" - else - source "$ZYNTHIAN_SCRIPT_MIDI_PROFILE" - fi - - if [ -f "$ZYNTHIAN_CONFIG_DIR/zynthian_custom_config.sh" ]; then - source "$ZYNTHIAN_CONFIG_DIR/zynthian_custom_config.sh" - fi -} - -function raw_splash_zynthian() { - if [ -c $FRAMEBUFFER ]; then - cat $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_boot.raw > $FRAMEBUFFER - fi -} - - -function raw_splash_zynthian_error() { - if [ -c $FRAMEBUFFER ]; then - cat $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_error.raw > $FRAMEBUFFER - fi -} - - -function splash_zynthian() { - xloadimage -fullscreen -onroot $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_boot.jpg -} - - -function splash_zynthian_message() { - zynthian_message=$1 - - img_fpath=$2 - [ "$img_fpath" ] || img_fpath="$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_boot.jpg" - - # Generate a splash image with the message... - img_w=$(identify -format '%w' $img_fpath) - img_h=$(identify -format '%h' $img_fpath) - if [[ "${#zynthian_message}" > "40" ]]; then - font_size=$(expr $img_w / 36) - else - font_size=$(expr $img_w / 28) - fi - strlen=$(expr ${#zynthian_message} \* $font_size / 2) - pos_x=$(expr $img_w / 2 - $strlen / 2) - pos_y=$(expr $img_h \* 10 / 100) - [[ "$pos_x" > "0" ]] || pos_x=5 - convert -strip -family \"$ZYNTHIAN_UI_FONT_FAMILY\" -pointsize $font_size -fill white -draw "text $pos_x,$pos_y \"$zynthian_message\"" $img_fpath $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg - - # Display error image - xloadimage -fullscreen -onroot $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg -} - - -function splash_zynthian_error() { - # Generate an error splash image... - splash_zynthian_message "$1" "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_error.jpg" -} - - -function splash_zynthian_error_exit_ip() { - # Grab exit code if set - zynthian_error=$1 - [ "$zynthian_error" ] || zynthian_error="???" - - case $zynthian_error in - 1) - message="Software" - ;; - 139) - message="SegFault" - ;; - 200) - message="Zyncore" - ;; - 201) - message="Control I/O" - ;; - 202) - message="Audio/MIDI" - ;; - 203) - message="CV/Gate" - ;; - *) - message="ErrCode $zynthian_error" - ;; - esac - - # Get the IP - #zynthian_ip=`ip route get 1 | awk '{print $NF;exit}'` - zynthian_ip=$(hostname -I | cut -d " " -f1) - - # Format the message - zynthian_message="IP:$zynthian_ip $message" - - # Generate an error splash image with the IP & exit code... - splash_zynthian_message "$zynthian_message" "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_error.jpg" -} - -function splash_zynthian_last_message() { - if [ -f "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg" ]; then - xloadimage -fullscreen -onroot $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg - else - xloadimage -fullscreen -onroot $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_boot.jpg - fi -} - -function clean_zynthian_last_message() { - if [ -f "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg" ]; then - rm -f $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg - fi -} - -function start_wifi_ap() { - readarray -t connected_devices <<< $(nmcli --terse c show | cut -d : -f 4 | tr -s '\n' | tr -s 'lo\n') - if [[ "${#connected_devices[*]}" < "2" ]]; then - nmcli radio wifi on - nmcli con up "zynthian-ap" - fi -} - -powersave_control.sh off -load_config_env - -#------------------------------------------------------------------------------ -# Test splash screen generator -#------------------------------------------------------------------------------ - -#splash_zynthian_message "Testing Splash Screen Generator..." -#sleep 10 -#exit - -if [[ "$(systemctl is-enabled first_boot)" == "enabled" ]]; then - is_first_boot=1 -else - is_first_boot=0 -fi - -#------------------------------------------------------------------------------ -# If needed, generate splash screen images -#------------------------------------------------------------------------------ - -if [[ ! -f "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_error.jpg" ]]; then - if [[ "$is_first_boot" == "1" ]]; then - $ZYNTHIAN_SYS_DIR/sbin/generate_fb_splash.sh >> /root/first_boot.log - else - $ZYNTHIAN_SYS_DIR/sbin/generate_fb_splash.sh - fi -fi - -#------------------------------------------------------------------------------ -# Run Hardware Test -#------------------------------------------------------------------------------ - -if [[ -n "$ZYNTHIAN_HW_TEST" ]]; then - echo "Running HW test: $ZYNTHIAN_HW_TEST" - result=$($ZYNTHIAN_SYS_DIR/sbin/zynthian_hw_test.py $ZYNTHIAN_HW_TEST | tail -1) - res=${result%:*} - message=${result#*:} - - if [[ "$res" == "OK" ]]; then - splash_zynthian_message "$result" - else - splash_zynthian_error "$message" - fi - - run_control_test="0" - if [[ "$ZYNTHIAN_UI_CONTROL_TEST_ENABLED" == "1" ]]; then - control_board_name="V5_CONTROL" - echo "Testing control board '$control_board_name'..." - result=$($ZYNTHIAN_SYS_DIR/sbin/zynthian_hw_test.py $control_board_name | tail -1) - res=${result%:*} - #echo "RESULT => $result => $res" - if [[ "$res" == "OK" ]]; then - run_control_test="1" - fi - fi - - echo "Running HW control test => $run_control_test" - if [[ "$run_control_test" == "0" ]]; then - sleep 3600 - exit - fi -fi - -#------------------------------------------------------------------------------ -# Build zyncore if needed -#------------------------------------------------------------------------------ - -if [[ ! -f "$ZYNTHIAN_DIR/zyncoder/build/libzyncore.so" ]]; then - splash_zynthian_message "Building zyncore. Please wait..." - $ZYNTHIAN_DIR/zyncoder/build.sh -fi - -#------------------------------------------------------------------------------ -# Detect first boot -#------------------------------------------------------------------------------ - -if [[ "$is_first_boot" == "1" ]]; then - echo "Running first boot..." - splash_zynthian_message "Configuring your zynthian. Time to relax before the waves..." - sleep 1800 - splash_zynthian_error "It takes too long! Bad sdcard/image, poor power supply..." - sleep 3600000 - exit -fi - -#------------------------------------------------------------------------------ -# Run Zynthian-UI -#------------------------------------------------------------------------------ - -splash_zynthian - -while true; do - -# brumby touchscreen mouse function not working, I have to reload -#modprobe -r hid_multitouch # Beispieltreiber (ändern!) -# sudo modprobe hid_multitouch -nohup bash -c "sleep 10 && modprobe -r hid_multitouch" >/dev/null 2>&1 & - - - clean_zynthian_last_message - - # Start Zynthian GUI & Synth Engine - cd $ZYNTHIAN_UI_DIR - ./zynthian_main.py - status=$? - - echo -e "\n*******************\nEXIT STATUS => $status\n*******************\n" - - # Proccess output status - case $status in - 0) - #splash_zynthian_message "Powering Off..." - splash_zynthian_last_message - poweroff - #backlight_control.sh off - break - ;; - 100) - #splash_zynthian_message "Rebooting..." - splash_zynthian_last_message - reboot - break - ;; - 101) - #splash_zynthian_message "Exiting..." - splash_zynthian_last_message - break - ;; - 102) - #splash_zynthian_message "Restarting UI..." - splash_zynthian_last_message - load_config_env - sleep 10 - ;; - *) - splash_zynthian_error_exit_ip $status - load_config_env - start_wifi_ap - sleep 10 - ;; - esac -done - -#------------------------------------------------------------------------------ From 36f82fe07c01114e6f02bfa141e917549f02ad66 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 18:35:30 +0200 Subject: [PATCH 53/57] must be removed for pull request. is at wrong place --- launch.json | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 launch.json diff --git a/launch.json b/launch.json deleted file mode 100644 index c1bdcd449..000000000 --- a/launch.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - // Peter development nach: https://wiki.zynthian.org/index.php/Contributing_to_Zynthian_Development - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Zynthian", - "type": "debugpy", - "request": "launch", - "program": "zynthian_main.py", - "python": "/zynthian/venv/bin/python3", - "console": "integratedTerminal", - "justMyCode": true, - "env": { - "DISPLAY": ":0" - } - }, - { - "name": "Zynthian Debug", - "type": "debugpy", - "request": "launch", - "program": "zynthian_main.py", - "python": "/zynthian/venv/bin/python3", - "console": "integratedTerminal", - "justMyCode": true, - "subProcess": true, - "env": { - "DISPLAY": ":0", - "ZYNTHIAN_LOG_LEVEL": "10" - } - } - ] -} - From e08f793bcfb1e543601c0f9538d43247003e6c5a Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 18:39:21 +0200 Subject: [PATCH 54/57] reverted to origin oram --- zynthian.sh | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100755 zynthian.sh diff --git a/zynthian.sh b/zynthian.sh new file mode 100755 index 000000000..e5f71f6c5 --- /dev/null +++ b/zynthian.sh @@ -0,0 +1,296 @@ +#!/bin/bash +#****************************************************************************** +# ZYNTHIAN PROJECT: Zynthian Start Script +# +# Start all services needed by zynthian and the zynthian UI +# +# Copyright (C) 2015-2023 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. +# +#****************************************************************************** + + +#export ZYNTHIAN_LOG_LEVEL=10 # 10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICAL +#export ZYNTHIAN_RAISE_EXCEPTIONS=0 + +#------------------------------------------------------------------------------ +# Some Functions +#------------------------------------------------------------------------------ + +function load_config_env() { + source "$ZYNTHIAN_SYS_DIR/scripts/zynthian_envars_extended.sh" + + if [ -z "$ZYNTHIAN_SCRIPT_MIDI_PROFILE" ]; then + source "$ZYNTHIAN_MY_DATA_DIR/midi-profiles/default.sh" + else + source "$ZYNTHIAN_SCRIPT_MIDI_PROFILE" + fi + + if [ -f "$ZYNTHIAN_CONFIG_DIR/zynthian_custom_config.sh" ]; then + source "$ZYNTHIAN_CONFIG_DIR/zynthian_custom_config.sh" + fi +} + +function raw_splash_zynthian() { + if [ -c $FRAMEBUFFER ]; then + cat $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_boot.raw > $FRAMEBUFFER + fi +} + + +function raw_splash_zynthian_error() { + if [ -c $FRAMEBUFFER ]; then + cat $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_error.raw > $FRAMEBUFFER + fi +} + + +function splash_zynthian() { + xloadimage -fullscreen -onroot $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_boot.jpg +} + + +function splash_zynthian_message() { + zynthian_message=$1 + + img_fpath=$2 + [ "$img_fpath" ] || img_fpath="$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_boot.jpg" + + # Generate a splash image with the message... + img_w=$(identify -format '%w' $img_fpath) + img_h=$(identify -format '%h' $img_fpath) + if [[ "${#zynthian_message}" > "40" ]]; then + font_size=$(expr $img_w / 36) + else + font_size=$(expr $img_w / 28) + fi + strlen=$(expr ${#zynthian_message} \* $font_size / 2) + pos_x=$(expr $img_w / 2 - $strlen / 2) + pos_y=$(expr $img_h \* 10 / 100) + [[ "$pos_x" > "0" ]] || pos_x=5 + convert -strip -family \"$ZYNTHIAN_UI_FONT_FAMILY\" -pointsize $font_size -fill white -draw "text $pos_x,$pos_y \"$zynthian_message\"" $img_fpath $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg + + # Display error image + xloadimage -fullscreen -onroot $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg +} + + +function splash_zynthian_error() { + # Generate an error splash image... + splash_zynthian_message "$1" "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_error.jpg" +} + + +function splash_zynthian_error_exit_ip() { + # Grab exit code if set + zynthian_error=$1 + [ "$zynthian_error" ] || zynthian_error="???" + + case $zynthian_error in + 1) + message="Software" + ;; + 139) + message="SegFault" + ;; + 200) + message="Zyncore" + ;; + 201) + message="Control I/O" + ;; + 202) + message="Audio/MIDI" + ;; + 203) + message="CV/Gate" + ;; + *) + message="ErrCode $zynthian_error" + ;; + esac + + # Get the IP + #zynthian_ip=`ip route get 1 | awk '{print $NF;exit}'` + zynthian_ip=$(hostname -I | cut -d " " -f1) + + # Format the message + zynthian_message="IP:$zynthian_ip $message" + + # Generate an error splash image with the IP & exit code... + splash_zynthian_message "$zynthian_message" "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_error.jpg" +} + +function splash_zynthian_last_message() { + if [ -f "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg" ]; then + xloadimage -fullscreen -onroot $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg + else + xloadimage -fullscreen -onroot $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_boot.jpg + fi +} + +function clean_zynthian_last_message() { + if [ -f "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg" ]; then + rm -f $ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_message.jpg + fi +} + +function start_wifi_ap() { + readarray -t connected_devices <<< $(nmcli --terse c show | cut -d : -f 4 | tr -s '\n' | tr -s 'lo\n') + if [[ "${#connected_devices[*]}" < "2" ]]; then + nmcli radio wifi on + nmcli con up "zynthian-ap" + fi +} + +powersave_control.sh off +load_config_env + +#------------------------------------------------------------------------------ +# Test splash screen generator +#------------------------------------------------------------------------------ + +#splash_zynthian_message "Testing Splash Screen Generator..." +#sleep 10 +#exit + +if [[ "$(systemctl is-enabled first_boot)" == "enabled" ]]; then + is_first_boot=1 +else + is_first_boot=0 +fi + +#------------------------------------------------------------------------------ +# If needed, generate splash screen images +#------------------------------------------------------------------------------ + +if [[ ! -f "$ZYNTHIAN_CONFIG_DIR/img/fb_zynthian_error.jpg" ]]; then + if [[ "$is_first_boot" == "1" ]]; then + $ZYNTHIAN_SYS_DIR/sbin/generate_fb_splash.sh >> /root/first_boot.log + else + $ZYNTHIAN_SYS_DIR/sbin/generate_fb_splash.sh + fi +fi + +#------------------------------------------------------------------------------ +# Run Hardware Test +#------------------------------------------------------------------------------ + +if [[ -n "$ZYNTHIAN_HW_TEST" ]]; then + echo "Running HW test: $ZYNTHIAN_HW_TEST" + result=$($ZYNTHIAN_SYS_DIR/sbin/zynthian_hw_test.py $ZYNTHIAN_HW_TEST | tail -1) + res=${result%:*} + message=${result#*:} + + if [[ "$res" == "OK" ]]; then + splash_zynthian_message "$result" + else + splash_zynthian_error "$message" + fi + + run_control_test="0" + if [[ "$ZYNTHIAN_UI_CONTROL_TEST_ENABLED" == "1" ]]; then + control_board_name="V5_CONTROL" + echo "Testing control board '$control_board_name'..." + result=$($ZYNTHIAN_SYS_DIR/sbin/zynthian_hw_test.py $control_board_name | tail -1) + res=${result%:*} + #echo "RESULT => $result => $res" + if [[ "$res" == "OK" ]]; then + run_control_test="1" + fi + fi + + echo "Running HW control test => $run_control_test" + if [[ "$run_control_test" == "0" ]]; then + sleep 3600 + exit + fi +fi + +#------------------------------------------------------------------------------ +# Build zyncore if needed +#------------------------------------------------------------------------------ + +if [[ ! -f "$ZYNTHIAN_DIR/zyncoder/build/libzyncore.so" ]]; then + splash_zynthian_message "Building zyncore. Please wait..." + $ZYNTHIAN_DIR/zyncoder/build.sh +fi + +#------------------------------------------------------------------------------ +# Detect first boot +#------------------------------------------------------------------------------ + +if [[ "$is_first_boot" == "1" ]]; then + echo "Running first boot..." + splash_zynthian_message "Configuring your zynthian. Time to relax before the waves..." + sleep 1800 + splash_zynthian_error "It takes too long! Bad sdcard/image, poor power supply..." + sleep 3600000 + exit +fi + +#------------------------------------------------------------------------------ +# Run Zynthian-UI +#------------------------------------------------------------------------------ + +splash_zynthian + +while true; do + clean_zynthian_last_message + + # Start Zynthian GUI & Synth Engine + cd $ZYNTHIAN_UI_DIR + ./zynthian_main.py + status=$? + + echo -e "\n*******************\nEXIT STATUS => $status\n*******************\n" + + # Proccess output status + case $status in + 0) + #splash_zynthian_message "Powering Off..." + splash_zynthian_last_message + poweroff + #backlight_control.sh off + break + ;; + 100) + #splash_zynthian_message "Rebooting..." + splash_zynthian_last_message + reboot + break + ;; + 101) + #splash_zynthian_message "Exiting..." + splash_zynthian_last_message + break + ;; + 102) + #splash_zynthian_message "Restarting UI..." + splash_zynthian_last_message + load_config_env + sleep 10 + ;; + *) + splash_zynthian_error_exit_ip $status + load_config_env + start_wifi_ap + sleep 10 + ;; + esac +done + +#------------------------------------------------------------------------------ From 4cda911c9df368ba5759b281fe5b185d1c2f4aa5 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 18:49:25 +0200 Subject: [PATCH 55/57] changes to zynthian_signal_manager.py file deleted must check out origin --- zyngine/zynthian_signal_manager.py | 190 ----------------------------- 1 file changed, 190 deletions(-) delete mode 100644 zyngine/zynthian_signal_manager.py diff --git a/zyngine/zynthian_signal_manager.py b/zyngine/zynthian_signal_manager.py deleted file mode 100644 index 6e8a10d7e..000000000 --- a/zyngine/zynthian_signal_manager.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- -# **************************************************************************** -# ZYNTHIAN PROJECT: Zynthian Signal Manager (zynthian_signal_manager) -# -# zynthian signal manager -# -# Copyright (C) 2015-2023 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 logging -import traceback -from queue import SimpleQueue -from threading import Thread - -# ---------------------------------------------------------------------------- -# Zynthian Signal Manager Class -# ---------------------------------------------------------------------------- - - -class zynthian_signal_manager: - - S_ALL = 0 # Clients registering for this signal, will receive all signals - S_STATE_MAN = 1 - S_CHAIN_MAN = 2 - S_CHAIN = 3 - S_AUDIO_RECORDER = 4 - S_AUDIO_PLAYER = 5 - S_SMF_RECORDER = 6 - S_ALSA_MIXER = 7 - S_AUDIO_MIXER = 8 - S_STEPSEQ = 9 - S_CUIA = 10 - S_GUI = 11 - S_MIDI = 12 - - SS_CUIA_REFRESH = 0 - SS_CUIA_MIDI_EVENT = 1 - - SS_GUI_SHOW_SCREEN = 0 - SS_GUI_SHOW_SIDEBAR = 1 - SS_GUI_CONTROL_MODE = 2 - SS_GUI_SHOW_FILE_SELECTOR = 3 - - SS_MIDI_ALL = 0 - SS_MIDI_CC = 1 - SS_MIDI_PC = 2 - SS_MIDI_NOTE_ON = 3 - SS_MIDI_NOTE_OFF = 4 - SS_MIDI_SYSEX = 5 - - last_signal = 13 - last_subsignal = 10 - - def __init__(self): - """ Create an instance of a signal manager - - Manages signaling. Clients register callbacks that are triggered when a given signal is received. - """ - - self.exit_flag = False - - # List of lists of registered callback functions. - # Indexes ar signal & subsignal numbers - self.signal_register = None - self.reset_register() - - self.queue = SimpleQueue() - self.queue_thread = None - self.start_queue_thread() - - def stop(self): - self.exit_flag = True - - # ---------------------------------------------------------------------------- - # Signal register handling - # ---------------------------------------------------------------------------- - - def reset_register(self): - # self.signal_register = [[[]] * self.last_subsignal] * self.last_signal - self.signal_register = [] - for i in range(self.last_signal): - self.signal_register.append([]) - for j in range(self.last_subsignal): - self.signal_register[i].append([]) - - def register(self, signal, subsignal, callback, queued=False): - if 0 <= signal <= self.last_signal and 0 <= subsignal <= self.last_subsignal: - # logging.debug(f"Registering callback '{callback.__name__}()' for signal({signal},{subsignal})") - self.signal_register[signal][subsignal].append((callback, queued)) - - def register_queued(self, signal, subsignal, callback): - if 0 <= signal <= self.last_signal and 0 <= subsignal <= self.last_subsignal: - # logging.debug(f"Registering queued callback '{callback.__name__}()' for signal({signal},{subsignal})") - self.signal_register[signal][subsignal].append((callback, True)) - - def unregister(self, signal, subsignal, callback): - if 0 <= signal <= self.last_signal and 0 <= subsignal <= self.last_subsignal: - # logging.debug(f"Unregistering callback '{callback.__name__}()' from signal({signal},{subsignal})") - n = 0 - for k, rdata in enumerate(self.signal_register[signal][subsignal]): - if rdata[0] == callback: - del self.signal_register[signal][subsignal][k] - n += 1 - if n == 0: - logging.warning( - f"Callback not registered for signal({signal},{subsignal})") - - def unregister_all(self, callback): - n = 0 - for i in range(self.last_signal): - for j in range(self.last_subsignal): - for k, rdata in enumerate(self.signal_register[i][j]): - if rdata[0] == callback: - del self.signal_register[i][j][k] - n += 1 - if n == 0: - logging.warning(f"Callback not registered") - - def process_signal(self, force_queued, signal, subsignal, **kwargs): - if 0 <= signal <= self.last_signal and 0 <= subsignal <= self.last_subsignal: - # logging.debug(f"Signal({signal},{subsignal}): {kwargs}") - for rdata in self.signal_register[signal][subsignal]: - if force_queued == 1 or rdata[1]: - self.queue.put_nowait( - (signal, subsignal, rdata[0], kwargs)) - else: - try: - # logging.debug(f" => calling {rdata[0].__name__}(...)") - rdata[0](**kwargs) - except Exception as e: - logging.error( - f"Callback '{rdata[0].__name__}(...)' for signal({signal},{subsignal}): {e}") - logging.exception(traceback.format_exc()) - - def send(self, signal, subsignal, **kwargs): - """ Send direct call signal - """ - self.process_signal(False, signal, subsignal, **kwargs) - - def send_queued(self, signal, subsignal, **kwargs): - """ Send queued signal - """ - self.process_signal(True, signal, subsignal, **kwargs) - - # ---------------------------------------------------------------------------- - # Queued signal handling - # ---------------------------------------------------------------------------- - - def start_queue_thread(self): - self.queue_thread = Thread(target=self.queue_thread_task, args=()) - self.queue_thread.name = "SIGNAL_QUEUE" - self.queue_thread.daemon = True # thread dies with the program - self.queue_thread.start() - - def queue_thread_task(self): - while not self.exit_flag: - try: - data = self.queue.get(True, 1) - except: - continue - try: - # logging.debug(f" => calling {data[2].__name__}(...)") - data[2](**data[3]) - except Exception as e: - logging.error( - f"Queued callback '{data[2].__name__}(...)' for signal({data[0]},{data[1]}): {e}") - logging.exception(traceback.format_exc()) - -# --------------------------------------------------------------------------- - - -global zynsigman -zynsigman = zynthian_signal_manager() # Instance signal manager - -# --------------------------------------------------------------------------- From 064df273a21e9e098d78a6a40fb28e2249f77af8 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 18:53:13 +0200 Subject: [PATCH 56/57] Reverted to original file --- zyngine/zynthian_signal_manager.py | 190 +++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 zyngine/zynthian_signal_manager.py diff --git a/zyngine/zynthian_signal_manager.py b/zyngine/zynthian_signal_manager.py new file mode 100644 index 000000000..6e8a10d7e --- /dev/null +++ b/zyngine/zynthian_signal_manager.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# **************************************************************************** +# ZYNTHIAN PROJECT: Zynthian Signal Manager (zynthian_signal_manager) +# +# zynthian signal manager +# +# Copyright (C) 2015-2023 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 logging +import traceback +from queue import SimpleQueue +from threading import Thread + +# ---------------------------------------------------------------------------- +# Zynthian Signal Manager Class +# ---------------------------------------------------------------------------- + + +class zynthian_signal_manager: + + S_ALL = 0 # Clients registering for this signal, will receive all signals + S_STATE_MAN = 1 + S_CHAIN_MAN = 2 + S_CHAIN = 3 + S_AUDIO_RECORDER = 4 + S_AUDIO_PLAYER = 5 + S_SMF_RECORDER = 6 + S_ALSA_MIXER = 7 + S_AUDIO_MIXER = 8 + S_STEPSEQ = 9 + S_CUIA = 10 + S_GUI = 11 + S_MIDI = 12 + + SS_CUIA_REFRESH = 0 + SS_CUIA_MIDI_EVENT = 1 + + SS_GUI_SHOW_SCREEN = 0 + SS_GUI_SHOW_SIDEBAR = 1 + SS_GUI_CONTROL_MODE = 2 + SS_GUI_SHOW_FILE_SELECTOR = 3 + + SS_MIDI_ALL = 0 + SS_MIDI_CC = 1 + SS_MIDI_PC = 2 + SS_MIDI_NOTE_ON = 3 + SS_MIDI_NOTE_OFF = 4 + SS_MIDI_SYSEX = 5 + + last_signal = 13 + last_subsignal = 10 + + def __init__(self): + """ Create an instance of a signal manager + + Manages signaling. Clients register callbacks that are triggered when a given signal is received. + """ + + self.exit_flag = False + + # List of lists of registered callback functions. + # Indexes ar signal & subsignal numbers + self.signal_register = None + self.reset_register() + + self.queue = SimpleQueue() + self.queue_thread = None + self.start_queue_thread() + + def stop(self): + self.exit_flag = True + + # ---------------------------------------------------------------------------- + # Signal register handling + # ---------------------------------------------------------------------------- + + def reset_register(self): + # self.signal_register = [[[]] * self.last_subsignal] * self.last_signal + self.signal_register = [] + for i in range(self.last_signal): + self.signal_register.append([]) + for j in range(self.last_subsignal): + self.signal_register[i].append([]) + + def register(self, signal, subsignal, callback, queued=False): + if 0 <= signal <= self.last_signal and 0 <= subsignal <= self.last_subsignal: + # logging.debug(f"Registering callback '{callback.__name__}()' for signal({signal},{subsignal})") + self.signal_register[signal][subsignal].append((callback, queued)) + + def register_queued(self, signal, subsignal, callback): + if 0 <= signal <= self.last_signal and 0 <= subsignal <= self.last_subsignal: + # logging.debug(f"Registering queued callback '{callback.__name__}()' for signal({signal},{subsignal})") + self.signal_register[signal][subsignal].append((callback, True)) + + def unregister(self, signal, subsignal, callback): + if 0 <= signal <= self.last_signal and 0 <= subsignal <= self.last_subsignal: + # logging.debug(f"Unregistering callback '{callback.__name__}()' from signal({signal},{subsignal})") + n = 0 + for k, rdata in enumerate(self.signal_register[signal][subsignal]): + if rdata[0] == callback: + del self.signal_register[signal][subsignal][k] + n += 1 + if n == 0: + logging.warning( + f"Callback not registered for signal({signal},{subsignal})") + + def unregister_all(self, callback): + n = 0 + for i in range(self.last_signal): + for j in range(self.last_subsignal): + for k, rdata in enumerate(self.signal_register[i][j]): + if rdata[0] == callback: + del self.signal_register[i][j][k] + n += 1 + if n == 0: + logging.warning(f"Callback not registered") + + def process_signal(self, force_queued, signal, subsignal, **kwargs): + if 0 <= signal <= self.last_signal and 0 <= subsignal <= self.last_subsignal: + # logging.debug(f"Signal({signal},{subsignal}): {kwargs}") + for rdata in self.signal_register[signal][subsignal]: + if force_queued == 1 or rdata[1]: + self.queue.put_nowait( + (signal, subsignal, rdata[0], kwargs)) + else: + try: + # logging.debug(f" => calling {rdata[0].__name__}(...)") + rdata[0](**kwargs) + except Exception as e: + logging.error( + f"Callback '{rdata[0].__name__}(...)' for signal({signal},{subsignal}): {e}") + logging.exception(traceback.format_exc()) + + def send(self, signal, subsignal, **kwargs): + """ Send direct call signal + """ + self.process_signal(False, signal, subsignal, **kwargs) + + def send_queued(self, signal, subsignal, **kwargs): + """ Send queued signal + """ + self.process_signal(True, signal, subsignal, **kwargs) + + # ---------------------------------------------------------------------------- + # Queued signal handling + # ---------------------------------------------------------------------------- + + def start_queue_thread(self): + self.queue_thread = Thread(target=self.queue_thread_task, args=()) + self.queue_thread.name = "SIGNAL_QUEUE" + self.queue_thread.daemon = True # thread dies with the program + self.queue_thread.start() + + def queue_thread_task(self): + while not self.exit_flag: + try: + data = self.queue.get(True, 1) + except: + continue + try: + # logging.debug(f" => calling {data[2].__name__}(...)") + data[2](**data[3]) + except Exception as e: + logging.error( + f"Queued callback '{data[2].__name__}(...)' for signal({data[0]},{data[1]}): {e}") + logging.exception(traceback.format_exc()) + +# --------------------------------------------------------------------------- + + +global zynsigman +zynsigman = zynthian_signal_manager() # Instance signal manager + +# --------------------------------------------------------------------------- From 2ee9eb5d88fba6b6ef38c79aab322fa16b0ec9b5 Mon Sep 17 00:00:00 2001 From: JBrumby Date: Sun, 21 Sep 2025 19:06:09 +0200 Subject: [PATCH 57/57] reverted to original file --- zyngine/zynthian_state_manager.py | 2803 +++++++++++++++++++++++++++++ 1 file changed, 2803 insertions(+) create mode 100644 zyngine/zynthian_state_manager.py diff --git a/zyngine/zynthian_state_manager.py b/zyngine/zynthian_state_manager.py new file mode 100644 index 000000000..5ee21e2f4 --- /dev/null +++ b/zyngine/zynthian_state_manager.py @@ -0,0 +1,2803 @@ +# -*- coding: utf-8 -*- +# **************************************************************************** +# ZYNTHIAN PROJECT: Zynthian State Manager (zynthian_state_manager) +# +# zynthian state manager +# +# Copyright (C) 2015-2024 Fernando Moyano +# Brian Walton +# +# **************************************************************************** +# +# 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 base64 +import ctypes +import logging +import traceback +from glob import glob +from threading import Thread +from queue import SimpleQueue +from datetime import datetime +from time import sleep, monotonic +from json import JSONEncoder, JSONDecoder +from subprocess import check_output, Popen, STDOUT, PIPE +from os.path import basename, isdir, isfile, join, dirname, splitext + +# Zynthian specific modules +import zynconf +import zynautoconnect + +from zyncoder.zyncore import lib_zyncore +from zynlibs.zynaudioplayer import * +from zynlibs.zynseq import zynseq +# Python wrapper for zynsmf (ensures initialised and wraps load() function) +from zynlibs.zynsmf import zynsmf +from zynlibs.zynsmf.zynsmf import libsmf # Direct access to shared library + +from zyngine.zynthian_chain_manager import * +from zyngine.zynthian_processor import zynthian_processor +from zyngine.zynthian_audio_recorder import zynthian_audio_recorder +from zyngine.zynthian_signal_manager import zynsigman +from zyngine.zynthian_legacy_snapshot import zynthian_legacy_snapshot, SNAPSHOT_SCHEMA_VERSION +from zyngine import zynthian_engine_audio_mixer +from zyngine import zynthian_midi_filter + +from zyngui import zynthian_gui_config +from zyngine.zynthian_ctrldev_manager import zynthian_ctrldev_manager + +# ---------------------------------------------------------------------------- +# Zynthian State Manager Class +# ---------------------------------------------------------------------------- + +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") + + +class zynthian_state_manager: + + # Subsignals are defined inside each module. Here we define state manager subsignals: + SS_LOAD_SNAPSHOT = 1 + SS_MIDI_PLAYER_STATE = 2 + SS_MIDI_RECORDER_STATE = 3 + SS_LOAD_ZS3 = 4 + SS_SAVE_ZS3 = 5 + SS_ALL_NOTES_OFF = 6 + + # Subsignals from other modules. Just to simplify access. + # From S_AUDIO_PLAYER + SS_AUDIO_PLAYER_STATE = 1 + # From S_AUDIO_RECORDER + SS_AUDIO_RECORDER_STATE = 1 + SS_AUDIO_RECORDER_ARM = 2 + + def __init__(self): + """ Create an instance of a state manager + + Manages full Zynthian state, i.e. snapshot + """ + + logging.info("Creating state manager") + + self.busy = set() # Set of clients indicating they are busy doing something (may be used by UI to show progress) + self.busy_message = None + self.busy_error = None + self.busy_warning = None + self.busy_success = None + 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.default_snapshot_fpath = join(self.snapshot_dir, "default.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 = "" + self.snapshot_bank = None # Name of snapshot bank (without path) + self.snapshot_program = 0 + self.zs3 = {} # Dictionary or zs3 configs indexed by "ch/pc" + self.last_zs3_id = None + + # Power saving + self.power_save_mode = False + self.last_event_flag = False + self.last_event_ts = monotonic() + + # Status + self.status_xrun = False + self.status_undervoltage = False + self.overtemp_warning = 75 # Temperature limit before warning overtemperature + self.status_overtemp = False + self.status_cpu_load = 0 # 0..100 + self.status_audio_player = False # True if playing + self.status_midi_recorder = False + self.status_midi_player = False + self.last_midi_file = None + self.status_midi = False + self.status_midi_clock = False + self.update_available = False # True when updates available from repositories + self.checking_for_updates = False # True whilst checking for updates + + self.midi_filter_script = None + self.midi_learn_state = False + # When ZS3 Program Change MIDI learning is enabled, the name used for creating new ZS3, empty string for auto-generating a name. None when disabled. + self.midi_learn_pc = None + self.midi_learn_zctrl = None # zctrl currently being learned + self.sync = False # True to request file system sync + self.zctrl_x = None + self.zctrl_y = None + + self.cuia_queue = SimpleQueue() # Queue for CUIA calls + + self.get_throttled_file = None + self.hwmon_thermal_file = None + self.hwmon_undervolt_file = None + + self.zynmixer = zynthian_engine_audio_mixer.zynmixer() + 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.refresh_controllers() + + self.audio_recorder = zynthian_audio_recorder(self) + self.zynseq = zynseq.zynseq(self) + self.ctrldev_manager = None + self.audio_player = None + self.aubio_in = [1, 2] # List of aubio inputs + + # List of lists [rate, cb, schedule] for registered regularly repeating callbacks + self.slow_update_callbacks = [] + + # Initialize SMF MIDI recorder and player + try: + self.smf_player = libsmf.addSmf() + libsmf.attachPlayer(self.smf_player) + except Exception as e: + logging.error(e) + + try: + self.smf_recorder = libsmf.addSmf() + libsmf.attachRecorder(self.smf_recorder) + except Exception as e: + logging.error(e) + + # Initialize internal MIDI sender + self.zynmidi = zynthian_zcmidi() + + self.exit_flag = False + self.slow_thread = None + self.fast_thread = None + self.start() + + self.end_busy("zynthian_state_manager") + + def start(self): + """Start state manager""" + + self.start_busy("start state") + # Initialize SOC sensors monitoring + + # Sysfs->hwmon monitoring interface + try: + sfpath = '/sys/class/hwmon/hwmon0/temp1_input' + self.hwmon_thermal_file = open(sfpath) + logging.debug(f"Opened temperature sensor '{sfpath}'") + except: + self.hwmon_thermal_file = None + logging.error("Can't access temperature sensor.") + + try: + result = glob("/sys/class/hwmon/**/in0_lcrit_alarm") + self.hwmon_undervolt_file = open(result[0]) + logging.debug(f"Opened undervoltage sensor '{result[0]}'") + except: + try: + 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: + self.hwmon_undervolt_file = None + logging.error("Can't access undervoltage sensor.") + + # 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') + except: + self.get_throttled_file = None + + # Start VNC as configured + self.default_vncserver() + + self.ctrldev_manager = zynthian_ctrldev_manager(self) + zynautoconnect.start(self) + self.jack_period = self.get_jackd_blocksize() / self.get_jackd_samplerate() + self.zynmixer.reset_state() + self.reload_midi_config() + self.create_audio_player() + self.chain_manager.add_chain(0) + + self.exit_flag = False + self.slow_thread = Thread(target=self.slow_thread_task) + self.slow_thread.name = "Status Manager Slow" + self.slow_thread.daemon = True # thread dies with the program + self.slow_thread.start() + + self.fast_thread = Thread(target=self.fast_thread_task) + self.fast_thread.name = "Status Manager Fast" + 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) + + self.end_busy("start state") + + def stop(self): + """Stop state manager""" + + self.start_busy("stop state") + + 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(): + self.fast_thread.join() + self.fast_thread = None + if self.slow_thread and self.slow_thread.is_alive(): + self.slow_thread.join() + self.slow_thread = None + + self.last_snapshot_fpath = "" + self.zynseq.transport_stop("ALL") + zynautoconnect.pause() + self.chain_manager.remove_all_chains(True) + self.reset_zs3() + self.zynseq.load("") + self.ctrldev_manager.unload_all_drivers() + self.destroy_audio_player() + zynautoconnect.stop() + + if self.hwmon_thermal_file: + self.hwmon_thermal_file.close() + self.hwmon_thermal_file = None + if self.hwmon_undervolt_file: + self.hwmon_undervolt_file.close() + self.hwmon_undervolt_file = None + if self.get_throttled_file: + self.get_throttled_file.close() + self.get_throttled_file = None + + self.end_busy("stop state") + + def reset(self): + """Reset state manager to clean initial start-up state""" + + self.start_busy("reset state") + self.stop() + sleep(0.2) + self.clear_busy() # TODO Is this needed? + self.start() + self.end_busy("reset state") + + def clean(self, chains=True, zynseq=True): + """Remove Chains & Sequences. + chains : True for cleaning all chains + sequences : True for cleaning zynseq state (sequences) + """ + + self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, 1) + # self.zynseq.transport_stop("ALL") + self.zynseq.libseq.stop() + if zynseq: + self.zynseq.load("") + if chains: + zynautoconnect.pause() + self.chain_manager.remove_all_chains(True) + self.reset_zs3() + self.zynmixer.reset_state() + self.reload_midi_config() + zynautoconnect.request_midi_connect(True) + zynautoconnect.request_audio_connect(True) + zynautoconnect.resume() + self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, 0) + + def clean_all(self): + """Remove ALL Chains & Sequences.""" + + self.start_busy("clean all", "cleaning all...") + self.clean(chains=True, zynseq=True) + self.last_snapshot_fpath = "" + self.end_busy("clean all") + self.busy.clear() # Sometimes it's needed, why?? + + def clean_chains(self): + """Remove ALL chains while keeping sequences.""" + + self.start_busy("clean chains", "cleaning chains...") + self.clean(chains=True, zynseq=False) + self.end_busy("clean chains") + self.busy.clear() # Sometimes it's needed, why?? + + def clean_sequences(self): + """Remove ALL sequences while keeping chains.""" + + self.start_busy("clean sequences", "cleaning sequences...") + self.clean(chains=False, zynseq=True) + self.end_busy("clean sequences") + self.busy.clear() # Sometimes it's needed, why?? + + # ------------------------------------------------------------------------- + # Internal parameters and core limits + # ------------------------------------------------------------------------- + + def get_max_num_mixer_chans(self): + return MAX_NUM_MIXER_CHANS + + def get_num_zmop_chains(self): + return NUM_ZMOP_CHAINS + + def get_max_num_zmops(self): + return MAX_NUM_ZMOPS + + def get_num_midi_devs_in(self): + return NUM_MIDI_DEVS_IN + + def get_num_midi_devs_out(self): + return NUM_MIDI_DEVS_OUT + + def get_max_num_midi_devs(self): + return MAX_NUM_MIDI_DEVS + + def get_zmip_seq_index(self): + return ZMIP_SEQ_INDEX + + def get_zmip_step_index(self): + return ZMIP_STEP_INDEX + + def get_zmip_int_index(self): + return ZMIP_INT_INDEX + + def get_zmip_ctrl_index(self): + return ZMIP_CTRL_INDEX + + # ------------------------------------------------------------------------- + # Busy state management + # ------------------------------------------------------------------------- + + def start_busy(self, clid, message=None, details=None): + """Add client to list of busy clients + clid : Client id + """ + + self.busy.add(clid) + if message: + self.busy_message = message + if details: + self.busy_details = details + + # logging.debug(f"Start busy for {clid}. Message: '{message}', Details: '{details}', Current clients: {self.busy})") + + def end_busy(self, clid): + """Remove client from list of busy clients + clid : Client id + """ + + try: + self.busy.remove(clid) + except: + pass + if len(self.busy) == 0: + self.busy_message = None + self.busy_error = None + self.busy_warning = None + self.busy_success = None + self.busy_details = None + + # logging.debug(f"End busy for {clid}. Remaining clients: {self.busy}") + + def clear_busy(self): + self.busy.clear() + self.busy_message = None + self.busy_error = None + self.busy_warning = None + self.busy_success = None + self.busy_details = None + + def is_busy(self, client=None): + """Check if clients are busy + client : Name of client to check (Default: all clients) + Returns : True if any clients are busy + """ + + if client: + return client in self.busy + return len(self.busy) > 0 + + def set_busy_message(self, message, details=None): + """Set busy message + message : message text + """ + + if len(self.busy) > 0: + self.busy_message = message + if details: + self.details = details + + def get_busy_message(self): + """Returns busy message and clean it + return message text + """ + + res = self.busy_message + self.busy_message = None + return res + + def set_busy_error(self, message, details=None): + """Set busy error message + message : message text + """ + + if len(self.busy) > 0: + self.busy_error = message + if details: + self.details = details + + def get_busy_error(self): + """Returns busy error message and clean it + return message text + """ + + res = self.busy_error + self.busy_error = None + return res + + def set_busy_warning(self, message, details=None): + """Set busy warning message + message : message text + """ + + if len(self.busy) > 0: + self.busy_warning = message + if details: + self.details = details + + def get_busy_warning(self): + """Returns busy warning message and clean it + return message text + """ + + res = self.busy_warning + self.busy_warning = None + return res + + def set_busy_success(self, message, details=None): + """Set busy success message text + details : details text + """ + + if len(self.busy) > 0: + self.busy_success = message + if details: + self.details = details + + def get_busy_success(self): + """Returns busy success message and clean it + return message text + """ + + res = self.busy_success + self.busy_success = None + return res + + def set_busy_details(self, details): + """Set busy details text + details : details text + """ + + if len(self.busy) > 0: + self.busy_details = details + + def get_busy_details(self): + """Returns busy details and clean it + return details text + """ + + res = self.busy_details + self.busy_details = None + return res + + # ---------------------------------------------------------------------------- + # CUIA Queue + # ---------------------------------------------------------------------------- + + def send_cuia(self, cuia, params=None): + self.cuia_queue.put_nowait((cuia, params)) + + def parse_cuia_params(self, params_str): + params = [] + for i, p in enumerate(params_str.split(",")): + try: + params.append(int(p)) + except: + params.append(p.strip()) + return params + + # ------------------------------------------------------------------ + # Background task threads + # ------------------------------------------------------------------ + + def slow_thread_task(self): + """Perform slow / low priority background tasks""" + + status_counter = 0 + xruns_status = self.status_xrun + midi_status = self.status_midi + midi_clock_status = self.status_midi_clock + # Short delay after startup before first slow update + next_second_check = monotonic() + 2 + self.add_slow_update_callback(3600, self.check_for_updates) + + while not self.exit_flag: + # Get CPU Load + # self.status_cpu_load = max(psutil.cpu_percent(None, True)) + self.status_cpu_load = zynautoconnect.get_jackd_cpu_load() + now = monotonic() + + try: + # Get SOC sensors (once each 5 refreshes) + if status_counter > 5: + status_counter = 0 + + self.status_overtemp = False + self.status_undervoltage = False + + # RBPi native sensors interface + if self.get_throttled_file: + try: + self.get_throttled_file.seek(0) + thr = int('0x%s' % self.get_throttled_file.read(), 16) + if thr & 0x1: + self.status_undervoltage = True + elif thr & (0x4 | 0x2): + self.status_overtemp = True + except Exception as e: + logging.error(e) + + # Alternate sensor interface + elif self.hwmon_thermal_file and self.hwmon_undervolt_file: + try: + self.hwmon_thermal_file.seek(0) + res = int(self.hwmon_thermal_file.read())/1000 + # logging.debug(f"CPU Temperature => {res}") + if res > self.overtemp_warning: + self.status_overtemp = True + except Exception as e: + logging.error(e) + + try: + self.hwmon_undervolt_file.seek(0) + res = self.hwmon_undervolt_file.read() + if res == "1": + self.status_undervoltage = True + except Exception as e: + logging.error(e) + + else: + self.status_overtemp = True + self.status_undervoltage = True + + else: + status_counter += 1 + + # MIDI Player + # TODO: Add callback from MIDI player to avoid polling (and regular access to c-lib) + 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) + + # 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) + + # Sequencer Status => It must be improved using callbacks + self.zynseq.update_state() + + # Clean some status flags + if xruns_status: + self.status_xrun = False + xruns_status = False + if self.status_xrun: + xruns_status = True + + if midi_status: + self.status_midi = False + midi_status = False + if self.status_midi: + midi_status = True + + if midi_clock_status: + self.status_midi_clock = False + midi_clock_status = False + if self.status_midi_clock: + midi_clock_status = True + + if self.sync: + self.sync = False + os.sync() + + if now > next_second_check: + for cb in self.slow_update_callbacks: + if now > cb[2]: + try: + cb[1]() + cb[2] = now + cb[0] + except Exception as e: + logging.error(e) + next_second_check = now + 1 + + except Exception as e: + logging.exception(e) + + sleep(0.2) + + def cb_status_audio_player(self, handle, state): + if handle == self.audio_player.handle: + self.status_audio_player = state + + def fast_thread_task(self): + """Perform fast / high priority background tasks""" + + while not self.exit_flag: + # Process MIDI events + self.zynmidi_read() + sleep(0.01) + + def add_slow_update_callback(self, rate, cb): + """Add a callback to be called every "rate" seconds + + rate - time in seconds between callbacks + cb - Callback function + """ + + self.remove_slow_update_callback(cb) + self.slow_update_callbacks.append([rate, cb, 0]) + + def remove_slow_update_callback(self, cb): + """Add a callback to be called every "rate" seconds + + rate - time in seconds between callbacks + cb - Callback function + """ + + for cb in self.slow_update_callbacks: + if cb[1] == cb: + cb.remove(cb) + break + + # ------------------------------------------------------------------ + # MIDI processing + # ------------------------------------------------------------------ + + def zynmidi_read(self): + try: + n = lib_zyncore.get_zynmidi_num_pending() + if n <= 0: + return + midi_events = (ctypes.c_uint32 * n)() + n = lib_zyncore.read_zynmidi_buffer(midi_events, n) + i = 0 + while i < n: + ev = midi_events[i].to_bytes(4, 'big') + i += 1 + izmip = ev[0] + evhead = ev[1] + ev = ev[1:] + + # Process SysEx + if evhead == 0xF0: + # logging.debug(f"RECEIVED SYSEX FROM {izmip}...") + sysex_data = bytearray(ev) + while i < n: + chunk = midi_events[i].to_bytes(4, 'big') + sysex_data.extend(chunk) + if 0xF7 in chunk: + break + i += 1 + # This is probably not correct and we should continue reading in the next period + if i == n: + logging.error(f"SysEx message from device {izmip} is not terminated") + continue + # Crop data until find the 0xF7 mark + while sysex_data[-1] != 0xF7: + del sysex_data[-1] + # logging.debug(f" SYSEX DATA => {sysex_data}") + ev = bytes(sysex_data) + + # Try to manage with a control device driver + if self.ctrldev_manager.midi_event(izmip, ev): + self.status_midi = True + self.last_event_flag = True + continue + + evtype = (evhead >> 4) & 0x0F + chan = evhead & 0x0F + + # logging.info(f"MIDI EVENT: IZMIP={izmip}, TYPE={evtype}, CHAN={chan}") + + # System Messages (Common & RT) + if evtype == 0xF: + # SysEx + if chan == 0x0: + # Handle SysEx from external devices only + if izmip < self.get_max_num_midi_devs(): + zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_SYSEX, izmip=izmip, data=ev) + # Clock + elif chan == 0x8: + self.status_midi_clock = True + continue + # Tick + elif chan == 0x9: + continue + # Active Sense + elif chan == 0xE: + continue + # Reset + elif chan == 0xF: + pass + + # Master MIDI Channel... + elif chan == zynthian_gui_config.master_midi_channel: + logging.info(f"MASTER MIDI MESSAGE: {ev.hex()}") + # Webconf configured messages for Snapshot Control... + if ev == zynthian_gui_config.master_midi_program_change_up: + logging.debug("PROGRAM CHANGE UP!") + self.load_snapshot_by_prog(self.snapshot_program + 1) + elif ev == zynthian_gui_config.master_midi_program_change_down: + logging.debug("PROGRAM CHANGE DOWN!") + self.load_snapshot_by_prog(self.snapshot_program - 1) + elif ev == zynthian_gui_config.master_midi_bank_change_up: + logging.debug("BANK CHANGE UP!") + self.set_snapshot_midi_bank(self.snapshot_bank + 1) + elif ev == zynthian_gui_config.master_midi_bank_change_down: + logging.debug("BANK CHANGE DOWN!") + self.set_snapshot_midi_bank(self.snapshot_bank - 1) + # Program Change => Snapshot Load + elif evtype == 0xC: + pgm = ev[1] & 0x7F + logging.debug("PROGRAM CHANGE %d" % pgm) + self.start_busy("load_snapshot", "loading snapshot") + self.load_snapshot_by_prog(pgm) + self.end_busy("load_snapshot") + # Control Change... + elif evtype == 0xB: + ccnum = ev[1] & 0x7F + ccval = ev[2] & 0x7F + if ccnum == zynthian_gui_config.master_midi_bank_change_ccnum: + logging.debug(f"BANK CHANGE {ccval}") + self.set_snapshot_midi_bank(ccval) + elif ccnum == 120: + self.all_sounds_off() + elif ccnum == 123: + self.all_notes_off() + else: + if self.midi_learn_zctrl: + self.chain_manager.add_midi_learn(chan, ccnum, self.midi_learn_zctrl, izmip) + else: + 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) + vel = ev[2] & 0x7F + if note in zynthian_gui_config.master_midi_note_cuia: + cuia_str = zynthian_gui_config.master_midi_note_cuia[note] + parts = cuia_str.split(" ", 2) + cuia = parts[0].lower() + if len(parts) > 1: + params = self.parse_cuia_params(parts[1]) + else: + params = None + # Emulate Zynswitch Push/Release with Note On/Off + if cuia == "zynswitch" and len(params) == 1: + if evtype == 0x8 or vel == 0: + params.append('R') + else: + params.append('P') + self.cuia_queue.put_nowait((cuia, params)) + # Or normal CUIA + elif evtype == 0x9 and vel > 0: + self.cuia_queue.put_nowait((cuia, params)) + + # Control Change... + elif evtype == 0xB: + ccnum = ev[1] & 0x7F + ccval = ev[2] & 0x7F + # 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) + # Special CCs >= Channel Mode + elif ccnum == 120: + self.all_sounds_off_chan(chan) + elif ccnum == 123: + self.all_notes_off_chan(chan) + + # Program Change... + elif evtype == 0xC: + pgm = ev[1] & 0x7F + logging.info(f"MIDI PROGRAM CHANGE: CH#{chan}, PRG#{pgm}") + # MIDI learn SubSnapShot (ZS3) + if self.midi_learn_pc is not None: + # When using internal PC, ignore MIDI channel + if izmip == 0xFF: + self.save_zs3(f"*/{pgm}") + else: + self.save_zs3(f"{chan}/{pgm}") + send_signal = True + else: + # select SubSnapShot (ZS3) + if zynthian_gui_config.midi_prog_change_zs3: + # When using internal PC, ignore MIDI channel + if izmip == 0xFF: + send_signal = self.load_zs3(f"*/{pgm}") + else: + send_signal = self.load_zs3(f"{chan}/{pgm}") + # or select preset + else: + # 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) + if send_signal: + zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_PC, + izmip=izmip, chan=chan, num=pgm) + + # Note Off + elif evtype == 0x8: + # Handle external devices only + if izmip < self.get_max_num_midi_devs(): + zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_OFF, + izmip=izmip, chan=chan, note=ev[1] & 0x7f, vel=ev[2] & 0x7f) + + # Note On + elif evtype == 0x9: + # Handle external devices only + if izmip < self.get_max_num_midi_devs(): + zynsigman.send_queued(zynsigman.S_MIDI, zynsigman.SS_MIDI_NOTE_ON, + izmip=izmip, chan=chan, note=ev[1] & 0x7f, vel=ev[2] & 0x7f) + + # Flag MIDI event + self.status_midi = True + self.last_event_flag = True + + except Exception as err: + logging.exception(err) + + # --------------------------------------------------------------------------- + # Power Saving + # --------------------------------------------------------------------------- + + def power_save_check(self): + if zynthian_gui_config.power_save_secs <= 0: + return + if self.last_event_flag: + self.last_event_ts = monotonic() + self.last_event_flag = False + if self.power_save_mode: + self.set_power_save_mode(False) + elif not self.power_save_mode and (monotonic() - self.last_event_ts) > zynthian_gui_config.power_save_secs: + self.set_power_save_mode(True) + + def set_power_save_mode(self, psm=True): + self.power_save_mode = psm + if psm: + logging.info("Power Save Mode: ON") + self.ctrldev_manager.sleep_on() + check_output("powersave_control.sh on", shell=True) + else: + logging.info("Power Save Mode: OFF") + check_output("powersave_control.sh off", shell=True) + self.ctrldev_manager.sleep_off() + + def set_event_flag(self): + self.last_event_flag = True + + def reset_event_flag(self): + self.last_event_flag = False + + # ---------------------------------------------------------------------------- + # Snapshot Save & Load + # ---------------------------------------------------------------------------- + + def get_state(self): + """Get a dictionary describing the full state model""" + + self.save_zs3("zs3-0", "Last state") + self.purge_zs3() + state = { + 'schema_version': SNAPSHOT_SCHEMA_VERSION, + 'last_snapshot_fpath': self.last_snapshot_fpath, + 'midi_profile_state': self.get_midi_profile_state(), + 'chains': self.chain_manager.get_state(), + 'zs3': self.zs3, + 'last_zs3_id': self.last_zs3_id + } + + engine_states = {} + for eid, engine in self.chain_manager.zyngines.items(): + engine_state = engine.get_extended_config() + if engine_state: + engine_states[eid] = engine_state + if engine_states: + state["engine_config"] = engine_states + + # Add ALSA-Mixer setting + if zynthian_gui_config.snapshot_mixer_settings and self.alsa_mixer_processor: + state['alsa_mixer'] = self.alsa_mixer_processor.get_state() + + # Audio Recorder Armed + armed_state = [] + for midi_chan in range(self.zynmixer.MAX_NUM_CHANNELS): + if self.audio_recorder.is_armed(midi_chan): + armed_state.append(midi_chan) + if armed_state: + state['audio_recorder_armed'] = armed_state + + # Zynseq RIFF data + binary_riff_data = self.zynseq.get_riff_data() + b64_data = base64.b64encode(binary_riff_data) + state['zynseq_riff_b64'] = b64_data.decode('utf-8') + + return state + + def export_chain(self, fpath, chain_id): + """Save just a single chain to a snapshot file + + fpath: Full filename and path + chain_id: Chain to export + """ + self.start_busy("export chain", "exporting chain") + try: + # Get state + state = self.get_state() + procs = [] + for id in list(state["chains"]): + if id != chain_id: + del state["chains"][id] + else: + for slot in state["chains"][id]["slots"]: + for proc in slot.keys(): + procs.append(proc) + for zs3 in list(state["zs3"]): + if zs3 != "zs3-0": + del state["zs3"][zs3] + else: + if "processors" in state["zs3"][zs3]: + for proc in list(state["zs3"][zs3]["processors"]): + if proc not in procs: + del state["zs3"][zs3]["processors"][proc] + for id in list(state["zs3"][zs3]["chains"]): + if id != chain_id: + del state["zs3"][zs3]["chains"][id] + for key in ["global", "midi_capture", "active_chain"]: + try: + del state["zs3"][zs3][key] + 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] + except: + pass + + # JSON Encode + json = JSONEncoder().encode(state) + with open(fpath, "w") as fh: + logging.info(f"Saving snapshot {fpath} ...") + # logging.debug(f"Snapshot JSON Data =>\n{json}") + fh.write(json) + fh.flush() + os.fsync(fh.fileno()) + except Exception as e: + logging.exception(traceback.format_exc()) + logging.error("Can't export chain file '%s': %s" % (fpath, e)) + self.set_busy_error("ERROR saving snapshot", e) + sleep(2) + self.end_busy("export chain") + return False + + self.end_busy("export chain") + return True + + def save_snapshot(self, fpath, extra_data=None): + """Save current state model to file + + fpath : Full filename and path + extra_data : Dictionary to add to snapshot, e.g. UI specific config + Returns : True on success + """ + + self.start_busy("save snapshot", "saving snapshot") + try: + # Get state + state = self.get_state() + if isinstance(extra_data, dict): + state = {**state, **extra_data} + # JSON Encode + json = JSONEncoder().encode(state) + with open(fpath, "w") as fh: + logging.info(f"Saving snapshot {fpath} ...") + # logging.debug(f"Snapshot JSON Data =>\n{json}") + fh.write(json) + fh.flush() + os.fsync(fh.fileno()) + except Exception as e: + logging.exception(traceback.format_exc()) + logging.error("Can't save snapshot file '%s': %s" % (fpath, e)) + self.set_busy_error("ERROR saving snapshot", e) + sleep(2) + self.end_busy("save snapshot") + return False + + self.last_snapshot_fpath = fpath + self.end_busy("save snapshot") + return True + + def load_snapshot(self, fpath, load_chains=True, load_sequences=True, merge=False): + """Loads a snapshot from file + + fpath : Full path and filename of snapshot file + load_chains : True to load chains + load_sequences : True to load sequences into step sequencer + Returns : State dictionary or None on failure + """ + + self.start_busy("load snapshot", "loading snapshot") + try: + with open(fpath, "r") as fh: + json = fh.read() + logging.info(f"Loading snapshot '{fpath}' ...") + # logging.debug(f"Snapshot JSON Data =>\n{json}") + except Exception as e: + logging.error("Can't load snapshot '%s': %s" % (fpath, e)) + self.end_busy("load snapshot") + return None + + mute = self.zynmixer.get_mute(self.zynmixer.MAX_NUM_CHANNELS - 1) + try: + snapshot = JSONDecoder().decode(json) + self.set_busy_details("fixing legacy snapshot") + converter = zynthian_legacy_snapshot(self) + state = converter.convert_state(snapshot) + + if load_chains: + # Mute output to avoid unwanted noises + self.zynmixer.set_mute( + self.zynmixer.MAX_NUM_CHANNELS - 1, True) + + zynautoconnect.pause() + if "chains" in state: + if "engine_config" in state: + engine_config = state["engine_config"] + else: + engine_config = None + + if merge: + # Remove elements that are not to be merged + for key in ["last_snapshot_fpath", "last_zs3_id", "midi_profile_state", "audio_recorder_armed", "zynseq_riff_b64", "alsa_mixer", "zyngui"]: + try: + del state[key] + except: + pass + # Need to reassign chains and processor ids + chain_map = {} # Map of new chain id indexed by old id + proc_map = {} # Map of new processor id indexed by old id + mixer_map = {} # Map of new mixer chan idx indexed by old idx + # Don't import main chain + try: + del state["chains"]["0"] + except: + pass + new_proc_id = 0 + for id in self.chain_manager.processors: + if new_proc_id <= id: + new_proc_id = id + 1 + + mixer_chan = 0 + for chain_id, chain_state in state["chains"].items(): + # Fix mixer channel + mixer_chan = self.chain_manager.get_next_free_mixer_chan(mixer_chan) + mixer_map[int(chain_state["mixer_chan"])] = mixer_chan + chain_state["mixer_chan"] = mixer_chan + mixer_chan += 1 + new_chain_id = 1 + while new_chain_id in self.chain_manager.chains: + new_chain_id += 1 + chain_map[chain_id] = new_chain_id + for slot, procs in enumerate(chain_state["slots"]): + new_procs = {} + for old_proc_id, proc in procs.items(): + new_procs[new_proc_id] = proc + proc_map[old_proc_id] = new_proc_id + new_proc_id += 1 + chain_state["slots"][slot] = new_procs + # Fix zs3 + procs = {} + for proc_id, proc_config in state["zs3"]["zs3-0"]["processors"].items(): + if proc_id in proc_map: + procs[proc_map[proc_id]] = proc_config + state["zs3"]["zs3-0"]["processors"] = procs + chains = {} + for chain_id, chain_config in state["zs3"]["zs3-0"]["chains"].items(): + if chain_id == '0': + continue + chains[chain_map[chain_id]] = chain_config + + if "midi_cc" in chain_config: + for cc, map in chain_config["midi_cc"].items(): + for ctrl_cfg in map: + if str(ctrl_cfg[0]) in proc_map: + ctrl_cfg[0] = proc_map[str(ctrl_cfg[0])] + state["zs3"]["zs3-0"]["chains"] = chains + mixer_chans = {} + for old_mixer_chan, new_mixer_chan in mixer_map.items(): + try: + mixer_chans[f"chan_{new_mixer_chan:02d}"] = state["zs3"]["zs3-0"]["mixer"][f"chan_{old_mixer_chan:02d}"] + except: + pass + state["zs3"]["zs3-0"]["mixer"] = mixer_chans + # We don't want to merge MIDI binding to mixer + try: + del state["zs3"]["zs3-0"]["mixer"]["midi_learn"] + except: + pass + # We don't want to merge MIDI capture + try: + del state["zs3"]["zs3-0"]["midi_capture"] + except: + pass + + self.chain_manager.set_state(state['chains'], engine_config, merge) + self.chain_manager.stop_unused_engines() + zynautoconnect.resume() + + if "last_zs3_id" in state: + self.last_zs3_id = state["last_zs3_id"] + else: + self.last_zs3_id = None + zs3 = self.sanitize_zs3_from_json(state["zs3"]) + if not merge: + self.zs3 = zs3 + self.load_zs3(zs3["zs3-0"], autoconnect=False) + try: + mute |= self.zs3["zs3-0"]["mixer"]["chan_16"]["mute"] + except: + pass + + if "alsa_mixer" in state: + self.alsa_mixer_processor.set_state(state["alsa_mixer"]) + + if "audio_recorder_armed" in state: + for midi_chan in range(self.zynmixer.MAX_NUM_CHANNELS): + if midi_chan in state["audio_recorder_armed"]: + self.audio_recorder.arm(midi_chan) + else: + self.audio_recorder.unarm(midi_chan) + + if "midi_profile_state" in state: + self.set_midi_profile_state(state["midi_profile_state"]) + + if load_sequences and "zynseq_riff_b64" in state: + b64_bytes = state["zynseq_riff_b64"].encode("utf-8") + binary_riff_data = base64.decodebytes(b64_bytes) + self.zynseq.restore_riff_data(binary_riff_data) + + if fpath == self.last_snapshot_fpath and "last_state_fpath" in state: + self.last_snapshot_fpath = state["last_snapshot_fpath"] + else: + self.last_snapshot_fpath = fpath + + self.last_snapshot_count += 1 + try: + self.snapshot_program = int(basename(fpath[:3])) + except: + pass + + except Exception as e: + state = None + logging.exception("Invalid snapshot: %s" % e) + self.set_busy_error("ERROR: Invalid snapshot", e) + sleep(2) + + zynautoconnect.request_midi_connect() + zynautoconnect.request_audio_connect(True) + + # Restore mute state + self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, mute) + + # Signal snapshot loading + zynsigman.send_queued(zynsigman.S_STATE_MAN, self.SS_LOAD_SNAPSHOT) + + self.end_busy("load snapshot") + return state + + def set_snapshot_midi_bank(self, bank): + """Set the current snapshot bank + + bank: Snapshot bank (0..127) + """ + + for bank in glob(f"{self.snapshot_dir}/{bank:03d}*"): + if isdir(bank): + self.snapshot_bank = basename(bank) + return + + def load_snapshot_by_prog(self, program, bank=None): + """Loads a snapshot from its MIDI program and bank + + program : MIDI program number + bank : MIDI bank number (Default: Use last selected bank) + Returns : True on success + """ + + if bank is None: + bank = self.snapshot_bank + if bank is None: + return # Don't load snapshot if invalid bank selected + files = glob(f"{self.snapshot_dir}/{bank}/{program:03d}-*.zss") + if files: + self.load_snapshot(files[0]) + return True + return False + + def backup_snapshot(self, path): + """Make a backup copy of a snapshot file""" + + if isfile(path): + dpath = dirname(path) + fbase, fext = splitext(basename(path)) + ts_str = datetime.now().strftime("%Y%m%d%H%M%S") + budir = dpath + "/.backup" + if not isdir(budir): + os.mkdir(budir) + os.rename(path, "{}/{}.{}{}".format(budir, fbase, ts_str, fext)) + + def save_default_snapshot(self): + self.save_snapshot(self.default_snapshot_fpath) + + def load_default_snapshot(self): + if isfile(self.default_snapshot_fpath): + return self.load_snapshot(self.default_snapshot_fpath) + + def save_last_state_snapshot(self): + self.save_snapshot(self.last_state_snapshot_fpath) + + def load_last_state_snapshot(self): + if isfile(self.last_state_snapshot_fpath): + return self.load_snapshot(self.last_state_snapshot_fpath) + + def delete_last_state_snapshot(self): + try: + os.remove(self.last_state_snapshot_fpath) + except: + pass + + # ---------------------------------------------------------------------------- + # ZS3 management + # ---------------------------------------------------------------------------- + + def get_zs3_title(self, zs3_id=None): + """Get ZS3 title + + zs3_id : ZS3 ID (default: Use last loaded zs3) + Returns : Title as string + """ + + try: + if zs3_id is None: + zs3_id = self.last_zs3_id + return self.zs3[zs3_id]["title"] + except: + return zs3_id + + def set_zs3_title(self, zs3_id, title): + self.zs3[zs3_id]["title"] = title + + def toggle_zs3_chain_restore_flag(self, zs3_id, chain_id): + zs3_state = self.zs3[zs3_id] + if chain_id == "mixer": + tstate = zs3_state["mixer"] + else: + tstate = zs3_state["chains"][chain_id] + try: + tstate["restore"] = not tstate["restore"] + except: + tstate["restore"] = False + + def load_zs3(self, zs3_id, autoconnect=True): + """Restore a ZS3 + + zs3_id : ID of ZS3 to restore or zs3 dict + Returns : True on success + """ + + if isinstance(zs3_id, str): + # Try loading exact match + try: + zs3_state = self.zs3[zs3_id] + except: + # else ignore MIDI channel => try loading "program change" match + try: + zs3_id = f"*/{zs3_id.split('/')[1]}" + zs3_state = self.zs3[zs3_id] + except: + logging.info(f"Not found ZS3 matching '{zs3_id}'") + return False + else: + try: + zs3_state = zs3_id + zs3_id = self.last_zs3_id + if zs3_id is None: + zs3_id = "zs3-0" + except: + zs3_id = "zs3-0" + + restored_chains = [] + restored_cc_mapping = [] + mute_pause = False + if "chains" in zs3_state: + self.set_busy_details("restoring chains state") + for chain_id, chain_state in zs3_state["chains"].items(): + chain_id = int(chain_id) + + try: + restore_flag = chain_state["restore"] + except: + restore_flag = True + + if not restore_flag: + continue + + chain = self.chain_manager.get_chain(chain_id) + if chain: + restored_chains.append(chain_id) + else: + continue + + try: + if zs3_state["mixer"][f"chan_{chain.mixer_chan:02}"]["mute"]: + # Avoid subsequent config changes from being heard on muted chains + self.zynmixer.set_mute(chain.mixer_chan, 1) + mute_pause = True + except: + pass + + if "midi_chan" in chain_state: + if chain.midi_chan is not None and chain.midi_chan != chain_state['midi_chan']: + self.chain_manager.set_midi_chan(chain_id, chain_state['midi_chan']) + + if chain.zmop_index is not None: + if "note_low" in chain_state: + lib_zyncore.zmop_set_note_low(chain.zmop_index, chain_state["note_low"]) + else: + lib_zyncore.zmop_set_note_low(chain.zmop_index, 0) + if "note_high" in chain_state: + lib_zyncore.zmop_set_note_high(chain.zmop_index, chain_state["note_high"]) + else: + lib_zyncore.zmop_set_note_high(chain.zmop_index, 127) + if "transpose_octave" in chain_state: + lib_zyncore.zmop_set_transpose_octave(chain.zmop_index, chain_state["transpose_octave"]) + else: + lib_zyncore.zmop_set_transpose_octave(chain.zmop_index, 0) + 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) + if "midi_in" in chain_state: + chain.midi_in = chain_state["midi_in"] + if "midi_out" in chain_state: + chain.midi_out = chain_state["midi_out"] + if "midi_thru" in chain_state: + chain.midi_thru = chain_state["midi_thru"] + if "audio_in" in chain_state: + chain.audio_in = chain_state["audio_in"] + chain.audio_out = [] + if "audio_out" in chain_state: + for out in chain_state["audio_out"]: + if isinstance(out, list): + chain.audio_out.append(f"{self.chain_manager.processors[out[0]].jackname}:{out[1]}") + 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: + chain.audio_thru = chain_state["audio_thru"] + chain.rebuild_graph() + if "midi_cc" in chain_state: + for cc, cfg in chain_state["midi_cc"].items(): + for proc_id, symbol in cfg: + if proc_id in self.chain_manager.processors: + restored_cc_mapping.append((proc_id, int(cc), symbol)) + if mute_pause: + # Wait for soft mutes to apply before changing settings + sleep(self.jack_period) + + if "processors" in zs3_state: + for proc_id, proc_state in zs3_state["processors"].items(): + try: + processor = self.chain_manager.processors[int(proc_id)] + if processor.chain_id in restored_chains: + self.set_busy_details(f"restoring {processor.get_basepath()} state") + processor.set_state(proc_state) + except Exception as e: + logging.error(f"Failed to restore processor {proc_id} state => {e}") + + for cc_map in restored_cc_mapping: + processor = self.chain_manager.processors[cc_map[0]] + try: + zctrl = processor.controllers_dict[cc_map[2]] + self.chain_manager.add_midi_learn(processor.midi_chan, cc_map[1], zctrl) + except: + logging.warning(f"Failed to restore MIDI learning {cc_map[1]} => {cc_map[2]}") + + if "active_chain" in zs3_state: + self.chain_manager.set_active_chain_by_id(zs3_state["active_chain"]) + + if "mixer" in zs3_state: + try: + restore_flag = zs3_state["mixer"]["restore"] + except: + restore_flag = True + if restore_flag: + self.set_busy_details("restoring mixer state") + self.zynmixer.set_state(zs3_state["mixer"]) + + if "midi_capture" in zs3_state: + self.set_busy_details("restoring midi capture state") + self.set_midi_capture_state(zs3_state['midi_capture']) + + if "global" in zs3_state: + if "midi_transpose" in zs3_state["global"]: + lib_zyncore.set_global_transpose(int(zs3_state["global"]["midi_transpose"])) + if "zctrl_x" in zs3_state["global"]: + try: + processor = self.chain_manager.processors[zs3_state["global"]["zctrl_x"][0]] + self.zctrl_x = processor.controllers_dict[zs3_state["global"]["zctrl_x"][1]] + except: + self.zctrl_x = None + if "zctrl_y" in zs3_state["global"]: + try: + processor = self.chain_manager.processors[zs3_state["global"]["zctrl_y"][0]] + self.zctrl_y = processor.controllers_dict[zs3_state["global"]["zctrl_y"][1]] + except: + self.zctrl_y = None + if "zynaptik" in zs3_state["global"]: + try: + zynaptik_config = zs3_state["global"]["zynaptik"] + lib_zyncore.zynaptik_cvin_set_volts_octave(ctypes.c_float(zynaptik_config["cvin_volts_octave"])) + lib_zyncore.zynaptik_cvin_set_note0(zynaptik_config["cvin_note0"]) + lib_zyncore.zynaptik_cvout_set_volts_octave(ctypes.c_float(zynaptik_config["cvout_volts_octave"])) + lib_zyncore.zynaptik_cvout_set_note0(zynaptik_config["cvout_note0"]) + except: + pass + + if zs3_id != 'zs3-0': + self.last_zs3_id = zs3_id + #self.zs3['zs3-0'] = self.zs3[zs3_id].copy() + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_LOAD_ZS3, zs3_id=zs3_id) + + if autoconnect: + zynautoconnect.request_midi_connect(True) + zynautoconnect.request_audio_connect(True) + return True + + def save_zs3(self, zs3_id=None, title=None): + """Store current state as ZS3 + + zs3_id : ID of zs3 to save / overwrite (Default: Create new id) + title : ZS3 title (Default: Create new title) + """ + + if zs3_id is None: + # Get next id and name + used_ids = [] + for zid in self.zs3: + if zid.startswith("zs3-"): + try: + used_ids.append(int(zid.split('-')[1])) + except: + pass + used_ids.sort() + # Get next free zs3 id + for index in range(1, len(used_ids) + 2): + if index not in used_ids: + zs3_id = f"zs3-{index}" + break + + if title is None: + title = self.midi_learn_pc + + if not title: + if zs3_id in self.zs3: + title = self.zs3[zs3_id]['title'] + else: + title = zs3_id.upper() + + # Initialise zs3 + self.zs3[zs3_id] = { + "title": title, + "active_chain": self.chain_manager.active_chain_id, + "global": {} + } + chain_states = {} + for chain_id, chain in self.chain_manager.chains.items(): + chain_state = { + "midi_chan": chain.midi_chan + } + if chain.is_midi(): + note_low = lib_zyncore.zmop_get_note_low(chain.zmop_index) + if note_low > 0: + chain_state["note_low"] = note_low + 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) + if transpose_octave: + chain_state["transpose_octave"] = transpose_octave + transpose_semitone = lib_zyncore.zmop_get_transpose_semitone(chain.zmop_index) + if transpose_semitone: + chain_state["transpose_semitone"] = transpose_semitone + if chain.midi_in: + chain_state["midi_in"] = chain.midi_in.copy() + if chain.midi_out: + chain_state["midi_out"] = chain.midi_out.copy() + if chain.midi_thru: + chain_state["midi_thru"] = chain.midi_thru + chain_state["audio_in"] = chain.audio_in.copy() + chain_state["audio_out"] = [] + for out in chain.audio_out: + if out in zynautoconnect.get_sidechain_portnames(): + client_name, port_name = out.split(":", 1) + for i, proc in self.chain_manager.processors.items(): + if proc.jackname == client_name: + out = [i, port_name] + break + chain_state["audio_out"].append(out) + if chain.audio_thru: + chain_state["audio_thru"] = chain.audio_thru + # Add chain MIDI mapping + for key, zctrls in self.chain_manager.chain_midi_cc_binding.items(): + if chain_id == (key >> 16) & 0xff: + cc = (key >> 8) & 0x7f + # TODO: Do not save default engine mapping + if "midi_cc" not in chain_state: + chain_state["midi_cc"] = {} + chain_state["midi_cc"][cc] = [] + for zctrl in zctrls: + chain_state["midi_cc"][cc].append([zctrl.processor.id, zctrl.symbol]) + if chain_state: + chain_states[chain_id] = chain_state + if chain_states: + self.zs3[zs3_id]["chains"] = chain_states + + # Add processors + processor_states = {} + for id, processor in self.chain_manager.processors.items(): + processor_state = { + "bank_info": processor.bank_info, + "preset_info": processor.preset_info, + "controllers": {} + } + # Add controllers + for symbol, zctrl in processor.controllers_dict.items(): + processor_state["controllers"][symbol] = zctrl.get_state() + processor_states[id] = processor_state + if processor_states: + self.zs3[zs3_id]["processors"] = processor_states + + # Add mixer state + mixer_state = self.zynmixer.get_state(False) + if mixer_state: + self.zs3[zs3_id]["mixer"] = mixer_state + + # Add MIDI capture state + mcstate = self.get_midi_capture_state() + if mcstate: + self.zs3[zs3_id]["midi_capture"] = mcstate + + # Add global parameters + self.zs3[zs3_id]["global"]["midi_transpose"] = lib_zyncore.get_global_transpose() + try: + processor_id = self.zctrl_x.processor.id + symbol = self.zctrl_x.symbol + self.zs3[zs3_id]["global"]["zctrl_x"] = [processor_id, symbol] + except: + pass + try: + processor_id = self.zctrl_y.processor.id + symbol = self.zctrl_y.symbol + self.zs3[zs3_id]["global"]["zctrl_y"] = [processor_id, symbol] + except: + pass + try: + if callable(lib_zyncore.init_zynaptik): + lib_zyncore.zynaptik_cvin_get_volts_octave.restype = ctypes.c_float + lib_zyncore.zynaptik_cvout_get_volts_octave.restype = ctypes.c_float + zynaptik_config = { + "cvin_volts_octave": lib_zyncore.zynaptik_cvin_get_volts_octave(), + "cvin_note0": lib_zyncore.zynaptik_cvin_get_note0(), + "cvout_volts_octave": lib_zyncore.zynaptik_cvout_get_volts_octave(), + "cvout_note0": lib_zyncore.zynaptik_cvout_get_note0() + } + self.zs3[zs3_id]["global"]["zynaptik"] = zynaptik_config + except: + pass + + if zs3_id != 'zs3-0': + self.last_zs3_id = zs3_id + # Jofemodo: this has not sense from my POV + #self.zs3['zs3-0'] = self.zs3[zs3_id].copy() + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_SAVE_ZS3, zs3_id=zs3_id) + + def delete_zs3(self, zs3_id): + """Remove a ZS3 + + zs3_id : Index of ZS3 to remove + """ + try: + del (self.zs3[zs3_id]) + if self.last_zs3_id == zs3_id: + self.last_zs3_id = None + + except: + logging.info("Tried to remove non-existant ZS3") + + def reset_zs3(self): + """Remove all ZS3""" + + # ZS3 list (subsnapshots) + self.zs3 = {} + + def sanitize_zs3_from_json(self, zs3_state): + """Fix chain & processor ID keys in ZS3 data decoded from JSON""" + + # TODO: Temporal compatibility fix with older vangelis => To remove!! + if 'last_zs3' in zs3_state: + if self.last_zs3_id is None: + self.last_zs3_id = zs3_state['last_zs3'] + del zs3_state['last_zs3'] + + for zs3_key, state in zs3_state.items(): + if 'chains' in state: + fixed_chains = {} + for chain_id, chain_state in state['chains'].items(): + try: + chain_id = int(chain_id) + except: + logging.error( + f"Chain in ZS3 {zs3_key} has an invalid ID: {chain_id}") + continue + fixed_chains[chain_id] = chain_state + state['chains'] = fixed_chains + if 'processors' in state: + fixed_processors = {} + for processor_id, processor_state in state['processors'].items(): + try: + processor_id = int(processor_id) + except: + logging.error( + f"Processor in ZS3 {zs3_key} has an invalid ID: {processor_id}") + continue + fixed_processors[processor_id] = processor_state + state['processors'] = fixed_processors + + return zs3_state + + def purge_zs3(self): + """Remove non-existant chains and processors from ZS3 state""" + + for key, state in self.zs3.items(): + if state["active_chain"] not in self.chain_manager.chains: + state["active_chain"] = self.chain_manager.active_chain_id + if "processors" in state: + for processor_id in list(state["processors"]): + if int(processor_id) not in self.chain_manager.processors: + logging.debug( + f"Purging processor {processor_id} from ZS3 {key}") + del state["processors"][processor_id] + if "chains" in state: + for chain_id in list(state["chains"]): + if int(chain_id) not in self.chain_manager.chains: + logging.debug( + f"Purging chain {chain_id} from ZS3 {key}") + del state["chains"][chain_id] + + def get_last_zs3_index(self): + return list(self.zs3.keys()).index(self.last_zs3_id) + + def load_zs3_by_index(self, index): + try: + zs3_id = list(self.zs3.keys())[index] + except: + logging.warning(f"Can't find ZS3 with index {index}") + return + return self.load_zs3(zs3_id) + + def load_next_zs3(self): + try: + index = self.get_last_zs3_index() + 1 + except: + return False + return self.load_zs3_by_index(index) + + def load_prev_zs3(self): + try: + index = self.get_last_zs3_index() - 1 + except: + return False + return self.load_zs3_by_index(index) + + # ------------------------------------------------------------------ + # Jackd Info + # ------------------------------------------------------------------ + + def get_jackd_samplerate(self): + """Get the samplerate that jackd is running""" + + return zynautoconnect.get_jackd_samplerate() + + def get_jackd_blocksize(self): + """Get the block size used by jackd""" + + return zynautoconnect.get_jackd_blocksize() + + # ------------------------------------------------------------------ + # All Notes/Sounds Off => PANIC! + # ------------------------------------------------------------------ + + def all_sounds_off(self): + logging.info("All Sounds Off!") + for chan in range(16): + lib_zyncore.ui_send_ccontrol_change(chan, 120, 0) + + def all_notes_off(self): + logging.info("All Notes Off!") + self.zynseq.libseq.stop() + for chan in range(16): + lib_zyncore.ui_send_ccontrol_change(chan, 123, 0) + try: + lib_zyncore.zynaptik_all_gates_off() + except: + pass + zynsigman.send_queued(zynsigman.S_STATE_MAN, self.SS_ALL_NOTES_OFF, chan=None) + + def raw_all_notes_off(self): + logging.info("Raw All Notes Off!") + lib_zyncore.ui_send_all_notes_off() + + def all_sounds_off_chan(self, chan): + logging.info(f"All Sounds Off for channel {chan}!") + lib_zyncore.ui_send_ccontrol_change(chan, 120, 0) + + def all_notes_off_chan(self, chan): + logging.info(f"All Notes Off for channel {chan}!") + lib_zyncore.ui_send_ccontrol_change(chan, 123, 0) + zynsigman.send_queued(zynsigman.S_STATE_MAN, self.SS_ALL_NOTES_OFF, chan=chan) + + def raw_all_notes_off_chan(self, chan): + logging.info(f"Raw All Notes Off for channel {chan}!") + lib_zyncore.ui_send_all_notes_off_chan(chan) + + # ------------------------------------------------------------------ + # MPE initialization + # ------------------------------------------------------------------ + + def init_mpe_zones(self, lower_n_chans, upper_n_chans): + # Configure Lower Zone + if not isinstance(lower_n_chans, int) or lower_n_chans < 0 or lower_n_chans > 0xF: + logging.error( + f"Can't initialize MPE Lower Zone. Incorrect num of channels ({lower_n_chans})") + else: + lib_zyncore.ctrlfb_send_ccontrol_change(0x0, 0x79, 0x0) + lib_zyncore.ctrlfb_send_ccontrol_change(0x0, 0x64, 0x6) + lib_zyncore.ctrlfb_send_ccontrol_change(0x0, 0x65, 0x0) + lib_zyncore.ctrlfb_send_ccontrol_change(0x0, 0x06, lower_n_chans) + + # Configure Upper Zone + if not isinstance(upper_n_chans, int) or upper_n_chans < 0 or upper_n_chans > 0xF: + logging.error( + f"Can't initialize MPE Upper Zone. Incorrect num of channels ({upper_n_chans})") + else: + lib_zyncore.ctrlfb_send_ccontrol_change(0xF, 0x79, 0x0) + lib_zyncore.ctrlfb_send_ccontrol_change(0xF, 0x64, 0x6) + lib_zyncore.ctrlfb_send_ccontrol_change(0xF, 0x65, 0x0) + lib_zyncore.ctrlfb_send_ccontrol_change(0xF, 0x06, upper_n_chans) + + # ---------------------------------------------------------------------------- + # MIDI Capture State + # ---------------------------------------------------------------------------- + + def get_midi_capture_state(self): + """Get state related to midi input (capture): flags, chain routing, etc. + + Returns : dictionary with state + """ + mcstate = {} + ctrldev_state_drivers = self.ctrldev_manager.get_state_drivers() + for izmip in range(NUM_MIDI_DEVS_IN): + if zynautoconnect.devices_in[izmip] is None: + continue + try: + uid = zynautoconnect.devices_in[izmip].aliases[0] + except: + logging.error(f"No aliases for device connected to {izmip} => Skipping!") + continue + routed_chains = [] + for ch in range(MAX_NUM_ZMOPS): + if lib_zyncore.zmop_get_route_from(ch, izmip): + routed_chains.append(ch) + mcstate[uid] = { + "zmip_input_mode": bool(lib_zyncore.zmip_get_flag_active_chain(izmip)), + "disable_ctrldev": self.ctrldev_manager.get_disabled_driver(uid), + "ctrldev_driver": self.ctrldev_manager.get_driver_class_name(izmip), + "routed_chains": routed_chains + } + # Ctrldev driver state + if uid in ctrldev_state_drivers: + mcstate[uid]["ctrldev_state"] = ctrldev_state_drivers[uid] + # Aubio state + if uid == "AUBIO:in": + mcstate[uid]["audio_in"] = self.aubio_in + # Add global / absolute MIDI mapping + for key, zctrls in self.chain_manager.absolute_midi_cc_binding.items(): + if izmip == (key >> 24) & 0xff: + chan_cc = (key >> 8) & 0x7f7f + if "midi_cc" not in mcstate[uid]: + mcstate[uid]["midi_cc"] = {} + mcstate[uid]["midi_cc"][chan_cc] = [] + for zctrl in zctrls: + mcstate[uid]["midi_cc"][chan_cc].append([zctrl.processor.id, zctrl.symbol]) + + return mcstate + + def set_midi_capture_state(self, mcstate=None): + """Set midi input (capture) state: flags, chain routing, etc. + + mcstate : dictionary with state. None for reset state to defaults. + """ + if mcstate: + ctrldev_state_drivers = {} + for uid, state in mcstate.items(): + #logging.debug(f"MCSTATE {uid} => {state}") + izmip = zynautoconnect.get_midi_in_devid_by_uid(uid, zynthian_gui_config.midi_usb_by_port) + if izmip is None: + continue + try: + lib_zyncore.zmip_set_flag_active_chain(izmip, bool(state["zmip_input_mode"])) + except: + pass + try: + self.aubio_in = state["audio_in"] + except: + pass + zynautoconnect.update_midi_in_dev_mode(izmip) + try: + #TODO: Use ctrldev_driver=None to disable driver + if state["disable_ctrldev"]: + self.ctrldev_manager.unload_driver(izmip, True) + else: + self.ctrldev_manager.load_driver(izmip, state["ctrldev_driver"]) + except: + pass + try: + ctrldev_state_drivers[uid] = state["ctrldev_state"] + except: + pass + # Route chain zmops + try: + routed_chains = state["routed_chains"] + for ch in range(0, 16): + lib_zyncore.zmop_set_route_from(ch, izmip, ch in routed_chains) + except: + pass + + if "midi_cc" in state: + for chan_cc, cfg in state["midi_cc"].items(): + for proc_id, symbol in cfg: + try: + processor = self.chain_manager.processors[proc_id] + except: + continue + try: + zctrl = processor.controllers_dict[symbol] + except: + logging.warning(f"Can't MIDI learn '{symbol}'. Controller not found in processor {proc_id}.") + continue + chan = (chan_cc >> 8) & 0xff + cc = chan_cc & 0x7f + self.chain_manager.add_midi_learn(chan, cc, zctrl, izmip) + + self.ctrldev_manager.set_state_drivers(ctrldev_state_drivers) + + else: + zynautoconnect.reset_midi_in_dev_all() + + # ------------------------------------------------------------------ + # MIDI learning + # ------------------------------------------------------------------ + + def set_midi_learn(self, state): + """Enable / disable MIDI learn in MIDI router + + state : True to enable MIDI learn + """ + + lib_zyncore.set_midi_learning_mode(state) + self.midi_learn_state = state + + def enable_learn_cc(self, zctrl): + """Enable MIDI CC learning + + zctrl : zctrl to learn to + """ + + self.disable_learn_pc() + self.midi_learn_zctrl = zctrl + self.midi_learn_zctrl.midi_cc_mode_reset() + self.set_midi_learn(True) + + def disable_learn_cc(self): + """Disables MIDI CC learning""" + + self.midi_learn_zctrl = None + self.set_midi_learn(False) + + def get_midi_learn_zctrl(self): + try: + return self.midi_learn_zctrl + except: + return None + + def enable_learn_pc(self, zs3_name=""): + self.disable_learn_cc() + self.midi_learn_pc = zs3_name + self.set_midi_learn(True) + + def disable_learn_pc(self): + self.midi_learn_pc = None + self.set_midi_learn(False) + + # --------------------------------------------------------------------------- + # MIDI Router Init & Config + # --------------------------------------------------------------------------- + + def init_midi(self): + """Initialise MIDI configuration""" + try: + # Set 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) + # Set MIDI System Messages flag + 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) + except Exception as e: + logging.error(f"ERROR initializing MIDI : {e}") + + def reload_midi_config(self): + """Reload MIDI configuration from saved state""" + + zynconf.load_config() + midi_profile_fpath = zynconf.get_midi_config_fpath() + if midi_profile_fpath: + zynconf.load_config(True, midi_profile_fpath) + zynthian_gui_config.set_midi_config() + self.init_midi() + self.init_midi_services() + zynautoconnect.request_midi_connect() + + def init_midi_services(self): + """Start/Stop MIDI aux. services""" + + self.default_rtpmidi() + self.default_qmidinet() + self.default_touchosc() + self.default_bluetooth() + self.default_aubionotes() + + # ------------------------------------------------------------------- + # MIDI transport & clock settings + # ------------------------------------------------------------------- + + def get_transport_clock_source(self): + val = self.zynseq.libseq.getClockSource() + if val == 5: + return 3 + elif val == 2: + return 2 + elif self.zynseq.libseq.getMidiClockOutput(): + return 1 + else: + return 0 + + def set_transport_clock_source(self, val=None, save_config=False): + if val is None: + val = zynthian_gui_config.transport_clock_source + + if val == 2: + self.zynseq.libseq.setClockSource(2) + elif val == 3: + self.zynseq.libseq.setClockSource(1 | 4) + else: + self.zynseq.libseq.setClockSource(1) + + self.zynseq.libseq.setMidiClockOutput(val == 1) + + if val > 0: + lib_zyncore.set_midi_system_events(1) + else: + lib_zyncore.set_midi_system_events(zynthian_gui_config.midi_sys_enabled) + + # Save config + if save_config: + zynthian_gui_config.transport_clock_source = val + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_TRANSPORT_CLOCK_SOURCE": str(int(val)) + }) + + # ------------------------------------------------------------------- + # MIDI profile + # ------------------------------------------------------------------- + + def get_midi_profile_state(self): + """Get MIDI profile state as an ordered dictionary""" + + midi_profile_state = OrderedDict() + for key in os.environ.keys(): + if key.startswith("ZYNTHIAN_MIDI_"): + midi_profile_state[key[14:]] = os.environ[key] + midi_profile_state["port_names"] = zynautoconnect.get_port_friendly_names() + return midi_profile_state + + def set_midi_profile_state(self, state): + """Set MIDI profile from state + + state : MIDI profile state dictionary + """ + + if state is not None: + for key in state: + if key == "port_names": + zynautoconnect.set_midi_port_names(state[key]) + # Drop Master Channel config, as it's global + elif not key.startswith("MASTER_"): + os.environ["ZYNTHIAN_MIDI_" + key] = state[key] + zynthian_gui_config.set_midi_config() + self.init_midi() + self.init_midi_services() + self.set_transport_clock_source() + zynautoconnect.request_midi_connect() + return True + + def reset_midi_profile(self): + """Clear MIDI profiles""" + + self.reload_midi_config() + + # --------------------------------------------------------------------------- + # Global Audio Player + # --------------------------------------------------------------------------- + + def create_audio_player(self): + if not self.audio_player: + try: + 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( + f"Can't create global Audio Player instance => {e}\n{traceback.format_exc()}") + + def destroy_audio_player(self): + if self.audio_player: + self.audio_player.engine.remove_processor(self.audio_player) + self.audio_player = None + self.status_audio_player = False + + def start_audio_player(self): + if (self.audio_player.preset_name and os.path.exists(self.audio_player.preset_info[0])) or zynaudioplayer.get_filename(self.audio_player.handle): + zynaudioplayer.start_playback(self.audio_player.handle) + else: + self.audio_player.engine.load_latest(self.audio_player) + zynaudioplayer.start_playback(self.audio_player.handle) + + def stop_audio_player(self, reset_pos=False): + zynaudioplayer.stop_playback(self.audio_player.handle) + if reset_pos: + zynaudioplayer.set_position(self.audio_player.handle, 0.0) + + def toggle_audio_player(self): + """Toggle playback of global audio player""" + + if zynaudioplayer.get_playback_state(self.audio_player.handle): + self.stop_audio_player() + else: + self.start_audio_player() + + # --------------------------------------------------------------------------- + # Global MIDI Player + # --------------------------------------------------------------------------- + + def get_new_midi_record_fpath(self): + exdirs = zynthian_gui_config.get_external_storage_dirs(ex_data_dir) + if exdirs: + path = exdirs[0] + else: + path = capture_dir_sdc + filename = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + if self.last_snapshot_fpath and len(self.last_snapshot_fpath) > 4: + filename += "_" + os.path.basename(self.last_snapshot_fpath[:-4]) + + filename = filename.replace( + "/", ";").replace(">", ";").replace(" ; ", ";") + # Append index to file to make unique + index = 1 + while "{}.{:03d}.mid".format(filename, index) in os.listdir(path): + index += 1 + return "{}/{}.{:03d}.mid".format(path, filename, index) + + 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) + return True + else: + return False + + def stop_midi_record(self): + result = False + if libsmf.isRecording(): + logging.info("STOPPING MIDI RECORDING ...") + libsmf.stopRecording() + + fpath = self.get_new_midi_record_fpath() + if zynsmf.save(self.smf_recorder, fpath): + self.sync = True + self.last_midi_file = fpath + result = True + + zynsigman.send(zynsigman.S_STATE_MAN, self.SS_MIDI_RECORDER_STATE, state=False) + + return result + + def toggle_midi_record(self): + if libsmf.isRecording(): + self.stop_midi_record() + else: + self.start_midi_record() + + def set_tempo(self, tempo): + self.zynseq.set_tempo(tempo) + zynaudioplayer.set_tempo(tempo) + + def start_midi_playback(self, fpath): + self.stop_midi_playback() + if fpath is None: + if self.last_midi_file: + fpath = self.last_midi_file + else: + # Get latest file + latest_mtime = 0 + for dir in [capture_dir_sdc] + zynthian_gui_config.get_external_storage_dirs(ex_data_dir): + for fn in glob(f"{dir}//*.mid"): + mtime = os.path.getmtime(fn) + if mtime > latest_mtime: + fpath = fn + latest_mtime = mtime + + if fpath is None: + logging.info("No track to play!") + return self.status_midi_player + + try: + zynsmf.load(self.smf_player, fpath) + tempo = libsmf.getTempo(self.smf_player, 0) + logging.info(f"STARTING MIDI PLAY '{fpath}' => {tempo}BPM") + self.set_tempo(tempo) + libsmf.startPlayback() + 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) + self.status_midi_player = False + self.last_midi_file = fpath + # self.zynseq.libseq.transportLocate(0) + except Exception as e: + logging.error(f"ERROR STARTING MIDI PLAY: {e}") + return False + return self.status_midi_player + + 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) + return self.status_midi_player + + def toggle_midi_playback(self, fname=None): + if libsmf.getPlayState() == zynsmf.PLAY_STATE_STOPPED: + return self.start_midi_playback(fname) + else: + return self.stop_midi_playback() + + # --------------------------------------------------------------------------- + # Core Network Services + # --------------------------------------------------------------------------- + + def start_vncserver(self, save_config=True): + # Start VNC for Zynthian-UI + self.start_busy("start_vncserver", "starting VNC") + + if not zynconf.is_service_active("vncserver0"): + try: + logging.info("STARTING VNC-UI SERVICE") + self.set_busy_details("starting VNC-UI service") + check_output("systemctl start novnc0", shell=True) + zynthian_gui_config.vncserver_enabled = 1 + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STARTING VNC-UI", e) + sleep(2.0) + + # Start VNC for Engine's native GUIs + if not zynconf.is_service_active("vncserver1"): + # Save state and stop engines + if self.chain_manager.get_chain_count() > 0: + self.save_last_state_snapshot() + restore_state = True + else: + restore_state = False + # Start VNC for Engines + try: + logging.info("STARTING VNC-ENGINES SERVICE") + self.set_busy_details("starting VNC-ENGINES service") + check_output("systemctl start novnc1", shell=True) + zynthian_gui_config.vncserver_enabled = 1 + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STARTING VNC-ENGINES", e) + sleep(2.0) + # Restore state + if restore_state: + self.load_last_state_snapshot() + + # Update Config + if save_config: + zynconf.save_config({ + "ZYNTHIAN_VNCSERVER_ENABLED": str(zynthian_gui_config.vncserver_enabled) + }) + + self.end_busy("start_vncserver") + + def stop_vncserver(self, save_config=True): + self.start_busy("stop_vncserver", "stopping VNC") + + # Stop VNC for Zynthian-UI + if zynconf.is_service_active("vncserver0"): + try: + logging.info("STOPPING VNC-UI SERVICE") + self.set_busy_details("stopping VNC-UI service") + check_output("systemctl stop vncserver0", shell=True) + zynthian_gui_config.vncserver_enabled = 0 + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STOPPING VNC-UI", e) + sleep(2.0) + + # Start VNC for Engine's native GUIs + if zynconf.is_service_active("vncserver1"): + # Save state and stop engines + if len(self.chain_manager.processors) > 0: + self.save_last_state_snapshot() + restore_state = True + else: + restore_state = False + # Stop VNC for engiens + try: + logging.info("STOPPING VNC-ENGINES SERVICE") + self.set_busy_details("stopping VNC-ENGINES service") + check_output("systemctl stop vncserver1", shell=True) + zynthian_gui_config.vncserver_enabled = 0 + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STOPPING VNC-ENGINES", e) + sleep(2.0) + # Restore state + if restore_state: + self.load_last_state_snapshot() + + # Update Config + if save_config: + zynconf.save_config({ + "ZYNTHIAN_VNCSERVER_ENABLED": str(zynthian_gui_config.vncserver_enabled) + }) + + self.end_busy("stop_vncserver") + + # Start/Stop VNC Server depending on configuration + def default_vncserver(self): + if zynthian_gui_config.vncserver_enabled: + self.start_vncserver(False) + else: + self.stop_vncserver(False) + + # --------------------------------------------------------------------------- + # MIDI Network Services + # --------------------------------------------------------------------------- + + # Start/Stop NetUMP-MIDI-2.0 depending on configuration + def default_netump(self): + if zynthian_gui_config.midi_netump_enabled: + self.start_netump(False) + else: + self.stop_netump(False) + + def start_netump(self, save_config=True, wait=0): + service = "jacknetumpd" + if zynconf.is_service_active(service): + zynthian_gui_config.midi_netump_enabled = 1 + return + self.start_busy("start_netump", "starting NetUMP MIDI 2.0") + logging.info("STARTING NetUMP MIDI 2.0") + try: + check_output(f"systemctl start {service}", shell=True) + zynthian_gui_config.midi_netump_enabled = 1 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_NETUMP_ENABLED": str(zynthian_gui_config.midi_netump_enabled) + }) + # Call autoconnect after a little time + sleep(wait) + zynautoconnect.request_midi_connect(True) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STARTING NetUMP MIDI 2.0", e) + sleep(2.0) + + self.end_busy("start_netump") + + def stop_netump(self, save_config=True, wait=0): + service = "jacknetumpd" + if not zynconf.is_service_active(service): + zynthian_gui_config.midi_netump_enabled = 0 + return + self.start_busy("stop_netump", "stopping NetUMP MIDI 2.0") + logging.info("STOPPING NetUMP MIDI 2.0") + try: + check_output(f"systemctl stop {service}", shell=True) + zynthian_gui_config.midi_netump_enabled = 0 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_NETUMP_ENABLED": str(zynthian_gui_config.midi_netump_enabled) + }) + sleep(wait) + + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STOPPING NetUMP MIDI 2.0", e) + sleep(2.0) + + self.end_busy("stop_netump") + + # Start/Stop RTP-MIDI depending on configuration + def default_rtpmidi(self): + if zynthian_gui_config.midi_rtpmidi_enabled: + self.start_rtpmidi(False) + else: + self.stop_rtpmidi(False) + + def start_rtpmidi(self, save_config=True, wait=0): + service = "jackrtpmidid" + if zynconf.is_service_active(service): + zynthian_gui_config.midi_rtpmidi_enabled = 1 + return + self.start_busy("start_rtpmidi", "starting RTP-MIDI") + logging.info("STARTING RTP-MIDI") + try: + check_output(f"systemctl start {service}", shell=True) + zynthian_gui_config.midi_rtpmidi_enabled = 1 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_RTPMIDI_ENABLED": str(zynthian_gui_config.midi_rtpmidi_enabled) + }) + # Call autoconnect after a little time + sleep(wait) + zynautoconnect.request_midi_connect(True) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STARTING RTP-MIDI", e) + sleep(2.0) + + self.end_busy("start_rtpmidi") + + def stop_rtpmidi(self, save_config=True, wait=0): + service = "jackrtpmidid" + if not zynconf.is_service_active(service): + zynthian_gui_config.midi_rtpmidi_enabled = 0 + return + self.start_busy("stop_rtpmidi", "stopping RTP-MIDI") + logging.info("STOPPING RTP-MIDI") + try: + check_output(f"systemctl stop {service}", shell=True) + zynthian_gui_config.midi_rtpmidi_enabled = 0 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_RTPMIDI_ENABLED": str(zynthian_gui_config.midi_rtpmidi_enabled) + }) + sleep(wait) + + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STOPPING RTP-MIDI", e) + sleep(2.0) + + self.end_busy("stop_rtpmidi") + + def start_qmidinet(self, save_config=True, wait=0): + service = "qmidinet" + if zynconf.is_service_active(service): + zynthian_gui_config.midi_network_enabled = 1 + return + self.start_busy("start_qmidinet", "starting QMidiNet") + logging.info("STARTING QMidiNet") + try: + check_output(f"systemctl start {service}", shell=True) + zynthian_gui_config.midi_network_enabled = 1 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_NETWORK_ENABLED": str(zynthian_gui_config.midi_network_enabled) + }) + # Call autoconnect after a little time + sleep(wait) + zynautoconnect.request_midi_connect(True) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STARTING QMidiNet", e) + sleep(2.0) + + self.end_busy("start_qmidinet") + + def stop_qmidinet(self, save_config=True, wait=0): + service = "qmidinet" + if not zynconf.is_service_active(service): + zynthian_gui_config.midi_network_enabled = 0 + return + self.start_busy("stop_qmidinet", "stopping QMidiNet") + logging.info("STOPPING QMidiNet") + try: + check_output(f"systemctl stop {service}", shell=True) + zynthian_gui_config.midi_network_enabled = 0 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_NETWORK_ENABLED": str(zynthian_gui_config.midi_network_enabled) + }) + sleep(wait) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STOPPING QMidiNet", e) + sleep(2.0) + + self.end_busy("stop_qmidinet") + + # Start/Stop QMidiNet depending on configuration + def default_qmidinet(self): + if zynthian_gui_config.midi_network_enabled: + self.start_qmidinet(False) + else: + self.stop_qmidinet(False) + + def start_touchosc2midi(self, save_config=True, wait=0): + service = "touchosc2midi" + if zynconf.is_service_active(service): + zynthian_gui_config.midi_touchosc_enabled = 1 + return + self.start_busy("start_touchosc2midi", "starting Touch-OSC") + logging.info("STARTING touchosc2midi") + try: + check_output(f"systemctl start {service}", shell=True) + zynthian_gui_config.midi_touchosc_enabled = 1 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_TOUCHOSC_ENABLED": str(zynthian_gui_config.midi_touchosc_enabled) + }) + # Call autoconnect after a little time + zynautoconnect.request_midi_connect(True) + sleep(wait) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STARTING Touch-OSC", e) + sleep(2.0) + + self.end_busy("start_touchosc2midi") + + def stop_touchosc2midi(self, save_config=True, wait=0): + service = "touchosc2midi" + if not zynconf.is_service_active(service): + zynthian_gui_config.midi_touchosc_enabled = 0 + return + self.start_busy("stop_touchosc2midi", "stopping Touch-OSC") + logging.info("STOPPING touchosc2midi") + try: + check_output(f"systemctl stop {service}", shell=True) + zynthian_gui_config.midi_touchosc_enabled = 0 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_TOUCHOSC_ENABLED": str(zynthian_gui_config.midi_touchosc_enabled) + }) + sleep(wait) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STOPPING Touch-OSC", e) + sleep(2.0) + + self.end_busy("stop_touchosc2midi") + + # Start/Stop TouchOSC depending on configuration + def default_touchosc(self): + if zynthian_gui_config.midi_touchosc_enabled: + self.start_touchosc2midi(False) + else: + self.stop_touchosc2midi(False) + + def select_bluetooth_controller(self, controller): + if controller.count(":") != 5: + return + proc = Popen('bluetoothctl', stdin=PIPE, stdout=PIPE, + stderr=PIPE, encoding='utf-8') + for addr in check_output("bluetoothctl list", shell=True, timeout=1, encoding="utf-8").split(): + if addr.count(":") == 5: + proc.stdin.write(f"select {addr}\n") + if controller == addr: + proc.stdin.write(f"power on\n") + else: + proc.stdin.write(f"power off\n") + proc.stdin.flush() + proc.stdin.write(f"exit\n") + proc.stdin.flush() + zynthian_gui_config.ble_controller = controller + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_BLE_CONTROLLER": zynthian_gui_config.ble_controller + }) + + def start_bluetooth(self, save_config=True, wait=0): + service = "bluetooth" + if zynconf.is_service_active(service): + zynthian_gui_config.bluetooth_enabled = 1 + self.select_bluetooth_controller( + zynthian_gui_config.ble_controller) + return + self.start_busy("start_bluetooth", "starting Bluetooth") + logging.info("STARTING Bluetooth") + try: + check_output(f"systemctl start {service}", shell=True, timeout=2) + sleep(wait) + zynthian_gui_config.bluetooth_enabled = 1 + self.select_bluetooth_controller( + zynthian_gui_config.ble_controller) + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_BLE_ENABLED": str(zynthian_gui_config.bluetooth_enabled) + }) + # Call autoconnect after a little time + zynautoconnect.request_midi_connect(True) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STARTING Bluetooth", e) + sleep(2.0) + + self.end_busy("start_bluetooth") + + def stop_bluetooth(self, save_config=True, wait=0): + service = "bluetooth" + if not zynconf.is_service_active(service): + zynthian_gui_config.bluetooth_enabled = 0 + return + self.start_busy("stop_bluetooth", "stopping Bluetooth") + logging.info("STOPPING bluetooth") + try: + check_output(f"systemctl stop {service}", shell=True, timeout=1) + sleep(wait) + zynthian_gui_config.bluetooth_enabled = 0 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_BLE_ENABLED": str(zynthian_gui_config.bluetooth_enabled) + }) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STOPPING Bluetooth", e) + sleep(2.0) + + self.end_busy("stop_bluetooth") + + # Start/Stop Bluetooth depending on configuration + def default_bluetooth(self): + if zynthian_gui_config.bluetooth_enabled: + self.start_bluetooth(False) + else: + self.stop_bluetooth(False) + + def start_aubionotes(self, save_config=True, wait=0): + service = "aubionotes" + if zynconf.is_service_active(service): + zynthian_gui_config.midi_aubionotes_enabled = 1 + return + self.start_busy("start_aubionotes", "starting AubioNotes") + logging.info("STARTING aubionotes") + try: + check_output(f"systemctl start {service}", shell=True) + zynthian_gui_config.midi_aubionotes_enabled = 1 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_AUBIONOTES_ENABLED": str(zynthian_gui_config.midi_aubionotes_enabled) + }) + # Call autoconnect after a little time + sleep(wait) + zynautoconnect.request_midi_connect(True) + zynautoconnect.request_audio_connect() + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STARTING AubioNotes", e) + sleep(2.0) + + self.end_busy("start_aubionotes") + + def stop_aubionotes(self, save_config=True, wait=0): + service = "aubionotes" + if not zynconf.is_service_active(service): + zynthian_gui_config.midi_aubionotes_enabled = 0 + return + + self.start_busy("stop_aubionotes", "stopping AubioNotes") + logging.info("STOPPING aubionotes") + try: + check_output(f"systemctl stop {service}", shell=True) + zynthian_gui_config.midi_aubionotes_enabled = 0 + # Update MIDI profile + if save_config: + zynconf.update_midi_profile({ + "ZYNTHIAN_MIDI_AUBIONOTES_ENABLED": str(zynthian_gui_config.midi_aubionotes_enabled) + }) + sleep(wait) + except Exception as e: + logging.error(e) + self.set_busy_error("ERROR STOPPING AubioNotes", e) + sleep(2.0) + + self.end_busy("stop_aubionotes") + + # Start/Stop AubioNotes depending on configuration + def default_aubionotes(self): + if zynthian_gui_config.midi_aubionotes_enabled: + self.start_aubionotes(False) + else: + self.stop_aubionotes(False) + + # --------------------------------------------------------------------------- + # Zynthian Config Info + # --------------------------------------------------------------------------- + + def get_zynthian_config(self, varname): + try: + return eval("zynthian_gui_config.{}".format(varname)) + except: + return None + + def allow_rbpi_headphones(self): + try: + return self.alsa_mixer_processor.engine.allow_rbpi_headphones() + except: + return False + + def check_for_updates(self): + if self.checking_for_updates: + return + self.checking_for_updates = True + + def update_thread(): + logging.debug("************ CHECKING FOR UPDATES ... ************") + try: + repos = ["zynthian-ui", "zynthian-sys", "zynthian-webconf", "zynthian-data", "zyncoder"] + # If attached to last stable => Detect if new tag relase available + if os.environ.get('ZYNTHIAN_STABLE_TAG', "") == "last": + stable_branch = os.environ.get('ZYNTHIAN_STABLE_BRANCH', "oram") + for repo in repos: + 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") + last_stag = stags[-1].strip() + #logging.debug(f"STABLE TAG RELEASES => {stags}") + if branch != last_stag: + #logging.info(f"For reposiroty '{repo}', current branch ({branch}) != last tag release ({last_stag})!") + self.update_available = True + break + # else => Check for commits to pull + else: + 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] + #logging.debug(f"*********** BRANCH {branch} => local hash {local_hash}, remote hash {remote_hash} ****************") + if local_hash != remote_hash: + self.update_available = True + break + except Exception as e: + logging.warning(e) + self.checking_for_updates = False + + def get_repo_branch(path): + res = check_output(["git", "-C", path, "rev-parse", "--abbrev-ref", "HEAD"], + encoding="utf-8", stderr=STDOUT).strip() + parts = res.split("/", 1) + if len(parts) > 1 and parts[0] == 'heads': + return parts[1] + else: + return res + + thread = Thread(target=update_thread, args=()) + thread.name = "Check update" + thread.daemon = True # thread dies with the program + thread.start() + + # ---------------------------------------------------------------------------