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.

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