From dcdb46cba8d7d2392d3c7db03f03a6cf8c21aa02 Mon Sep 17 00:00:00 2001 From: Nils <> Date: Mon, 15 Nov 2021 18:48:34 +0100 Subject: [PATCH] explicitly unload all instruments on quit to circumvent a jackd freeze --- engine/api.py | 33 ++++++++++++++++++++++++++++----- engine/main.py | 2 -- qtgui/instrument.py | 3 +++ qtgui/mainwindow.py | 1 + 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/engine/api.py b/engine/api.py index 82b6354..d3d33cb 100644 --- a/engine/api.py +++ b/engine/api.py @@ -24,6 +24,8 @@ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library Modules from typing import List, Set, Dict, Tuple +import atexit + #Third Party Modules from calfbox import cbox @@ -142,23 +144,44 @@ def startEngine(nsmClient, additionalData): callbacks._auditionerVolumeChanged() + atexit.register(unloadAllInstrumentSamples) #this will handle all python exceptions, but not segfaults of C modules. logger.info("Tembro api startEngine complete") def loadAllInstrumentSamples(): """Actually load all instrument samples""" + logger.info(f"Loading all instruments.") for instrument in Instrument.allInstruments.values(): callbacks._startLoadingSamples(*instrument.idKey) instrument.loadSamples() callbacks._instrumentStatusChanged(*instrument.idKey) - callbacks._dataChanged + callbacks._dataChanged() + +def unloadAllInstrumentSamples(): + """Cleanup. + It turned out relying on cbox closes ports or so too fast for JACK (not confirmed). + If too many instruments (> ~12) were loaded the program will freeze on quit and freeze + JACK as well. + A controlled deactivating of instruments circumvents this. + #TODO: Fixing jack2 is out of scope for us. If that even is a jack2 error. + + The order of atexit is the reverse order of registering. Since the template stopSession + is registered before api.startEngine it is safe to rely on atexit. + + The function could also be used from the menu of course. + """ + logger.info(f"Unloading all instruments.") + for instrument in Instrument.allInstruments.values(): + if instrument.enabled: + instrument.disable() + callbacks._instrumentStatusChanged(*instrument.idKey) + callbacks._dataChanged() #Needs to be here to have incremental updates in the gui. def unloadInstrumentSamples(idkey:tuple): instrument = Instrument.allInstruments[idkey] instrument.disable() callbacks._instrumentStatusChanged(*instrument.idKey) - callbacks._dataChanged - + callbacks._dataChanged() def loadInstrumentSamples(idkey:tuple): """Load one .sfz from a library.""" @@ -170,7 +193,7 @@ def loadInstrumentSamples(idkey:tuple): instrument.loadSamples() callbacks._instrumentStatusChanged(*instrument.idKey) - callbacks._dataChanged + callbacks._dataChanged() def chooseVariantByIndex(idkey:tuple, variantIndex:int): """Choose a variant of an already enabled instrument""" @@ -178,7 +201,7 @@ def chooseVariantByIndex(idkey:tuple, variantIndex:int): instrument = Instrument.allInstruments[idkey] instrument.chooseVariantByIndex(variantIndex) callbacks._instrumentStatusChanged(*instrument.idKey) - callbacks._dataChanged + callbacks._dataChanged() def setInstrumentMixerVolume(idkey:tuple, value:float): """From 0 to -21. diff --git a/engine/main.py b/engine/main.py index fbdfbce..1e9fb51 100644 --- a/engine/main.py +++ b/engine/main.py @@ -59,13 +59,11 @@ class Data(TemplateData): session = self.parentSession #self.parentSession is already defined in template.data. We just want to work conveniently in init with it by creating a local var. self._processAfterInit() - def _processAfterInit(self): session = self.parentSession #We just want to work conveniently in init with it by creating a local var. self.cachedSerializedDataForStartEngine = None - def parseAndLoadInstrumentLibraries(self, baseSamplePath): """Called once by api.startEngine, which receives the global sample path from the GUI""" diff --git a/qtgui/instrument.py b/qtgui/instrument.py index e6893eb..7856923 100644 --- a/qtgui/instrument.py +++ b/qtgui/instrument.py @@ -438,6 +438,9 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem): self.mixSendDial.setEnabled(False) self.mixSendDial.setUpdatesEnabled(False) #this is a hack to make the widget disappear. Because hiding a QTreeWidgetItem does not work. + self.parentTreeController.parentMainWindow.qtApp.processEvents() #actually show the new state, even if mass unloading (which did not work before. But loading worked!) + + def _mixSendDialContextMenuEvent(self, event): if self._cachedInstrumentStatus["mixerEnabled"]: mixerMuteText = QtCore.QCoreApplication.translate("InstrumentMixerLevelContextMenu", "Mute/Disable Mixer-Send for {}".format(self.instrumentDict["name"])) diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index 6490b69..245e8b9 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -113,6 +113,7 @@ class MainWindow(TemplateMainWindow): #self.menu.addMenuEntry("menuEdit", "actionNils", "Nils", lambda: print("Merle")) self.menu.addMenuEntry("menuEdit", "actionSampleDirPathDialog", "Sample Files Location", ChooseDownloadDirectory) self.menu.addMenuEntry("menuEdit", "actionLoadSamples", QtCore.QCoreApplication.translate("Menu", "Load all Instrument Samples (slow!)"), api.loadAllInstrumentSamples) + self.menu.addMenuEntry("menuEdit", "actionUnloadSamples", QtCore.QCoreApplication.translate("Menu", "Unload all Instrument Samples (also slow.)"), api.unloadAllInstrumentSamples) #self.menu.connectMenuEntry("actionNils", lambda: print("Override")) self.menu.addSubmenu("menuView", QtCore.QCoreApplication.translate("Menu", "View"))