diff --git a/template/calfbox/metadata.py b/template/calfbox/metadata.py index 2c42ddc..bd051da 100644 --- a/template/calfbox/metadata.py +++ b/template/calfbox/metadata.py @@ -3,7 +3,7 @@ """ This file implements the JackIO Python side of Jack Medata as described here: - http://www.jackaudio.org/files/docs/html/group__Metadata.html + https://jackaudio.org/api/metadata_8h.html """ import base64 # for icons @@ -64,6 +64,13 @@ class Metadata: return TypeError("value {} must be int or str but was {}".format(value, type(value))) do_cmd("/io/client_set_property", None, [key, value, jackPropertyType]) + @staticmethod + def client_remove_property(key): + """ + This is directly for our client, which we do not need to provide here. + """ + do_cmd("/io/client_remove_property", None, [key]) + @staticmethod def remove_property(port, key): """port is the portname as string System:out_1""" diff --git a/template/documentation/readme.template b/template/documentation/readme.template index 3b767ed..0284f49 100644 --- a/template/documentation/readme.template +++ b/template/documentation/readme.template @@ -36,6 +36,7 @@ It is possible to clone a git repository. ## Dependencies * Python 3.6 (maybe earlier) * PyQt5 for Python 3 +* PyQt OpenGL and SVG modules, if they are separated in your distribution * DejaVu Sans Sarif TTF (Font) (recommended, but not technically necessary) * libcalfbox-lss https://git.laborejo.org/lss/libcalfbox-lss diff --git a/template/engine/duration.py b/template/engine/duration.py index 6e21d84..37428e1 100644 --- a/template/engine/duration.py +++ b/template/engine/duration.py @@ -150,8 +150,33 @@ ticksToLyDurationLogDict = { D256: 8, #1/256 } -def ticksToLilypond(ticks:int)->int: - if ticks in ticksToLyDurationLogDict: - return ticksToLyDurationLogDict[ticks] + + +ticksToLilypondDict = {} +for ourticks, lyAsInt in baseDurationToTraditionalNumber.items(): + ly = str(lyAsInt) + ticksToLilypondDict[ourticks] = ly + ticksToLilypondDict[ourticks * 1.5] = ly + "." #dot + ticksToLilypondDict[ourticks * 1.75] = ly + ".." #double dot +ticksToLilypondDict[DM] = "\\maxima" +ticksToLilypondDict[DL] = "\\longa" +ticksToLilypondDict[DB] = "\\breve" +ticksToLilypondDict[DM*1.5] = "\\maxima." +ticksToLilypondDict[DL*1.5] = "\\longa." +ticksToLilypondDict[DB*1.5] = "\\breve." +ticksToLilypondDict[DM*1.75] = "\\maxima.." +ticksToLilypondDict[DL*1.75] = "\\longa.." +ticksToLilypondDict[DB*1.75] = "\\breve.." + + + +def ticksToLilypond(ticks:int)->str: + """Returns a lilypond string with the number 1, 2, 4, 8 etc. + It must be a string and not a number because we have dots and words like maxima and brevis. + + This is mostly used for tempo items. + """ + if ticks in ticksToLilypondDict: + return ticksToLilypondDict[ticks] else: - raise ValueError(f"{ticks} not in duration.py ticksToLyDurationLogDict") + raise ValueError(f"{ticks} not in duration.py ticksToLilypondDict") diff --git a/template/engine/input_midi.py b/template/engine/input_midi.py index fc7470a..4a1b946 100644 --- a/template/engine/input_midi.py +++ b/template/engine/input_midi.py @@ -109,6 +109,9 @@ class MidiInput(object): for hp in hardwareMidiPorts: cbox.JackIO.port_connect(hp, cbox.JackIO.status().client_name + ":" + self.portName) + def fullName(self)->str: + return cbox.JackIO.status().client_name + ":" + self.portName + class MidiProcessor(object): """ @@ -165,7 +168,7 @@ class MidiProcessor(object): This function gets called very often. So every optimisation is good. """ - events = cbox.JackIO.get_new_events(self.parentInput.cboxMidiPortUid) + events = cbox.JackIO.get_new_events(self.parentInput.cboxMidiPortUid) #We get the events even if not active. Otherwise they pile up. if not self.active: return if not events: diff --git a/template/engine/metronome.py b/template/engine/metronome.py index 496ed40..edf33b9 100644 --- a/template/engine/metronome.py +++ b/template/engine/metronome.py @@ -73,6 +73,10 @@ class Metronome(object): self.label = "" #E.g. current Track Name, but can be anything. self.setEnabled(False) #TODO: save load + def getPortNames(self)->(str,str): + """Return two client:port , for left and right channel""" + return (self.sfzInstrumentSequencerInterface.portnameL, self.sfzInstrumentSequencerInterface.portnameR) + def soundStresses(self, value:bool): self._soundStresses = value self.generate(self._cachedData, self.label) diff --git a/template/engine/pitch.py b/template/engine/pitch.py index 352bf25..ed7eb61 100644 --- a/template/engine/pitch.py +++ b/template/engine/pitch.py @@ -1006,7 +1006,7 @@ sortedNoteNameList = [ "bisis'''''" , ] -baseNotesToBaseNames = { +baseNotesToBaseLyNames = { 20 : "C", 70 : "D", 120 : "E", @@ -1021,7 +1021,15 @@ baseNotesToBaseNames = { 170-10 : "Fes", 220-10 : "Ges", 270-10 : "Aes", - 320-10 : "Bes/Bb", + 320-10 : "Bes", + + 20-20 : "Ceses", + 70-20 : "Deses", + 120-20 : "Eeses", + 170-20 : "Feses", + 220-20 : "Geses", + 270-20 : "Aeses", + 320-20 : "Beses", 20+10 : "Cis", 70+10 : "Dis", @@ -1029,7 +1037,57 @@ baseNotesToBaseNames = { 170+10 : "Fis", 220+10 : "Gis", 270+10 : "Ais", - 320+10 : "Bis/His", + 320+10 : "Bis", + + 20+20 : "Cisis", + 70+20 : "Disis", + 120+20 : "Eisis", + 170+20 : "Fisis", + 220+20 : "Gisis", + 270+20 : "Aisis", + 320+20 : "Bisis", + } + +baseNotesToAccidentalNames = { + 20 : "C", + 70 : "D", + 120 : "E", + 170 : "F", + 220 : "G", + 270 : "A", + 320 : "B♮/H", + + 20-10 : "C♭", + 70-10 : "D♭", + 120-10 : "E♭", + 170-10 : "F♭", + 220-10 : "G♭", + 270-10 : "A♭", + 320-10 : "B♭/H♭", + + 20-20 : "C𝄫", + 70-20 : "D𝄫", + 120-20 : "E𝄫", + 170-20 : "F𝄫", + 220-20 : "G𝄫", + 270-20 : "A𝄫", + 320-20 : "B𝄫/H𝄫", + + 20+10 : "C♯", + 70+10 : "D♯", + 120+10 : "E♯", + 170+10 : "F♯", + 220+10 : "G♯", + 270+10 : "A♯", + 320+10 : "B♯/H♯", + + 20+20 : "C𝄪", + 70+20 : "D𝄪", + 120+20 : "E𝄪", + 170+20 : "F𝄪", + 220+20 : "G𝄪", + 270+20 : "A𝄪", + 320+20 : "B𝄪/H𝄪", } orderedBaseNotes = ["C", "D", "E", "F", "G", "A", "H/B"] diff --git a/template/engine/sequencer.py b/template/engine/sequencer.py index b8ffee1..d4d29f4 100644 --- a/template/engine/sequencer.py +++ b/template/engine/sequencer.py @@ -503,10 +503,21 @@ class SfzInstrumentSequencerInterface(_Interface): self.calfboxTrack.set_external_output("") #Metadata - portnameL = f"{cbox.JackIO.status().client_name}:out_1" - portnameR = f"{cbox.JackIO.status().client_name}:out_2" - cbox.JackIO.Metadata.set_pretty_name(portnameL, name.title() + "-L") - cbox.JackIO.Metadata.set_pretty_name(portnameR, name.title() + "-R") + l = f"{cbox.JackIO.status().client_name}:out_1" + r = f"{cbox.JackIO.status().client_name}:out_2" + self.portnameL = cbox.JackIO.status().client_name + ":" + name.title() + "-L" + self.portnameR = cbox.JackIO.status().client_name + ":" + name.title() + "-R" + + luuid = cbox.JackIO.create_audio_output(name.title() + "-L") + ruuid = cbox.JackIO.create_audio_output(name.title() + "-R") + cbox.JackIO.Metadata.set_pretty_name(self.portnameL, name.title() + "-L") + cbox.JackIO.Metadata.set_pretty_name(self.portnameR, name.title() + "-R") + + + self.outputMergerRouter = cbox.JackIO.create_audio_output_router(luuid, ruuid) + self.outputMergerRouter.set_gain(-1.0) + self.instrumentLayer.get_output_slot(0).rec_wet.attach(self.outputMergerRouter) #output_slot is 0 based and means a pair. + def enable(self, enabled): if enabled: diff --git a/template/helper.py b/template/helper.py index 2fef53f..e8eda83 100644 --- a/template/helper.py +++ b/template/helper.py @@ -27,6 +27,14 @@ from functools import lru_cache #https://docs.python.org/3.4/library/functools.h cache_unlimited = lru_cache(maxsize=None) #use as @cache_unlimited decorator +def dictdiff(a, b): + """We use dicts for internal export packages. Compare two of them for changes""" + result = {} + for key, value in a.items(): + if not b[key] == value: + result[key] = (value, b[key]) + return result + import itertools def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." diff --git a/template/qtgui/debugScript.py b/template/qtgui/debugScript.py index 5b6f4ff..6ed0b53 100644 --- a/template/qtgui/debugScript.py +++ b/template/qtgui/debugScript.py @@ -39,9 +39,11 @@ class DebugScriptRunner(object): assert nsmClient self.nsmClient = nsmClient self.absoluteScriptFilePath = os.path.join(self.nsmClient.ourPath, "debugscript.py") - + logger.info(f"{self.nsmClient.ourClientNameUnderNSM}: Using script file: {self.absoluteScriptFilePath}") + def _createEmptyDebugScript(self): assert self.absoluteScriptFilePath + print(f"{self.nsmClient.ourClientNameUnderNSM}: Script file not found. Initializing: {self.absoluteScriptFilePath}") logger.info(f"{self.nsmClient.ourClientNameUnderNSM}: Script file not found. Initializing: {self.absoluteScriptFilePath}") text = ("""#! /usr/bin/env python3""" "\n" @@ -54,14 +56,14 @@ class DebugScriptRunner(object): ) with open(self.absoluteScriptFilePath, "w", encoding="utf-8") as f: - f.write(text) - + f.write(text) + def run(self): if not os.path.exists(self.absoluteScriptFilePath): if not os.path.exists(self.nsmClient.ourPath): - os.makedirs(self.nsmClient.ourPath) + os.makedirs(self.nsmClient.ourPath) self._createEmptyDebugScript() - + try: exec(compile(open(self.absoluteScriptFilePath).read(), filename=self.absoluteScriptFilePath, mode="exec"), globals(), self.apilocals) except Exception as e: diff --git a/template/qtgui/helper.py b/template/qtgui/helper.py index 71f4cf1..21a9d7b 100644 --- a/template/qtgui/helper.py +++ b/template/qtgui/helper.py @@ -26,6 +26,41 @@ from PyQt5 import QtCore, QtGui, QtWidgets from hashlib import md5 + +def makeValueWidget(value:any): + """Create a widget just from a value. Needs an external label e.g. in a formLayout. + First usecase was laborejo lilypond metadata and properties where it edited an engine-dict + directly and inplace. + Use with getValueFromWidget""" + types = { + str : QtWidgets.QLineEdit, + int : QtWidgets.QSpinBox, + float : QtWidgets.QDoubleSpinBox, + bool : QtWidgets.QCheckBox, + } + typ = type(value) + widget = types[typ]() + + if typ == str: + widget.setText(value) + elif typ == int or typ == float: + widget.setValue(value) + elif typ == bool: + widget.setChecked(value) + return widget + + +def getValueFromWidget(widget): + """Use with makeValueWidget""" + typ = type(widget) + if typ == QtWidgets.QLineEdit: + return widget.text() + elif typ == QtWidgets.QSpinBox or typ == QtWidgets.QDoubleSpinBox: + return widget.value() + elif typ == QtWidgets.QCheckBox: + return widget.isChecked() + + def iconFromString(st, size=128): px = QtGui.QPixmap(size,size) color = stringToColor(st) diff --git a/template/qtgui/midiinquickwidget.py b/template/qtgui/midiinquickwidget.py index 9fe80e1..9dd37f7 100644 --- a/template/qtgui/midiinquickwidget.py +++ b/template/qtgui/midiinquickwidget.py @@ -34,9 +34,11 @@ import engine.api as api class QuickMidiInputComboController(QtWidgets.QWidget): """This widget breaks a bit with the convention of engine/gui. However, it is a pure convenience - fire-and-forget function with no callbacks.""" + fire-and-forget function with no callbacks. - def __init__(self, parentWidget): + If you give a text it must already be translated.""" + + def __init__(self, parentWidget, text=None, stretch=True): super().__init__(parentWidget) self.parentWidget = parentWidget self.layout = QtWidgets.QHBoxLayout() @@ -54,7 +56,10 @@ class QuickMidiInputComboController(QtWidgets.QWidget): self.wholePanel = parentWidget.ui.auditionerWidget """ self.label = QtWidgets.QLabel(self) - self.label.setText(QtCore.QCoreApplication.translate("QuickMidiInputComboController", "Midi Input. Use JACK to connect multiple inputs.")) + if text: + self.label.setText(text) #already translated by parent. + else: + self.label.setText(QtCore.QCoreApplication.translate("QuickMidiInputComboController", "Midi Input. Use JACK to connect multiple inputs.")) self.layout.addWidget(self.label) #if not api.isStandaloneMode(): @@ -66,9 +71,8 @@ class QuickMidiInputComboController(QtWidgets.QWidget): self.comboBox.showPopup = self.showPopup self.comboBox.activated.connect(self._newPortChosen) - self.cboxPortname, self.cboxMidiPortUid = api.getMidiInputNameAndUuid() #these don't change during runtime. - - self.layout.addStretch() + if stretch: + self.layout.addStretch() #api.callbacks.startLoadingAuditionerInstrument.append(self.callback_startLoadingAuditionerInstrument) #api.callbacks.auditionerInstrumentChanged.append(self.callback_auditionerInstrumentChanged) #api.callbacks.auditionerVolumeChanged.append(self.callback__auditionerVolumeChanged) @@ -130,17 +134,23 @@ class QuickMidiInputComboController(QtWidgets.QWidget): """externalPort is in the Client:Port JACK format If "" False or None disconnect all ports.""" + cboxPortname, cboxMidiPortUid = api.getMidiInputNameAndUuid() #these don't change during runtime. But if the system it not ready yet it returns None, None + + if cboxPortname is None and cboxMidiPortUid is None: + logging.info("engine is not ready yet to deliver midi input name and port id. This is normal during startup but a problem during normal runtime.") + return #startup delay + try: - currentConnectedList = cbox.JackIO.get_connected_ports(self.cboxMidiPortUid) + currentConnectedList = cbox.JackIO.get_connected_ports(cboxMidiPortUid) except: #port not found. currentConnectedList = [] for port in currentConnectedList: - cbox.JackIO.port_disconnect(port, self.cboxPortname) + cbox.JackIO.port_disconnect(port, cboxPortname) if externalPort: availablePorts = self.getAvailablePorts() if not (externalPort in availablePorts["hardware"] or externalPort in availablePorts["software"]): raise RuntimeError(f"QuickMidiInput was instructed to connect to port {externalPort}, which does not exist") - cbox.JackIO.port_connect(externalPort, self.cboxPortname) + cbox.JackIO.port_connect(externalPort, cboxPortname) diff --git a/template/qtgui/submenus.py b/template/qtgui/submenus.py index 920c6d9..e107b74 100644 --- a/template/qtgui/submenus.py +++ b/template/qtgui/submenus.py @@ -50,6 +50,10 @@ class Submenu(QtWidgets.QDialog): label = QtWidgets.QLabel(labelString) #"Choose a clef" or so. self.layout.addWidget(label) + #Second Label that can be changed on every call with self.dynamicLabel.setText() + self.dynamicLabel = QtWidgets.QLabel("") + self.layout.addWidget(self.dynamicLabel) + #self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons. if hasOkCancelButtons == 1: #or true @@ -100,7 +104,8 @@ class Submenu(QtWidgets.QDialog): self.done(True) def __call__(self): - """This instance can be called like a function""" + """This instance can be called like a function. + Subclasses can subclass this as well to create non-static functionality.""" if self.buttonBox: self.layout.addWidget(self.buttonBox) self.setFixedSize(self.layout.geometry().size())