Collection of tools useful for audio production
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.

474 lines
16KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # JACK-Capture frontend, with freewheel and transport support
  4. # Copyright (C) 2010-2018 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # For a full copy of the GNU General Public License see the COPYING file
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Global)
  19. from time import sleep
  20. if True:
  21. from PyQt5.QtCore import pyqtSlot, QProcess, QTime, QTimer, QSettings
  22. from PyQt5.QtWidgets import QDialog
  23. else:
  24. from PyQt4.QtCore import pyqtSlot, QProcess, QTime, QTimer, QSettings
  25. from PyQt4.QtGui import QDialog
  26. # ------------------------------------------------------------------------------------------------------------
  27. # Imports (Custom Stuff)
  28. import ui_render
  29. from shared import *
  30. from jacklib_helpers import *
  31. # ------------------------------------------------------------------------------------------------------------
  32. # Global variables
  33. global gJackCapturePath, gJackClient
  34. gJackCapturePath = ""
  35. gJackClient = None
  36. # ------------------------------------------------------------------------------------------------------------
  37. # Find 'jack_capture'
  38. # Check for cxfreeze
  39. if sys.path[0].rsplit(os.sep, 1)[-1] in ("catia", "claudia", "cadence-render", "render"):
  40. name = sys.path[0].rsplit(os.sep, 1)[-1]
  41. cwd = sys.path[0].rsplit(name, 1)[0]
  42. if os.path.exists(os.path.join(cwd, "jack_capture")):
  43. gJackCapturePath = os.path.join(cwd, "jack_capture")
  44. del cwd, name
  45. # Check in PATH
  46. if not gJackCapturePath:
  47. for iPATH in PATH:
  48. if os.path.exists(os.path.join(iPATH, "jack_capture")):
  49. gJackCapturePath = os.path.join(iPATH, "jack_capture")
  50. break
  51. def canRender():
  52. global gJackCapturePath
  53. return bool(gJackCapturePath)
  54. # ------------------------------------------------------------------------------------------------------------
  55. # Render Window
  56. class RenderW(QDialog):
  57. def __init__(self, parent):
  58. QDialog.__init__(self, parent)
  59. self.ui = ui_render.Ui_RenderW()
  60. self.ui.setupUi(self)
  61. self.fFreewheel = False
  62. self.fLastTime = -1
  63. self.fMaxTime = 180
  64. self.fTimer = QTimer(self)
  65. self.fProcess = QProcess(self)
  66. # -------------------------------------------------------------
  67. # Get JACK client and base information
  68. global gJackClient
  69. if gJackClient:
  70. self.fJackClient = gJackClient
  71. else:
  72. self.fJackClient = jacklib.client_open("Render-Dialog", jacklib.JackNoStartServer, None)
  73. self.fBufferSize = int(jacklib.get_buffer_size(self.fJackClient))
  74. self.fSampleRate = int(jacklib.get_sample_rate(self.fJackClient))
  75. for i in range(self.ui.cb_buffer_size.count()):
  76. if int(self.ui.cb_buffer_size.itemText(i)) == self.fBufferSize:
  77. self.ui.cb_buffer_size.setCurrentIndex(i)
  78. break
  79. else:
  80. self.ui.cb_buffer_size.addItem(str(self.fBufferSize))
  81. self.ui.cb_buffer_size.setCurrentIndex(self.ui.cb_buffer_size.count() - 1)
  82. # -------------------------------------------------------------
  83. # Set-up GUI stuff
  84. # Get List of formats
  85. self.fProcess.start(gJackCapturePath, ["-pf"])
  86. self.fProcess.waitForFinished()
  87. formats = str(self.fProcess.readAllStandardOutput(), encoding="utf-8").split(" ")
  88. formatsList = []
  89. for i in range(len(formats) - 1):
  90. iFormat = formats[i].strip()
  91. if iFormat:
  92. formatsList.append(iFormat)
  93. formatsList.sort()
  94. # Put all formats in combo-box, select 'wav' option
  95. for i in range(len(formatsList)):
  96. self.ui.cb_format.addItem(formatsList[i])
  97. if formatsList[i] == "wav":
  98. self.ui.cb_format.setCurrentIndex(i)
  99. self.ui.cb_depth.setCurrentIndex(4) #Float
  100. self.ui.rb_stereo.setChecked(True)
  101. self.ui.te_end.setTime(QTime(0, 3, 0))
  102. self.ui.progressBar.setFormat("")
  103. self.ui.progressBar.setMinimum(0)
  104. self.ui.progressBar.setMaximum(1)
  105. self.ui.progressBar.setValue(0)
  106. self.ui.b_render.setIcon(getIcon("media-record"))
  107. self.ui.b_stop.setIcon(getIcon("media-playback-stop"))
  108. self.ui.b_close.setIcon(getIcon("window-close"))
  109. self.ui.b_open.setIcon(getIcon("document-open"))
  110. self.ui.b_stop.setVisible(False)
  111. self.ui.le_folder.setText(HOME)
  112. # -------------------------------------------------------------
  113. # Set-up connections
  114. self.ui.b_render.clicked.connect(self.slot_renderStart)
  115. self.ui.b_stop.clicked.connect(self.slot_renderStop)
  116. self.ui.b_open.clicked.connect(self.slot_getAndSetPath)
  117. self.ui.b_now_start.clicked.connect(self.slot_setStartNow)
  118. self.ui.b_now_end.clicked.connect(self.slot_setEndNow)
  119. self.ui.te_start.timeChanged.connect(self.slot_updateStartTime)
  120. self.ui.te_end.timeChanged.connect(self.slot_updateEndTime)
  121. self.ui.group_time.clicked.connect(self.slot_transportChecked)
  122. self.fTimer.timeout.connect(self.slot_updateProgressbar)
  123. # -------------------------------------------------------------
  124. self.loadSettings()
  125. @pyqtSlot()
  126. def slot_renderStart(self):
  127. if not os.path.exists(self.ui.le_folder.text()):
  128. QMessageBox.warning(self, self.tr("Warning"), self.tr("The selected directory does not exist. Please choose a valid one."))
  129. return
  130. timeStart = self.ui.te_start.time()
  131. timeEnd = self.ui.te_end.time()
  132. minTime = (timeStart.hour() * 3600) + (timeStart.minute() * 60) + (timeStart.second())
  133. maxTime = (timeEnd.hour() * 3600) + (timeEnd.minute() * 60) + (timeEnd.second())
  134. newBufferSize = int(self.ui.cb_buffer_size.currentText())
  135. useTransport = self.ui.group_time.isChecked()
  136. self.fFreewheel = bool(self.ui.cb_render_mode.currentIndex() == 1)
  137. self.fLastTime = -1
  138. self.fMaxTime = maxTime
  139. if self.fFreewheel:
  140. self.fTimer.setInterval(100)
  141. else:
  142. self.fTimer.setInterval(500)
  143. self.ui.group_render.setEnabled(False)
  144. self.ui.group_time.setEnabled(False)
  145. self.ui.group_encoding.setEnabled(False)
  146. self.ui.b_render.setVisible(False)
  147. self.ui.b_stop.setVisible(True)
  148. self.ui.b_close.setEnabled(False)
  149. if useTransport:
  150. self.ui.progressBar.setFormat("%p%")
  151. self.ui.progressBar.setMinimum(minTime)
  152. self.ui.progressBar.setMaximum(maxTime)
  153. self.ui.progressBar.setValue(minTime)
  154. else:
  155. self.ui.progressBar.setFormat("")
  156. self.ui.progressBar.setMinimum(0)
  157. self.ui.progressBar.setMaximum(0)
  158. self.ui.progressBar.setValue(0)
  159. self.ui.progressBar.update()
  160. arguments = []
  161. # Filename prefix
  162. arguments.append("-fp")
  163. arguments.append(self.ui.le_prefix.text())
  164. # Format
  165. arguments.append("-f")
  166. arguments.append(self.ui.cb_format.currentText())
  167. # Bit depth
  168. arguments.append("-b")
  169. arguments.append(self.ui.cb_depth.currentText())
  170. # Channels
  171. arguments.append("-c")
  172. if self.ui.rb_mono.isChecked():
  173. arguments.append("1")
  174. elif self.ui.rb_stereo.isChecked():
  175. arguments.append("2")
  176. else:
  177. arguments.append(str(self.ui.sb_channels.value()))
  178. # Controlled only by freewheel
  179. if self.fFreewheel:
  180. arguments.append("-jf")
  181. # Controlled by transport
  182. elif useTransport:
  183. arguments.append("-jt")
  184. # Silent mode
  185. arguments.append("-dc")
  186. arguments.append("-s")
  187. # Change current directory
  188. os.chdir(self.ui.le_folder.text())
  189. if newBufferSize != int(jacklib.get_buffer_size(self.fJackClient)):
  190. print("NOTICE: buffer size changed before render")
  191. jacklib.set_buffer_size(self.fJackClient, newBufferSize)
  192. if useTransport:
  193. if jacklib.transport_query(self.fJackClient, None) > jacklib.JackTransportStopped: # rolling or starting
  194. jacklib.transport_stop(self.fJackClient)
  195. jacklib.transport_locate(self.fJackClient, minTime * self.fSampleRate)
  196. self.fProcess.start(gJackCapturePath, arguments)
  197. self.fProcess.waitForStarted()
  198. if self.fFreewheel:
  199. print("NOTICE: rendering in freewheel mode")
  200. sleep(1)
  201. jacklib.set_freewheel(self.fJackClient, 1)
  202. if useTransport:
  203. self.fTimer.start()
  204. jacklib.transport_start(self.fJackClient)
  205. @pyqtSlot()
  206. def slot_renderStop(self):
  207. useTransport = self.ui.group_time.isChecked()
  208. if useTransport:
  209. jacklib.transport_stop(self.fJackClient)
  210. if self.fFreewheel:
  211. jacklib.set_freewheel(self.fJackClient, 0)
  212. sleep(1)
  213. self.fProcess.terminate()
  214. #self.fProcess.waitForFinished(5000)
  215. if useTransport:
  216. self.fTimer.stop()
  217. self.ui.group_render.setEnabled(True)
  218. self.ui.group_time.setEnabled(True)
  219. self.ui.group_encoding.setEnabled(True)
  220. self.ui.b_render.setVisible(True)
  221. self.ui.b_stop.setVisible(False)
  222. self.ui.b_close.setEnabled(True)
  223. self.ui.progressBar.setFormat("")
  224. self.ui.progressBar.setMinimum(0)
  225. self.ui.progressBar.setMaximum(1)
  226. self.ui.progressBar.setValue(0)
  227. self.ui.progressBar.update()
  228. # Restore buffer size
  229. newBufferSize = int(jacklib.get_buffer_size(self.fJackClient))
  230. if newBufferSize != self.fBufferSize:
  231. jacklib.set_buffer_size(self.fJackClient, newBufferSize)
  232. @pyqtSlot()
  233. def slot_getAndSetPath(self):
  234. getAndSetPath(self, self.ui.le_folder.text(), self.ui.le_folder)
  235. @pyqtSlot()
  236. def slot_setStartNow(self):
  237. time = int(jacklib.get_current_transport_frame(self.fJackClient) / self.fSampleRate)
  238. secs = time % 60
  239. mins = int(time / 60) % 60
  240. hrs = int(time / 3600) % 60
  241. self.ui.te_start.setTime(QTime(hrs, mins, secs))
  242. @pyqtSlot()
  243. def slot_setEndNow(self):
  244. time = int(jacklib.get_current_transport_frame(self.fJackClient) / self.fSampleRate)
  245. secs = time % 60
  246. mins = int(time / 60) % 60
  247. hrs = int(time / 3600) % 60
  248. self.ui.te_end.setTime(QTime(hrs, mins, secs))
  249. @pyqtSlot(QTime)
  250. def slot_updateStartTime(self, time):
  251. if time >= self.ui.te_end.time():
  252. self.ui.te_end.setTime(time)
  253. renderEnabled = False
  254. else:
  255. renderEnabled = True
  256. if self.ui.group_time.isChecked():
  257. self.ui.b_render.setEnabled(renderEnabled)
  258. @pyqtSlot(QTime)
  259. def slot_updateEndTime(self, time):
  260. if time <= self.ui.te_start.time():
  261. self.ui.te_start.setTime(time)
  262. renderEnabled = False
  263. else:
  264. renderEnabled = True
  265. if self.ui.group_time.isChecked():
  266. self.ui.b_render.setEnabled(renderEnabled)
  267. @pyqtSlot(bool)
  268. def slot_transportChecked(self, yesNo):
  269. if yesNo:
  270. renderEnabled = bool(self.ui.te_end.time() > self.ui.te_start.time())
  271. else:
  272. renderEnabled = True
  273. self.ui.b_render.setEnabled(renderEnabled)
  274. @pyqtSlot()
  275. def slot_updateProgressbar(self):
  276. time = int(jacklib.get_current_transport_frame(self.fJackClient)) / self.fSampleRate
  277. self.ui.progressBar.setValue(time)
  278. if time > self.fMaxTime or (self.fLastTime > time and not self.fFreewheel):
  279. self.slot_renderStop()
  280. self.fLastTime = time
  281. def saveSettings(self):
  282. settings = QSettings("Cadence", "Cadence-Render")
  283. if self.ui.rb_mono.isChecked():
  284. channels = 1
  285. elif self.ui.rb_stereo.isChecked():
  286. channels = 2
  287. else:
  288. channels = self.ui.sb_channels.value()
  289. settings.setValue("Geometry", self.saveGeometry())
  290. settings.setValue("OutputFolder", self.ui.le_folder.text())
  291. settings.setValue("FilenamePrefix", self.ui.le_prefix.text())
  292. settings.setValue("EncodingFormat", self.ui.cb_format.currentText())
  293. settings.setValue("EncodingDepth", self.ui.cb_depth.currentText())
  294. settings.setValue("EncodingChannels", channels)
  295. settings.setValue("UseTransport", self.ui.group_time.isChecked())
  296. settings.setValue("StartTime", self.ui.te_start.time())
  297. settings.setValue("EndTime", self.ui.te_end.time())
  298. def loadSettings(self):
  299. settings = QSettings("Cadence", "Cadence-Render")
  300. self.restoreGeometry(settings.value("Geometry", b""))
  301. outputFolder = settings.value("OutputFolder", HOME)
  302. if os.path.exists(outputFolder):
  303. self.ui.le_folder.setText(outputFolder)
  304. self.ui.le_prefix.setText(settings.value("FilenamePrefix", "jack_capture_"))
  305. encFormat = settings.value("EncodingFormat", "Wav", type=str)
  306. for i in range(self.ui.cb_format.count()):
  307. if self.ui.cb_format.itemText(i) == encFormat:
  308. self.ui.cb_format.setCurrentIndex(i)
  309. break
  310. encDepth = settings.value("EncodingDepth", "Float", type=str)
  311. for i in range(self.ui.cb_depth.count()):
  312. if self.ui.cb_depth.itemText(i) == encDepth:
  313. self.ui.cb_depth.setCurrentIndex(i)
  314. break
  315. encChannels = settings.value("EncodingChannels", 2, type=int)
  316. if encChannels == 1:
  317. self.ui.rb_mono.setChecked(True)
  318. elif encChannels == 2:
  319. self.ui.rb_stereo.setChecked(True)
  320. else:
  321. self.ui.rb_outro.setChecked(True)
  322. self.ui.sb_channels.setValue(encChannels)
  323. self.ui.group_time.setChecked(settings.value("UseTransport", False, type=bool))
  324. self.ui.te_start.setTime(settings.value("StartTime", self.ui.te_start.time(), type=QTime))
  325. self.ui.te_end.setTime(settings.value("EndTime", self.ui.te_end.time(), type=QTime))
  326. def closeEvent(self, event):
  327. self.saveSettings()
  328. if self.fJackClient:
  329. jacklib.client_close(self.fJackClient)
  330. QDialog.closeEvent(self, event)
  331. def done(self, r):
  332. QDialog.done(self, r)
  333. self.close()
  334. # ------------------------------------------------------------------------------------------------------------
  335. # Allow to use this as a standalone app
  336. if __name__ == '__main__':
  337. # Additional imports
  338. from PyQt5.QtWidgets import QApplication
  339. # App initialization
  340. app = QApplication(sys.argv)
  341. app.setApplicationName("Cadence-Render")
  342. app.setApplicationVersion(VERSION)
  343. app.setOrganizationName("Cadence")
  344. app.setWindowIcon(QIcon(":/scalable/cadence.svg"))
  345. if jacklib is None:
  346. QMessageBox.critical(None, app.translate("RenderW", "Error"), app.translate("RenderW",
  347. "JACK is not available in this system, cannot use this application."))
  348. sys.exit(1)
  349. if not gJackCapturePath:
  350. QMessageBox.critical(None, app.translate("RenderW", "Error"), app.translate("RenderW",
  351. "The 'jack_capture' application is not available.\n"
  352. "Is not possible to render without it!"))
  353. sys.exit(2)
  354. jackStatus = jacklib.jack_status_t(0x0)
  355. gJackClient = jacklib.client_open("Render", jacklib.JackNoStartServer, jacklib.pointer(jackStatus))
  356. if not gJackClient:
  357. errorString = get_jack_status_error_string(jackStatus)
  358. QMessageBox.critical(None, app.translate("RenderW", "Error"), app.translate("RenderW",
  359. "Could not connect to JACK, possible reasons:\n"
  360. "%s" % errorString))
  361. sys.exit(1)
  362. # Show GUI
  363. gui = RenderW(None)
  364. gui.setWindowIcon(getIcon("media-record", 48))
  365. gui.show()
  366. # App-Loop
  367. sys.exit(app.exec_())