Assists music production by grouping standalone programs into sessions. Community version of "Non Session Manager".
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
11KB

  1. #! /usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. PyNSMClient - A New Session Manager Client-Library in one file.
  5. The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/
  6. New Session Manager, by LinuxAudio.org: https://github.com/linuxaudio/new-session-manager
  7. With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 )
  8. MIT License
  9. Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved.
  10. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  11. associated documentation files (the "Software"), to deal in the Software without restriction,
  12. including without limitation the rights to use, copy, modify, merge, publish, distribute,
  13. sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
  14. furnished to do so, subject to the following conditions:
  15. The above copyright notice and this permission notice shall be included in all copies or
  16. substantial portions of the Software.
  17. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
  18. NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  20. DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
  21. OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. """
  23. import sys #for qt app args.
  24. from os import getpid #we use this as jack meta data
  25. from PyQt5 import QtWidgets, QtCore
  26. #jack only
  27. import ctypes
  28. from random import uniform #to generate samples between -1.0 and 1.0 for Jack.
  29. #nsm only
  30. from nsmclient import NSMClient
  31. ########################################################################
  32. #Prepare the Qt Window
  33. ########################################################################
  34. class Main(QtWidgets.QWidget):
  35. def __init__(self, qtApp):
  36. super().__init__()
  37. self.qtApp = qtApp
  38. self.layout = QtWidgets.QVBoxLayout()
  39. self.layout.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
  40. self.setLayout(self.layout)
  41. niceTitle = "PyNSM v2 Example - JACK Noise"
  42. self.title = QtWidgets.QLabel("")
  43. self.saved = QtWidgets.QLabel("")
  44. self._value = QtWidgets.QSlider(orientation = 1) #horizontal
  45. self._value.setMinimum(0)
  46. self._value.setMaximum(100)
  47. self._value.setValue(50) #only visible for the first start.
  48. self.valueLabel = QtWidgets.QLabel("Noise Volume: " + str(self._value.value()))
  49. self._value.valueChanged.connect(lambda new: self.valueLabel.setText("Noise Volume: " + str(new)))
  50. self.layout.addWidget(self.title)
  51. self.layout.addWidget(self.saved)
  52. self.layout.addWidget(self._value)
  53. self.layout.addWidget(self.valueLabel)
  54. #Prepare the NSM Client
  55. #This has to be done as soon as possible because NSM provides paths and names for us.
  56. #and may quit if NSM environment var was not found.
  57. self.nsmClient = NSMClient(prettyName = niceTitle, #will raise an error and exit if this example is not run from NSM.
  58. supportsSaveStatus = True,
  59. saveCallback = self.saveCallback,
  60. openOrNewCallback = self.openOrNewCallback,
  61. exitProgramCallback = self.exitCallback,
  62. loggingLevel = "info", #"info" for development or debugging, "error" for production. default is error.
  63. )
  64. #If NSM did not start up properly the program quits here with an error message from NSM.
  65. #No JACK client gets created, no Qt window can be seen.
  66. self.title.setText("<b>" + self.nsmClient.ourClientNameUnderNSM + "</b>")
  67. self.eventLoop = QtCore.QTimer()
  68. self.eventLoop.start(100) #10ms-20ms is smooth for "real time" feeling. 100ms is still ok.
  69. self.eventLoop.timeout.connect(self.nsmClient.reactToMessage)
  70. #self.show is called as the new/open callback.
  71. @property
  72. def value(self):
  73. return str(self._value.value())
  74. @value.setter
  75. def value(self, new):
  76. new = int(new)
  77. self._value.setValue(new)
  78. def saveCallback(self, ourPath, sessionName, ourClientNameUnderNSM): #parameters are filled in by NSM.
  79. if self.value:
  80. with open(ourPath, "w") as f: #ourpath is taken as a filename here. We have this path name at our disposal and we know we only want one file. So we don't make a directory. This way we don't have to create a dir first.
  81. f.write(self.value)
  82. def openOrNewCallback(self, ourPath, sessionName, ourClientNameUnderNSM): #parameters are filled in by NSM.
  83. try:
  84. with open(ourPath, "r") as f:
  85. savedValue = f.read() #a string
  86. self.saved.setText("{}: {}".format(ourPath, savedValue))
  87. self.value = savedValue #internal casting to int. Sets the slider.
  88. except FileNotFoundError:
  89. self.saved.setText("{}: No save file found. Normal for first start.".format(ourPath))
  90. finally:
  91. self.show()
  92. def exitCallback(self, ourPath, sessionName, ourClientNameUnderNSM):
  93. """This function is a callback for NSM.
  94. We have a chance to close our clients and open connections here.
  95. If not nsmclient will just kill us no matter what
  96. """
  97. cjack.jack_remove_properties(ctypesJackClient, ctypesJackUuid) #clean our metadata
  98. cjack.jack_client_close(ctypesJackClient) #omitting this introduces problems. in Jack1 this would mute all jack clients for several seconds.
  99. exit() #or get SIGKILLed through NSM
  100. def closeEvent(self, event):
  101. """Qt likes to quits on its own. For example when the window manager closes the
  102. main window. Ignore that request and instead send a roundtrip through NSM"""
  103. self.nsmClient.serverSendExitToSelf()
  104. event.ignore()
  105. #Prepare the window instance. Gets executed at the end of this file.
  106. qtApp = QtWidgets.QApplication(sys.argv)
  107. ourClient = Main(qtApp)
  108. ########################################################################
  109. #Prepare the JACK Client
  110. #We need the client name from NSM first.
  111. ########################################################################
  112. cjack = ctypes.cdll.LoadLibrary("libjack.so.0")
  113. clientName = ourClient.nsmClient.prettyName #the nsm client is in the qt instance here. But in your program it can be anywhere.
  114. options = 0
  115. status = None
  116. class jack_client_t(ctypes.Structure):
  117. _fields_ = []
  118. cjack.jack_client_open.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int)] #the two ints are enum and pointer to enum. #http://jackaudio.org/files/docs/html/group__ClientFunctions.html#gab8b16ee616207532d0585d04a0bd1d60
  119. cjack.jack_client_open.restype = ctypes.POINTER(jack_client_t)
  120. ctypesJackClient = cjack.jack_client_open(clientName.encode("ascii"), options, status)
  121. #Create one output port
  122. class jack_port_t(ctypes.Structure):
  123. _fields_ = []
  124. JACK_DEFAULT_AUDIO_TYPE = "32 bit float mono audio".encode("ascii") #http://jackaudio.org/files/docs/html/types_8h.html
  125. JACK_PORT_IS_OUTPUT = 0x2 #http://jackaudio.org/files/docs/html/types_8h.html
  126. portname = "output".encode("ascii")
  127. cjack.jack_port_register.argtypes = [ctypes.POINTER(jack_client_t), ctypes.c_char_p, ctypes.c_char_p, ctypes.c_ulong, ctypes.c_ulong] #http://jackaudio.org/files/docs/html/group__PortFunctions.html#ga3e21d145c3c82d273a889272f0e405e7
  128. cjack.jack_port_register.restype = ctypes.POINTER(jack_port_t)
  129. outputPort = cjack.jack_port_register(ctypesJackClient, portname, JACK_DEFAULT_AUDIO_TYPE, JACK_PORT_IS_OUTPUT, 0)
  130. cjack.jack_client_close.argtypes = [ctypes.POINTER(jack_client_t),]
  131. #Create the callback
  132. #http://jackaudio.org/files/docs/html/group__ClientCallbacks.html#gafb5ec9fb4b736606d676c135fb97888b
  133. jack_nframes_t = ctypes.c_uint32
  134. cjack.jack_port_get_buffer.argtypes = [ctypes.POINTER(jack_port_t), jack_nframes_t]
  135. cjack.jack_port_get_buffer.restype = ctypes.POINTER(ctypes.c_float) #this is only valid for audio, not for midi. C Jack has a pointer to void here.
  136. def pythonJackCallback(nframes, void): #types: jack_nframes_t (ctypes.c_uint32), pointer to void
  137. """http://jackaudio.org/files/docs/html/simple__client_8c.html#a01271cc6cf692278ae35d0062935d7ae"""
  138. out = cjack.jack_port_get_buffer(outputPort, nframes) #out should be a pointer to jack_default_audio_sample_t (float, ctypes.POINTER(ctypes.c_float))
  139. #For each required sample
  140. for i in range(nframes):
  141. factor = ourClient._value.value() / 100
  142. val = ctypes.c_float(round(uniform(-0.5, 0.5) * factor, 10))
  143. out[i]= val
  144. return 0 # 0 on success, otherwise a non-zero error code, causing JACK to remove that client from the process() graph.
  145. JACK_CALLBACK_TYPE = ctypes.CFUNCTYPE(ctypes.c_int, jack_nframes_t, ctypes.c_void_p) #the first parameter is the return type, the following are input parameters
  146. callbackFunction = JACK_CALLBACK_TYPE(pythonJackCallback)
  147. cjack.jack_set_process_callback.argtypes = [ctypes.POINTER(jack_client_t), JACK_CALLBACK_TYPE, ctypes.c_void_p]
  148. cjack.jack_set_process_callback.restype = ctypes.c_uint32 #I think this is redundant since ctypes has int as default result type
  149. cjack.jack_set_process_callback(ctypesJackClient, callbackFunction, 0)
  150. #Ready. Activate the client.
  151. cjack.jack_activate(ctypesJackClient)
  152. #The Jack Processing functions gets called by jack in another thread. We just have to keep this program itself running. Qt does the job.
  153. #Jack Metadata - Inform the jack server about our program. Optional but has benefits when used with other programs that rely on metadata.
  154. #http://jackaudio.org/files/docs/html/group__Metadata.html
  155. jack_uuid_t = ctypes.c_uint64
  156. cjack.jack_set_property.argtypes = [ctypes.POINTER(jack_client_t), jack_uuid_t, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)] #client(we), subject/uuid,key,value/data,type
  157. cjack.jack_remove_properties.argtypes = [ctypes.POINTER(jack_client_t), jack_uuid_t] #for cleaning up when the program stops. the jack server can do it in newer jack versions, but this is safer.
  158. cjack.jack_get_uuid_for_client_name.argtypes = [ctypes.POINTER(jack_client_t), ctypes.c_char_p]
  159. cjack.jack_get_uuid_for_client_name.restype = ctypes.c_char_p
  160. ourJackUuid = cjack.jack_get_uuid_for_client_name(ctypesJackClient, clientName.encode("ascii"))
  161. ourJackUuid = int(ourJackUuid.decode("UTF-8"))
  162. ctypesJackUuid = jack_uuid_t(ourJackUuid)
  163. cjack.jack_set_property(ctypesJackClient, ctypesJackUuid, ctypes.c_char_p(b"pid"), ctypes.c_char_p(str(getpid()).encode()), None)
  164. ##################
  165. #Start everything
  166. qtApp.exec_()