Browse Source

update template

master
Nils 4 years ago
parent
commit
1826a60d51
  1. 7
      template/engine/api.py
  2. 49
      template/engine/sampler_sf2.py
  3. 4
      template/engine/session.py
  4. 6
      template/helper.py
  5. 2
      template/qtgui/mainwindow.py
  6. 20
      template/qtgui/nsmclient.py
  7. 78
      template/start.py

7
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.

49
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

4
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:

6
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'

2
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.

20
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.

78
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)

Loading…
Cancel
Save