D1024=int(D512/2)# set this to a number with many factors, like 210. According to http://homes.sice.indiana.edu/donbyrd/CMNExtremes.htm this is the real world limit.
ifnotint(D1024)==D1024:
logging.error(f"Warning: Lowest duration D0124 has decimal places: {D0124} but should be a plain integer. Your D4 value: {D4} has not enough 2^n factors. ")
logger.error(f"Warning: Lowest duration D0124 has decimal places: {D0124} but should be a plain integer. Your D4 value: {D4} has not enough 2^n factors. ")
#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.
#assert timesigDenom in traditionalNumberToBaseDuration, (timesigDenom, traditionalNumberToBaseDuration) For Laborejo this makes sense, but Patroneo has a fallback option with irregular timesigs: 8 steps in groups of 3 to make a quarter is valid. Results in "8/12"
#Most set config must be called before start audio. Set audio outputs seems to be an exception.
@ -110,13 +111,13 @@ class Session(object):
self.data=None#This makes debugging output nicer. If we init Data() here all errors will be presented as follow-up error "while handling exception FileNotFoundError".
except(NotADirectoryError,PermissionError)ase:
self.data=None
logging.error("Will not load or save because: "+e.__repr__())
logger.error("Will not load or save because: "+e.__repr__())
self.nsmOSCUrl=self.getNsmOSCUrl()#this fails and raises NSMNotRunningError if NSM is not available. Host programs can ignore it or exit their program.
self.realClient=True
self.cachedSaveStatus=True#save status checks for this.
self.cachedSaveStatus=None#save status checks for this.
logging.info(prettyName+":pynsm2: Starting PyNSM2 Client with logging level INFO. Switch to 'error' for a release!")#the NSM name is not ready yet so we just use the pretty name
logger.info("Starting PyNSM2 Client with logging level INFO. Switch to 'error' for a release!")#the NSM name is not ready yet so we just use the pretty name
raiseValueError("Unknown logging level: {}. Choose 'info' or 'error'".format(loggingLevel))
@ -281,23 +285,30 @@ class NSMClient(object):
self.saveCallback=saveCallback
self.exitProgramCallback=exitProgramCallback
self.openOrNewCallback=openOrNewCallback#The host needs to: Create a jack client with ourClientNameUnderNSM - Open the saved file and all its resources
self.hideGUICallback=hideGUICallbackifhideGUICallbackelseNone#if this stays None we don't ever need to check for it. This function will never be called by NSM anyway.
self.showGUICallback=showGUICallbackifshowGUICallbackelseNone#if this stays None we don't ever need to check for it. This function will never be called by NSM anyway.
#Hello source-code reader. You can add your own reactions here by nsmClient.reactions[oscpath]=func, where func gets the raw _IncomingMessage OSC object as argument.
#broadcast is handled directly by the function because it has more parameters
self.sock.bind(('',0))#pick a free port on localhost.
ip,port=self.sock.getsockname()
self.ourOscUrl=f"osc.udp://{ip}:{port}/"
self.executableName=self.getExecutableName()
@ -322,6 +333,50 @@ class NSMClient(object):
self.sock.setblocking(False)#We have waited for tha handshake. Now switch blocking off because we expect sock.recvfrom to be empty in 99.99...% of the time so we shouldn't wait for the answer.
#After this point the host must include self.reactToMessage in its event loop
#We assume we are save at startup.
self.announceSaveStatus(isClean=True)
defreactToMessage(self):
"""This is the main loop message. It is added to the clients event loop."""
try:
data,addr=self.sock.recvfrom(4096)#4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. However, messages will crash the program if they are bigger than 4096.
exceptBlockingIOError:#happens while no data is received. Has nothing to do with blocking or not.
returnNone
msg=_IncomingMessage(data)
ifmsg.oscpathinself.reactions:
self.reactions[msg.oscpath](msg)
elifmsg.oscpathinself.discardReactions:
pass
elifmsg.oscpath=="/reply"andmsg.params==["/nsm/server/open","Loaded."]:#NSM sends that all programs of the session were loaded.
logger.info("Got /reply Loaded from NSM Server")
elifmsg.oscpath=="/reply"andmsg.params==["/nsm/server/save","Saved."]:#NSM sends that all program-states are saved. Does only happen from the general save instruction, not when saving our client individually
logger.info("Got /reply Saved from NSM Server")
elifmsg.isBroadcast:
ifself.broadcastCallback:
logger.info(f"Got broadcast with messagePath {msg.oscpath} and listOfArguments {msg.params}")
logging.info(self.ourClientNameUnderNSM+":pynsm2: Got '/nsm/client/open' from NSM. Telling our client to load or create a file with name {}".format(self.ourPath))
logger.info("Got '/nsm/client/open' from NSM. Telling our client to load or create a file with name {}".format(self.ourPath))
self.openOrNewCallback(self.ourPath,self.sessionName,self.ourClientNameUnderNSM)#Host function to either load an existing session or create a new one.
logging.info(self.ourClientNameUnderNSM+":pynsm2: Our client should be done loading or creating the file {}".format(self.ourPath))
logger.info("Our client should be done loading or creating the file {}".format(self.ourPath))
replyToOpen=_OutgoingMessage("/reply")
replyToOpen.add_arg("/nsm/client/open")
replyToOpen.add_arg("{} is opened or created".format(self.prettyName))
#it is assumed that after saving the state is clear
self.announceSaveStatus(isClean=True)
defreactToMessage(self):
try:
data,addr=self.sock.recvfrom(4096)#4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. See next lines comment
exceptBlockingIOError:#happens while no data is received. Has nothing to do with blocking or not.
returnNone
msg=_IncomingMessage(data)#However, messages will crash the program if they are bigger than 4096.
ifmsg.oscpathinself.reactions:
self.reactions[msg.oscpath]()
elifmsg.oscpathinself.discardReactions:
pass
elifmsg.oscpath=="/reply"andmsg.params==["/nsm/server/open","Loaded."]:#NSM sends that all programs of the session were loaded.
logging.info(self.ourClientNameUnderNSM+":pynsm2: Got /reply Loaded from NSM Server")
elifmsg.oscpath=="/reply"andmsg.params==["/nsm/server/save","Saved."]:#NSM sends that all program-states are saved. Does only happen from the general save instruction, not when saving our client individually
logging.info(self.ourClientNameUnderNSM+":pynsm2: Got /reply Saved from NSM Server")
elifmsg.isBroadcast:
ifself.broadcastCallback:
logging.info(self.ourClientNameUnderNSM+f":pynsm2: Got broadcast with messagePath {msg.oscpath} and listOfArguments {msg.params}")
#There is a chance that exitProgramCallback will hang and the program won't quit. However, this is broken design and bad programming. We COULD place a timeout here and just kill after 10s or so, but that would make quitting our responsibility and fixing a broken thing.
#If we reach this point we have reached the point of no return. Say goodbye.
logging.warning(self.ourClientNameUnderNSM+":pynsm2: Client did not quit on its own. Sending SIGKILL.")
logger.warning("Client did not quit on its own. Sending SIGKILL.")
kill(getpid(),SIGKILL)
logging.error(self.ourClientNameUnderNSM+":pynsm2: pynsm2: SIGKILL did nothing. Do it manually.")
logger.error("SIGKILL did nothing. Do it manually.")
defdebugResetDataAndExit(self):
"""This is solely meant for debugging and testing. The user way of action should be to
logging.info(self.ourClientNameUnderNSM+":pynsm2: instructing the NSM-Server to send SIGTERM to ourselves.")
logger.info("instructing the NSM-Server to send SIGTERM to ourselves.")
if"server-control"inself.serverFeatures:
message=_OutgoingMessage("/nsm/server/stop")
message.add_arg("{}".format(self.ourClientId))
self.sock.sendto(message.build(),self.nsmOSCUrl)
else:
logging.warning(self.ourClientNameUnderNSM+":pynsm2: ...but the NSM-Server does not support server control. Quitting on our own. Server only supports: {}".format(self.serverFeatures))
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."
logging.warning(self.ourClientNameUnderNSM+":pynsm2: ...but the NSM-Server does not support server control. Server only supports: {}".format(self.serverFeatures))
logger.warning("...but the NSM-Server does not support server control. Server only supports: {}".format(self.serverFeatures))
defchangeLabel(self,label:str):
"""This function is implemented because it is provided by NSM. However, it does not much.
#loadResource from our session dir. Portable session, manually copied beforehand or just loading a link again.
linkedPath=filePath#we could return here, but we continue to get the tests below.
logging.info(self.ourClientNameUnderNSM+f":pynsm2: tried to import external resource {filePath} but this is already in our session directory. We use this file directly instead. ")
logger.info(f"tried to import external resource {filePath} but this is already in our session directory. We use this file directly instead. ")
#the imported file already exists as link in our session dir. We do not link it again but simply report the existing link.
#We only check for the first target of the existing link and do not follow it through to a real file.
#This way all user abstractions and file structures will be honored.
linkedPath=linkedPath
logging.info(self.ourClientNameUnderNSM+f":pynsm2: tried to import external resource {filePath} but this was already linked to our session directory before. We use the old link: {linkedPath}")
logger.info(f"tried to import external resource {filePath} but this was already linked to our session directory before. We use the old link: {linkedPath}")
eliflinkedPathAlreadyExists:
#A new file shall be imported but it would create a linked name which already exists in our session dir.
logging.info(self.ourClientNameUnderNSM+f":pysm2: tried to import external resource {filePath} but potential target link {linkedPath} already exists. Linked to {uniqueLinkedPath} instead.")
logger.info(self.ourClientNameUnderNSM+f":pysm2: tried to import external resource {filePath} but potential target link {linkedPath} already exists. Linked to {uniqueLinkedPath} instead.")
linkedPath=uniqueLinkedPath
else:#this is the "normal" case. External resources will be linked.
assertnotos.path.exists(linkedPath)
os.symlink(filePath,linkedPath)
logging.info(self.ourClientNameUnderNSM+f":pynsm2: imported external resource {filePath} as link {linkedPath}")
logger.info(f"imported external resource {filePath} as link {linkedPath}")
#else try to use system-wide calfbox. Check for this and if the .so exists at the end of this file.
logging.info("PATHS: {}".format(PATHS))
logger.info("PATHS: {}".format(PATHS))
defexitWithMessage(message:str):
@ -231,7 +235,7 @@ def profiler(*pargs, **kwds):
fromtempfileimportNamedTemporaryFile
cprofPath=NamedTemporaryFile().name+".cprof"
pr.dump_stats(cprofPath)
logging.info("{}: write profiling data to {}".format(METADATA["name"],cprofPath))
logger.info("{}: write profiling data to {}".format(METADATA["name"],cprofPath))
print(f"pyprof2calltree -k -i {cprofPath}")
pr=cProfile.Profile()
@ -243,6 +247,18 @@ def profiler(*pargs, **kwds):
#Program execution
yield
#Catch Exceptions even if PyQt crashes.
importsys
sys._excepthook=sys.excepthook
defexception_hook(exctype,value,traceback):
"""This hook purely exists to call sys.exit(1) even on a Qt crash
sothatatexitgetstriggered"""
#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=exception_hook
defstartPseudoNSMServer(path):
fromosimportgetenv
@ -288,13 +304,13 @@ if args.mute:
#Make sure calfbox is available.
if"CALFBOXLIBABSPATH"inos.environ:
logging.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:
logging.info("Looking for calfbox shared library systemwide through ctypes.util.find_library")
logger.info("Looking for calfbox shared library systemwide through ctypes.util.find_library")
try:
fromcalfboximportcbox
logging.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__)))