diff --git a/template/engine/api.py b/template/engine/api.py index f530e88..9d0d41f 100644 --- a/template/engine/api.py +++ b/template/engine/api.py @@ -130,6 +130,8 @@ class Callbacks(object): def _setPlaybackTicks(self): + """This gets called very very often. Any connected function needs to watch closely + for performance issues""" ppqn = cbox.Transport.status().pos_ppqn status = playbackStatus() for func in self.setPlaybackTicks: @@ -146,6 +148,8 @@ class Callbacks(object): This is deprecated. Append to _checkPlaybackStatusAndSendSignal which is checked by the event loop. """ + raise NotImplementedError("this function was deprecated. use _checkPlaybackStatusAndSendSignal") + pass #only keep for the docstring and to keep the pattern. def _checkPlaybackStatusAndSendSignal(self): @@ -286,6 +290,7 @@ def startEngine(nsmClient): it takes after imports. """ + logger.info("Starting template api engine") assert session assert callbacks @@ -301,6 +306,8 @@ def startEngine(nsmClient): callbacks._recordingModeChanged() #recording mode is in the save file. callbacks._historyChanged() #send initial undo status to the GUI, which will probably deactivate its undo/redo menu because it is empty. + logger.info("Template api engine started") + def _deprecated_updatePlayback(): """The only place in the program to update the cbox playback besides startEngine. diff --git a/template/engine/sampler_sf2.py b/template/engine/sampler_sf2.py index 1cf0bc1..087139b 100644 --- a/template/engine/sampler_sf2.py +++ b/template/engine/sampler_sf2.py @@ -53,8 +53,8 @@ class Sampler_sf2(Data): self.patchlist = {} #bank:{program:name} self.buffer_ignoreProgramChanges = ignoreProgramChanges - self.midiInput = MidiInput(session=parentSession, portName="in") - + self.midiInput = MidiInput(session=parentSession, portName="in") + #Set libfluidsynth! to 16 output pairs. We prepared 32 jack ports in the session start. "soundfont" is our given name, in the line below. This is a prepared config which will be looked up by add_new_instrument cbox.Config.set("instrument:soundfont", "engine", "fluidsynth") cbox.Config.set("instrument:soundfont", "output_pairs", 16) #this is not the same as session.py cbox.Config.set("io", "outputs", METADATA["cboxOutputs"]). It is yet another layer and those two need connections @@ -68,31 +68,38 @@ class Sampler_sf2(Data): self.midiInput.scene.status().layers[0].set_ignore_program_changes(ignoreProgramChanges) assert self._ignoreProgramChanges == ignoreProgramChanges - #Active patches is a dynamic value. Load the saved ones here, but do not save their state locally + #Active patches is a dynamic value. Load the saved ones here, but do not save their state locally try: self.setActivePatches(activePatches) except: self._correctActivePatches() - #Create a dynamic pair of audio output ports and route all stereo pairs to our summing channel. - #This does _not_ need updating when loading another sf2 or changing instruments. - lmixUuid = cbox.JackIO.create_audio_output('left_mix', "#1") #add "#1" as second parameter for auto-connection to system out 1 - rmixUuid = cbox.JackIO.create_audio_output('right_mix', "#2") #add "#2" as second parameter for auto-connection to system out 2 - for i in range(16): + #Create a dynamic pair of audio output ports and route all stereo pairs to our summing channel. + #This does _not_ need updating when loading another sf2 or changing instruments. + + assert not self.parentSession.standaloneMode is None + if self.parentSession.standaloneMode: + lmixUuid = cbox.JackIO.create_audio_output('left_mix', "#1") #add "#1" as second parameter for auto-connection to system out 1 + rmixUuid = cbox.JackIO.create_audio_output('right_mix', "#2") #add "#2" as second parameter for auto-connection to system out 2 + else: + lmixUuid = cbox.JackIO.create_audio_output('left_mix') + rmixUuid = cbox.JackIO.create_audio_output('right_mix') + + for i in range(16): router = cbox.JackIO.create_audio_output_router(lmixUuid, rmixUuid) - router.set_gain(-3.0) - self.instrument.get_output_slot(i).rec_wet.attach(router) #output_slot is 0 based and means a pair + router.set_gain(-3.0) + self.instrument.get_output_slot(i).rec_wet.attach(router) #output_slot is 0 based and means a pair #slot 17 is an error. cbox tells us there is only [1, 16], good. - #Set port order. It is already correct because alphanumerial order, but explicit is better than implicit + #Set port order. It is already correct because alphanumerial order, but explicit is better than implicit for channelNumber in range(1,33): portname = f"{cbox.JackIO.status().client_name}:out_{channelNumber}" try: cbox.JackIO.Metadata.set_port_order(portname, channelNumber) except Exception as e: #No Jack Meta Data logger.error(e) - - #Also sort the mixing channels + + #Also sort the mixing channels try: portname = f"{cbox.JackIO.status().client_name}:left_mix" cbox.JackIO.Metadata.set_port_order(portname, 33) @@ -140,12 +147,12 @@ class Sampler_sf2(Data): def loadSoundfont(self, filePath, defaultSoundfont=None): """defaultSoundfont is a special case. The path is not saved""" - logger.info(f"loading path: \"{filePath}\" defaultSoundfont parameter: {defaultSoundfont}") + logger.info(f"loading path: \"{filePath}\" defaultSoundfont parameter: {defaultSoundfont}") #Remove the old link, if present. We cannot unlink directly in loadSoundfont because it is quite possible that a user will try out another soundfont but decide not to save but close and reopen to get his old soundfont back. if self.filePath and os.path.islink(self.filePath): self._unlinkOnSave.append(self.filePath) - + #self.midiInput.scene.clear() do not call for the sf2 engine. Will kill our instrument. for stereoPair in range(16): #midi channels fluidsynth output . 0-15 @@ -161,7 +168,7 @@ class Sampler_sf2(Data): else: self.instrument.engine.load_soundfont(filePath) self.filePath = filePath - self.patchlist = self._convertPatches( self.instrument.engine.get_patches() ) + self.patchlist = self._convertPatches( self.instrument.engine.get_patches() ) self._correctActivePatches() return True, "" except Exception as e: #throws a general Exception if not a good file @@ -177,7 +184,7 @@ class Sampler_sf2(Data): change cbox here and now.""" if not self.patchlist: self.patchlist = self._convertPatches( self.instrument.engine.get_patches() ) #try again - if not self.patchlist: raise ValueError("No programs in this sound font") + if not self.patchlist: raise ValueError("No programs in this sound font") for channel, (value, name) in self.instrument.engine.status().patch.items(): #channel is base 1 program = value & 127 @@ -192,7 +199,7 @@ class Sampler_sf2(Data): logger.info(METADATA["name"] + f": Channel {channel} Old bank/program not possible in new soundfont. Switching to first available program.") self.setPatch(channel, youchoose_bank, youchoose_program) break #inner loop. one instrument is enough. - + def activePatches(self): @@ -203,7 +210,7 @@ class Sampler_sf2(Data): Takes the current bank into consideration """ - result = {} + result = {} for channel, (value, name) in self.instrument.engine.status().patch.items(): program = value & 127 bank = value >> 7 @@ -218,7 +225,7 @@ class Sampler_sf2(Data): self.setPatch(channel, bank, program) def setPatch(self, channel, bank, program): - """An input error happens not often, but can happen if the saved data mismatches the + """An input error happens not often, but can happen if the saved data mismatches the soundfont. This happens on manual save file change or soundfont change (version update?) between program runs. We assume self.patchlist is up to date.""" @@ -246,7 +253,7 @@ class Sampler_sf2(Data): portnameR = f"{cbox.JackIO.status().client_name}:out_{chanR}" #Use the instrument name as port name: 03-L:Violin - try: + try: cbox.JackIO.Metadata.set_pretty_name(portnameL, f"{str(channel).zfill(2)}-L : {name}") cbox.JackIO.Metadata.set_pretty_name(portnameR, f"{str(channel).zfill(2)}-R : {name}") except Exception as e: #No Jack Meta Data diff --git a/template/engine/session.py b/template/engine/session.py index d3e6db8..0ed55f6 100644 --- a/template/engine/session.py +++ b/template/engine/session.py @@ -51,7 +51,7 @@ class Session(object): self.recordingEnabled = False #MidiInput callbacks can use this to prevent/allow data creation. Handled via api callback. Saved. self.eventLoop = None # added in api.startEngine self.data = None #nsm_openOrNewCallback - + self.standaloneMode = None #fake NSM single server for LaborejoSoftwareSuite programs or not. Set in nsm_openOrNewCallback def addSessionPrefix(self, jsonDataAsString:str): """During load the current session prefix gets added. Turning pseudo-relative paths into @@ -103,6 +103,8 @@ class Session(object): self.sessionPrefix = ourPath #if we want to save and load resources they need to be in the session dir. We never load from outside, the scheme is always "import first, load local file" self.absoluteJsonFilePath = os.path.join(ourPath, "save." + METADATA["shortName"] + ".json") + self.standaloneMode = sessionName == "NOT-A-SESSION" + try: self.data = self.openFromJson(self.absoluteJsonFilePath) except FileNotFoundError: diff --git a/template/helper.py b/template/helper.py index e5f21a7..6c2d771 100644 --- a/template/helper.py +++ b/template/helper.py @@ -95,4 +95,10 @@ def whoCalled(): print() +def provokecrash(): + """Obviously for testing""" + import ctypes + p = ctypes.pointer(ctypes.c_char.from_address(5)) + p[0] = b'x' + diff --git a/template/qtgui/mainwindow.py b/template/qtgui/mainwindow.py index da4a37e..1861601 100644 --- a/template/qtgui/mainwindow.py +++ b/template/qtgui/mainwindow.py @@ -186,7 +186,7 @@ class MainWindow(QtWidgets.QMainWindow): if not settings.contains("showAboutDialog"): settings.setValue("showAboutDialog", METADATA["showAboutDialogFirstStart"]) - self.about = About(mainWindow=self) + self.about = About(mainWindow=self) #This does not show, it only creates. Showing is decided in self.start self.ui.menubar.setNativeMenuBar(False) #Force a real menu bar. Qt on wayland will not display it otherwise. self.menu = Menu(mainWindow=self) #needs the about dialog, save file and the api.session ready. diff --git a/template/qtgui/nsmclient.py b/template/qtgui/nsmclient.py index 42c1e56..e422261 100644 --- a/template/qtgui/nsmclient.py +++ b/template/qtgui/nsmclient.py @@ -548,18 +548,14 @@ class NSMClient(object): """If you want a very strict client you can block any non-NSM quit-attempts, like ignoring a qt closeEvent, and instead send the NSM Server a request to close this client. This method is a shortcut to do just that. - - Using this method will not result in a NSM-"client died unexpectedly" message that usually - happens a client quits on its own. This message is harmless but may confuse a user.""" - - logger.info("instructing the NSM-Server to send SIGTERM to ourselves.") - if "server-control" in self.serverFeatures: - message = _OutgoingMessage("/nsm/server/stop") - message.add_arg("{}".format(self.ourClientId)) - self.sock.sendto(message.build(), self.nsmOSCUrl) - else: - logger.warning("...but the NSM-Server does not support server control. Quitting on our own. Server only supports: {}".format(self.serverFeatures)) - kill(getpid(), SIGTERM) #this calls the exit callback but nsm will output something like "client died unexpectedly." + """ + logger.info("Sending SIGTERM to ourselves to trigger the exit callback.") + #if "server-control" in self.serverFeatures: + # message = _OutgoingMessage("/nsm/server/stop") + # message.add_arg("{}".format(self.ourClientId)) + # self.sock.sendto(message.build(), self.nsmOSCUrl) + #else: + kill(getpid(), SIGTERM) #this calls the exit callback def serverSendSaveToSelf(self): """Some clients want to offer a manual Save function, mostly for psychological reasons. diff --git a/template/start.py b/template/start.py index b88cffd..a59a997 100644 --- a/template/start.py +++ b/template/start.py @@ -83,13 +83,13 @@ try: compiledVersion = True logger.info("Compiled prefix found: {}".format(prefix)) except ModuleNotFoundError as e: - compiledVersion = False + compiledVersion = False logger.info("Compiled version: {}".format(compiledVersion)) cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"] -if compiledVersion: +if compiledVersion: PATHS={ #this gets imported "root": "", "bin": os.path.join(prefix, "bin"), @@ -98,26 +98,26 @@ if compiledVersion: "share": os.path.join(prefix, "share", METADATA["shortName"]), "templateShare": os.path.join(prefix, "share", METADATA["shortName"], "template"), #"lib": os.path.join(prefix, "lib", METADATA["shortName"]), #cbox is found via the PYTHONPATH - } - - cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName) + } + + cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName) _root = os.path.dirname(__file__) _root = os.path.abspath(os.path.join(_root, "..")) fallback_cboxSharedObjectPath = os.path.join(_root, "site-packages", cboxSharedObjectVersionedName) - + #Local version has higher priority - - - if os.path.exists(fallback_cboxSharedObjectPath): #we are not yet installed, look in the source site-packages dir - os.environ["CALFBOXLIBABSPATH"] = fallback_cboxSharedObjectPath - elif os.path.exists(cboxSharedObjectPath): #we are installed + + + if os.path.exists(fallback_cboxSharedObjectPath): #we are not yet installed, look in the source site-packages dir + os.environ["CALFBOXLIBABSPATH"] = fallback_cboxSharedObjectPath + elif os.path.exists(cboxSharedObjectPath): #we are installed os.environ["CALFBOXLIBABSPATH"] = cboxSharedObjectPath - - else: + + else: pass #no support for system-wide cbox in compiled mode. Error handling at the bottom of the file - - + + else: _root = os.path.dirname(__file__) _root = os.path.abspath(os.path.join(_root, "..")) @@ -128,16 +128,16 @@ else: "desktopfile": os.path.join(_root, "desktop", "desktop.desktop"), #not ~/Desktop but our desktop file "share": os.path.join(_root, "engine", "resources"), "templateShare": os.path.join(_root, "template", "engine", "resources"), - #"lib": "", #use only system paths - } - if os.path.exists (os.path.join(_root, "site-packages", cboxSharedObjectVersionedName)): - os.environ["CALFBOXLIBABSPATH"] = os.path.join(_root, "site-packages", cboxSharedObjectVersionedName) + #"lib": "", #use only system paths + } + if os.path.exists (os.path.join(_root, "site-packages", cboxSharedObjectVersionedName)): + os.environ["CALFBOXLIBABSPATH"] = os.path.join(_root, "site-packages", cboxSharedObjectVersionedName) #else use system-wide. - + if os.path.exists (os.path.join(_root, "site-packages", "calfbox", "cbox.py")): #add to the front to have higher priority than system site-packages logger.info("Will attempt to start with local calfbox python module: {}".format(os.path.join(_root, "site-packages", "calfbox", "cbox.py"))) - sys.path.insert(0, os.path.join(os.path.join(_root, "site-packages"))) + sys.path.insert(0, os.path.join(os.path.join(_root, "site-packages"))) #else try to use system-wide calfbox. Check for this and if the .so exists at the end of this file. @@ -147,7 +147,7 @@ logger.info("PATHS: {}".format(PATHS)) QtGui.QGuiApplication.setDesktopSettingsAware(False) #We need our own font so the user interface stays predictable QtGui.QGuiApplication.setDesktopFileName(PATHS["desktopfile"]) qtApp = QApplication(sys.argv) -setPaletteAndFont(qtApp) +setPaletteAndFont(qtApp) def exitWithMessage(message:str): @@ -157,7 +157,7 @@ def exitWithMessage(message:str): else: from PyQt5.QtWidgets import QMessageBox #This is the start file for the Qt client so we know at least that Qt is installed and use that for a warning. - QMessageBox.critical(qtApp.desktop(), title, message) + QMessageBox.critical(qtApp.desktop(), title, message) sys.exit(title + ": " + message) def setProcessName(executableName): @@ -180,7 +180,7 @@ def setProcessName(executableName): libpthread_path = ctypes.util.find_library("pthread") if not libpthread_path: return - + libpthread = ctypes.CDLL(libpthread_path) if hasattr(libpthread, "pthread_setname_np"): _pthread_setname_np = libpthread.pthread_setname_np @@ -194,7 +194,7 @@ def setProcessName(executableName): if _pthread_setname_np is None: return - + _pthread_setname_np(_pthread_self(), executableName.encode()) def checkNsmOrExit(prettyName): @@ -204,17 +204,17 @@ def checkNsmOrExit(prettyName): import sys from os import getenv if not getenv("NSM_URL"): #NSMClient checks for this itself but we can anticipate an error and inform the user. - + path = ChooseSessionDirectory(qtApp).path #ChooseSessionDirectory is calling exec. We can't call qtapp.exec_ because that blocks forever, even after quitting the window. #qSessionDirApp.quit() #del qSessionDirApp - #path = "/tmp" + #path = "/tmp" if path: - startPseudoNSMServer(path) + startPseudoNSMServer(path) else: sys.exit() - + #message = f"""Please start {prettyName} only through the New Session Manager (NSM) or use the --save command line parameter.""" #exitWithMessage(message) @@ -281,14 +281,14 @@ def profiler(*pargs, **kwds): #Catch Exceptions even if PyQt crashes. import sys -sys._excepthook = sys.excepthook +sys._excepthook = sys.excepthook def exception_hook(exctype, value, traceback): """This hook purely exists to call sys.exit(1) even on a Qt crash - so that atexit gets triggered""" + so that atexit gets triggered""" #print(exctype, value, traceback) logger.error("Caught crash in execpthook. Trying too execute atexit anyway") - sys._excepthook(exctype, value, traceback) - sys.exit(1) + sys._excepthook(exctype, value, traceback) + sys.exit(1) sys.excepthook = exception_hook @@ -340,13 +340,13 @@ if args.mute: #Make sure calfbox is available. if "CALFBOXLIBABSPATH" in os.environ: - logger.info("Looking for calfbox shared library in absolute path: {}".format(os.environ["CALFBOXLIBABSPATH"])) + logger.info("Looking for calfbox shared library in absolute path: {}".format(os.environ["CALFBOXLIBABSPATH"])) else: logger.info("Looking for calfbox shared library systemwide through ctypes.util.find_library") try: from calfbox import cbox - logger.info("{}: using cbox python module from {} . Local version has higher priority than system wide.".format(METADATA["name"], os.path.abspath(cbox.__file__))) + logger.info("{}: using cbox python module from {} . Local version has higher priority than system wide.".format(METADATA["name"], os.path.abspath(cbox.__file__))) except Exception as e: print (e) @@ -355,8 +355,14 @@ except Exception as e: print (sys.modules["calfbox"], "->", os.path.abspath(sys.modules["calfbox"].__file__)) else: print ("calfbox python module is not in sys.modules. This means it truly can't be found or you forgot --mute") - + print ("sys.path start and tail:", sys.path[0:5], sys.path[-1]) exitWithMessage("Calfbox module could not be loaded") +#Capture Ctlr+C / SIGINT and let @atexit handle the rest. +import signal +import sys +def signal_handler(sig, frame): + sys.exit(0) #atexit will trigger +signal.signal(signal.SIGINT, signal_handler)