Audio plugin host https://kx.studio/carla
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.

646 lines
23KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla plugin host (plugin UI)
  4. # Copyright (C) 2013-2016 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation; either version 2 of
  9. # the License, or 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 GPL.txt file
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Config)
  19. from carla_config import *
  20. # ------------------------------------------------------------------------------------------------------------
  21. # Imports (Global)
  22. if config_UseQt5:
  23. from PyQt5.QtGui import QKeySequence
  24. from PyQt5.QtWidgets import QHBoxLayout
  25. else:
  26. from PyQt4.QtGui import QHBoxLayout, QKeySequence
  27. # ------------------------------------------------------------------------------------------------------------
  28. # Imports (Custom Stuff)
  29. from carla_host import *
  30. from externalui import ExternalUI
  31. # ------------------------------------------------------------------------------------------------------------
  32. # Host Plugin object
  33. class PluginHost(CarlaHostQtPlugin):
  34. def __init__(self):
  35. CarlaHostQtPlugin.__init__(self)
  36. if False:
  37. # kdevelop likes this :)
  38. self.fExternalUI = ExternalUI()
  39. # ---------------------------------------------------------------
  40. self.fExternalUI = None
  41. # -------------------------------------------------------------------
  42. def setExternalUI(self, extUI):
  43. self.fExternalUI = extUI
  44. def sendMsg(self, lines):
  45. if self.fExternalUI is None:
  46. return False
  47. return self.fExternalUI.send(lines)
  48. # -------------------------------------------------------------------
  49. def engine_init(self, driverName, clientName):
  50. return True
  51. def engine_close(self):
  52. return True
  53. def engine_idle(self):
  54. self.fExternalUI.idleExternalUI()
  55. def is_engine_running(self):
  56. if self.fExternalUI is None:
  57. return False
  58. return self.fExternalUI.isRunning()
  59. def set_engine_about_to_close(self):
  60. return True
  61. # ------------------------------------------------------------------------------------------------------------
  62. # Main Window
  63. class CarlaMiniW(ExternalUI, HostWindow):
  64. def __init__(self, host, isPatchbay, parent=None):
  65. ExternalUI.__init__(self)
  66. HostWindow.__init__(self, host, isPatchbay, parent)
  67. self.host = host
  68. if False:
  69. # kdevelop likes this :)
  70. host = PluginHost()
  71. self.host = host
  72. host.setExternalUI(self)
  73. self.fFirstInit = True
  74. self.setWindowTitle(self.fUiName)
  75. self.ready()
  76. # Override this as it can be called from several places.
  77. # We really need to close all UIs as events are driven by host idle which is only available when UI is visible
  78. def closeExternalUI(self):
  79. for i in reversed(range(self.fPluginCount)):
  80. self.host.show_custom_ui(i, False)
  81. ExternalUI.closeExternalUI(self)
  82. # -------------------------------------------------------------------
  83. # ExternalUI Callbacks
  84. def uiShow(self):
  85. if self.parent() is not None:
  86. return
  87. self.show()
  88. def uiFocus(self):
  89. if self.parent() is not None:
  90. return
  91. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  92. self.show()
  93. self.raise_()
  94. self.activateWindow()
  95. def uiHide(self):
  96. if self.parent() is not None:
  97. return
  98. self.hide()
  99. def uiQuit(self):
  100. self.closeExternalUI()
  101. self.close()
  102. if self != gui:
  103. gui.close()
  104. # there might be other qt windows open which will block carla-plugin from quitting
  105. app.quit()
  106. def uiTitleChanged(self, uiTitle):
  107. self.setWindowTitle(uiTitle)
  108. # -------------------------------------------------------------------
  109. # Qt events
  110. def closeEvent(self, event):
  111. self.closeExternalUI()
  112. HostWindow.closeEvent(self, event)
  113. # there might be other qt windows open which will block carla-plugin from quitting
  114. app.quit()
  115. # -------------------------------------------------------------------
  116. # Custom callback
  117. def msgCallback(self, msg):
  118. try:
  119. self.msgCallback2(msg)
  120. except:
  121. print("msgCallback error, skipped for", msg)
  122. def msgCallback2(self, msg):
  123. msg = charPtrToString(msg)
  124. #if not msg:
  125. #return
  126. if msg.startswith("PEAKS_"):
  127. pluginId = int(msg.replace("PEAKS_", ""))
  128. in1, in2, out1, out2 = [float(i) for i in self.readlineblock().split(":")]
  129. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  130. elif msg.startswith("PARAMVAL_"):
  131. pluginId, paramId = [int(i) for i in msg.replace("PARAMVAL_", "").split(":")]
  132. paramValue = float(self.readlineblock())
  133. if paramId < 0:
  134. self.host._set_internalValue(pluginId, paramId, paramValue)
  135. else:
  136. self.host._set_parameterValue(pluginId, paramId, paramValue)
  137. elif msg.startswith("ENGINE_CALLBACK_"):
  138. action = int(msg.replace("ENGINE_CALLBACK_", ""))
  139. pluginId = int(self.readlineblock())
  140. value1 = int(self.readlineblock())
  141. value2 = int(self.readlineblock())
  142. value3 = float(self.readlineblock())
  143. valueStr = self.readlineblock().replace("\r", "\n")
  144. if action == ENGINE_CALLBACK_PLUGIN_RENAMED:
  145. self.host._set_pluginName(pluginId, valueStr)
  146. elif action == ENGINE_CALLBACK_PARAMETER_VALUE_CHANGED:
  147. if value1 < 0:
  148. self.host._set_internalValue(pluginId, value1, value3)
  149. else:
  150. self.host._set_parameterValue(pluginId, value1, value3)
  151. elif action == ENGINE_CALLBACK_PARAMETER_DEFAULT_CHANGED:
  152. self.host._set_parameterDefault(pluginId, value1, value3)
  153. elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CC_CHANGED:
  154. self.host._set_parameterMidiCC(pluginId, value1, value2)
  155. elif action == ENGINE_CALLBACK_PARAMETER_MIDI_CHANNEL_CHANGED:
  156. self.host._set_parameterMidiChannel(pluginId, value1, value2)
  157. elif action == ENGINE_CALLBACK_PROGRAM_CHANGED:
  158. self.host._set_currentProgram(pluginId, value1)
  159. elif action == ENGINE_CALLBACK_MIDI_PROGRAM_CHANGED:
  160. self.host._set_currentMidiProgram(pluginId, value1)
  161. engineCallback(self.host, action, pluginId, value1, value2, value3, valueStr)
  162. elif msg.startswith("ENGINE_OPTION_"):
  163. option = int(msg.replace("ENGINE_OPTION_", ""))
  164. forced = bool(self.readlineblock() == "true")
  165. value = self.readlineblock()
  166. if self.fFirstInit and not forced:
  167. return
  168. if option == ENGINE_OPTION_PROCESS_MODE:
  169. self.host.processMode = int(value)
  170. elif option == ENGINE_OPTION_TRANSPORT_MODE:
  171. self.host.transportMode = int(value)
  172. elif option == ENGINE_OPTION_FORCE_STEREO:
  173. self.host.forceStereo = bool(value == "true")
  174. elif option == ENGINE_OPTION_PREFER_PLUGIN_BRIDGES:
  175. self.host.preferPluginBridges = bool(value == "true")
  176. elif option == ENGINE_OPTION_PREFER_UI_BRIDGES:
  177. self.host.preferUIBridges = bool(value == "true")
  178. elif option == ENGINE_OPTION_UIS_ALWAYS_ON_TOP:
  179. self.host.uisAlwaysOnTop = bool(value == "true")
  180. elif option == ENGINE_OPTION_MAX_PARAMETERS:
  181. self.host.maxParameters = int(value)
  182. elif option == ENGINE_OPTION_UI_BRIDGES_TIMEOUT:
  183. self.host.uiBridgesTimeout = int(value)
  184. elif option == ENGINE_OPTION_PATH_BINARIES:
  185. self.host.pathBinaries = value
  186. elif option == ENGINE_OPTION_PATH_RESOURCES:
  187. self.host.pathResources = value
  188. elif msg.startswith("PLUGIN_INFO_"):
  189. pluginId = int(msg.replace("PLUGIN_INFO_", ""))
  190. self.host._add(pluginId)
  191. type_, category, hints, uniqueId, optsAvail, optsEnabled = [int(i) for i in self.readlineblock().split(":")]
  192. filename = self.readlineblock().replace("\r", "\n")
  193. name = self.readlineblock().replace("\r", "\n")
  194. iconName = self.readlineblock().replace("\r", "\n")
  195. realName = self.readlineblock().replace("\r", "\n")
  196. label = self.readlineblock().replace("\r", "\n")
  197. maker = self.readlineblock().replace("\r", "\n")
  198. copyright = self.readlineblock().replace("\r", "\n")
  199. pinfo = {
  200. 'type': type_,
  201. 'category': category,
  202. 'hints': hints,
  203. 'optionsAvailable': optsAvail,
  204. 'optionsEnabled': optsEnabled,
  205. 'filename': filename,
  206. 'name': name,
  207. 'label': label,
  208. 'maker': maker,
  209. 'copyright': copyright,
  210. 'iconName': iconName,
  211. 'patchbayClientId': 0,
  212. 'uniqueId': uniqueId
  213. }
  214. self.host._set_pluginInfo(pluginId, pinfo)
  215. self.host._set_pluginRealName(pluginId, realName)
  216. elif msg.startswith("AUDIO_COUNT_"):
  217. pluginId, ins, outs = [int(i) for i in msg.replace("AUDIO_COUNT_", "").split(":")]
  218. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  219. elif msg.startswith("MIDI_COUNT_"):
  220. pluginId, ins, outs = [int(i) for i in msg.replace("MIDI_COUNT_", "").split(":")]
  221. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  222. elif msg.startswith("PARAMETER_COUNT_"):
  223. pluginId, ins, outs, count = [int(i) for i in msg.replace("PARAMETER_COUNT_", "").split(":")]
  224. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  225. elif msg.startswith("PARAMETER_DATA_"):
  226. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_DATA_", "").split(":")]
  227. paramType, paramHints, midiChannel, midiCC = [int(i) for i in self.readlineblock().split(":")]
  228. paramName = self.readlineblock().replace("\r", "\n")
  229. paramUnit = self.readlineblock().replace("\r", "\n")
  230. paramInfo = {
  231. 'name': paramName,
  232. 'symbol': "",
  233. 'unit': paramUnit,
  234. 'scalePointCount': 0,
  235. }
  236. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  237. paramData = {
  238. 'type': paramType,
  239. 'hints': paramHints,
  240. 'index': paramId,
  241. 'rindex': -1,
  242. 'midiCC': midiCC,
  243. 'midiChannel': midiChannel
  244. }
  245. self.host._set_parameterData(pluginId, paramId, paramData)
  246. elif msg.startswith("PARAMETER_RANGES_"):
  247. pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_RANGES_", "").split(":")]
  248. def_, min_, max_, step, stepSmall, stepLarge = [float(i) for i in self.readlineblock().split(":")]
  249. paramRanges = {
  250. 'def': def_,
  251. 'min': min_,
  252. 'max': max_,
  253. 'step': step,
  254. 'stepSmall': stepSmall,
  255. 'stepLarge': stepLarge
  256. }
  257. self.host._set_parameterRanges(pluginId, paramId, paramRanges)
  258. elif msg.startswith("PROGRAM_COUNT_"):
  259. pluginId, count, current = [int(i) for i in msg.replace("PROGRAM_COUNT_", "").split(":")]
  260. self.host._set_programCount(pluginId, count)
  261. self.host._set_currentProgram(pluginId, current)
  262. elif msg.startswith("PROGRAM_NAME_"):
  263. pluginId, progId = [int(i) for i in msg.replace("PROGRAM_NAME_", "").split(":")]
  264. progName = self.readlineblock().replace("\r", "\n")
  265. self.host._set_programName(pluginId, progId, progName)
  266. elif msg.startswith("MIDI_PROGRAM_COUNT_"):
  267. pluginId, count, current = [int(i) for i in msg.replace("MIDI_PROGRAM_COUNT_", "").split(":")]
  268. self.host._set_midiProgramCount(pluginId, count)
  269. self.host._set_currentMidiProgram(pluginId, current)
  270. elif msg.startswith("MIDI_PROGRAM_DATA_"):
  271. pluginId, midiProgId = [int(i) for i in msg.replace("MIDI_PROGRAM_DATA_", "").split(":")]
  272. bank, program = [int(i) for i in self.readlineblock().split(":")]
  273. name = self.readlineblock().replace("\r", "\n")
  274. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  275. elif msg.startswith("CUSTOM_DATA_COUNT_"):
  276. pluginId, count = [int(i) for i in msg.replace("CUSTOM_DATA_COUNT_", "").split(":")]
  277. self.host._set_customDataCount(pluginId, count)
  278. elif msg.startswith("CUSTOM_DATA_"):
  279. pluginId, customDataId = [int(i) for i in msg.replace("CUSTOM_DATA_", "").split(":")]
  280. type_ = self.readlineblock().replace("\r", "\n")
  281. key = self.readlineblock().replace("\r", "\n")
  282. value = self.readlineblock().replace("\r", "\n")
  283. self.host._set_customData(pluginId, customDataId, {'type': type_, 'key': key, 'value': value})
  284. elif msg == "osc-urls":
  285. tcp = self.readlineblock().replace("\r", "\n")
  286. udp = self.readlineblock().replace("\r", "\n")
  287. self.host.fOscTCP = tcp
  288. self.host.fOscUDP = udp
  289. elif msg == "max-plugin-number":
  290. maxnum = int(self.readlineblock())
  291. self.host.fMaxPluginNumber = maxnum
  292. elif msg == "buffer-size":
  293. bufsize = int(self.readlineblock())
  294. self.host.fBufferSize = bufsize
  295. elif msg == "sample-rate":
  296. srate = float(self.readlineblock())
  297. self.host.fSampleRate = srate
  298. elif msg == "transport":
  299. playing = bool(self.readlineblock() == "true")
  300. frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
  301. bpm = float(self.readlineblock())
  302. self.host._set_transport(playing, frame, bar, beat, tick, bpm)
  303. elif msg == "error":
  304. error = self.readlineblock().replace("\r", "\n")
  305. engineCallback(self.host, ENGINE_CALLBACK_ERROR, 0, 0, 0, 0.0, error)
  306. elif msg == "show":
  307. self.fFirstInit = False
  308. self.uiShow()
  309. elif msg == "focus":
  310. self.uiFocus()
  311. elif msg == "hide":
  312. self.uiHide()
  313. elif msg == "quit":
  314. self.fQuitReceived = True
  315. self.uiQuit()
  316. elif msg == "uiTitle":
  317. uiTitle = self.readlineblock().replace("\r", "\n")
  318. self.uiTitleChanged(uiTitle)
  319. else:
  320. print("unknown message: \"" + msg + "\"")
  321. # ------------------------------------------------------------------------------------------------------------
  322. # Embed Widget
  323. if config_UseQt5 and LINUX:
  324. from PyQt5.QtGui import QMouseEvent
  325. class QEmbedWidget(QWidget):
  326. def __init__(self, winId):
  327. QWidget.__init__(self)
  328. self.setAttribute(Qt.WA_LayoutUsesWidgetRect)
  329. self.move(0, 0)
  330. self.fPos = (0, 0)
  331. self.fWinId = 0
  332. def getWidget(self):
  333. return self
  334. def finalSetup(self, gui, winId):
  335. self.fWinId = int(self.winId())
  336. gui.ui.centralwidget.installEventFilter(self)
  337. gui.ui.menubar.installEventFilter(self)
  338. gCarla.utils.x11_reparent_window(self.fWinId, winId)
  339. self.show()
  340. def fixPosition(self):
  341. pos = gCarla.utils.x11_get_window_pos(self.fWinId)
  342. if self.fPos == pos:
  343. return
  344. self.fPos = pos
  345. self.move(pos[0], pos[1])
  346. gCarla.utils.x11_move_window(self.fWinId, 0, 0)
  347. def eventFilter(self, obj, ev):
  348. if isinstance(ev, QMouseEvent):
  349. self.fixPosition()
  350. return False
  351. def enterEvent(self, ev):
  352. self.fixPosition()
  353. QWidget.enterEvent(self, ev)
  354. elif config_UseQt5 and not LINUX:
  355. from PyQt5.QtGui import QWindow
  356. class QEmbedWidget(object):
  357. def __init__(self, winId):
  358. self.fWindow = QWindow.fromWinId(winId)
  359. self.fWidget = QWidget.createWindowContainer(self.fWindow)
  360. self.fWidget.setAttribute(Qt.WA_LayoutUsesWidgetRect)
  361. # TODO: show/hide/close events
  362. def getWidget(self):
  363. return self.fWidget
  364. def finalSetup(self, gui, winId):
  365. self.fWidget.show()
  366. elif not config_UseQt5 and LINUX:
  367. from PyQt4.QtGui import QX11EmbedWidget
  368. class QEmbedWidget(QX11EmbedWidget):
  369. def __init__(self, winId):
  370. QX11EmbedWidget.__init__(self)
  371. def getWidget(self):
  372. return self
  373. def finalSetup(self, gui, winId):
  374. self.embedInto(winId)
  375. self.show()
  376. else:
  377. class QEmbedWidget(object):
  378. def __init__(self, winId):
  379. print("Cannot use embed UI with this configuration")
  380. raise Exception
  381. # ------------------------------------------------------------------------------------------------------------
  382. # Embed plugin UI
  383. class CarlaEmbedW(QEmbedWidget):
  384. def __init__(self, host, winId, isPatchbay):
  385. QEmbedWidget.__init__(self, winId)
  386. self.host = host
  387. self.fWinId = winId
  388. self.fWidget = self.getWidget()
  389. self.fWidget.setFixedSize(740, 512)
  390. self.fLayout = QVBoxLayout(self.fWidget)
  391. self.fLayout.setContentsMargins(0, 0, 0, 0)
  392. self.fLayout.setSpacing(0)
  393. self.fWidget.setLayout(self.fLayout)
  394. self.gui = CarlaMiniW(host, isPatchbay, self.fWidget)
  395. self.gui.hide()
  396. self.gui.ui.act_file_quit.setEnabled(False)
  397. self.gui.ui.act_file_quit.setVisible(False)
  398. self.fShortcutActions = []
  399. self.addShortcutActions(self.gui.ui.menu_File.actions())
  400. self.addShortcutActions(self.gui.ui.menu_Plugin.actions())
  401. self.addShortcutActions(self.gui.ui.menu_PluginMacros.actions())
  402. self.addShortcutActions(self.gui.ui.menu_Settings.actions())
  403. self.addShortcutActions(self.gui.ui.menu_Help.actions())
  404. if self.host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
  405. self.addShortcutActions(self.gui.ui.menu_Canvas.actions())
  406. self.addShortcutActions(self.gui.ui.menu_Canvas_Zoom.actions())
  407. self.addWidget(self.gui.ui.menubar)
  408. self.addLine()
  409. self.addWidget(self.gui.ui.toolBar)
  410. if self.host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
  411. self.addLine()
  412. self.addWidget(self.gui.centralWidget())
  413. self.finalSetup(self.gui, winId)
  414. self.gui.send(["ready", int(self.fWidget.winId())])
  415. def addShortcutActions(self, actions):
  416. for action in actions:
  417. if not action.shortcut().isEmpty():
  418. self.fShortcutActions.append(action)
  419. def addWidget(self, widget):
  420. widget.setParent(self.fWidget)
  421. self.fLayout.addWidget(widget)
  422. def addLine(self):
  423. line = QFrame(self.fWidget)
  424. line.setFrameShadow(QFrame.Sunken)
  425. line.setFrameShape(QFrame.HLine)
  426. line.setLineWidth(0)
  427. line.setMidLineWidth(1)
  428. self.fLayout.addWidget(line)
  429. def keyPressEvent(self, event):
  430. modifiers = event.modifiers()
  431. modifiersStr = ""
  432. if modifiers & Qt.ShiftModifier:
  433. modifiersStr += "Shift+"
  434. if modifiers & Qt.ControlModifier:
  435. modifiersStr += "Ctrl+"
  436. if modifiers & Qt.AltModifier:
  437. modifiersStr += "Alt+"
  438. if modifiers & Qt.MetaModifier:
  439. modifiersStr += "Meta+"
  440. keyStr = QKeySequence(event.key()).toString()
  441. keySeq = QKeySequence(modifiersStr + keyStr)
  442. for action in self.fShortcutActions:
  443. if not action.isEnabled():
  444. continue
  445. if keySeq.matches(action.shortcut()) != QKeySequence.ExactMatch:
  446. continue
  447. event.accept()
  448. action.trigger()
  449. return
  450. QEmbedWidget.keyPressEvent(self, event)
  451. def showEvent(self, event):
  452. QEmbedWidget.showEvent(self, event)
  453. # set our gui as parent for all plugins UIs
  454. if self.host.manageUIs:
  455. winIdStr = "%x" % self.fWinId
  456. self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, winIdStr)
  457. def hideEvent(self, event):
  458. # disable parent
  459. self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0")
  460. QEmbedWidget.hideEvent(self, event)
  461. def closeEvent(self, event):
  462. self.gui.close()
  463. self.gui.closeExternalUI()
  464. QEmbedWidget.closeEvent(self, event)
  465. # there might be other qt windows open which will block carla-plugin from quitting
  466. app.quit()
  467. def setLoadRDFsNeeded(self):
  468. self.gui.setLoadRDFsNeeded()
  469. # ------------------------------------------------------------------------------------------------------------
  470. # Main
  471. if __name__ == '__main__':
  472. # -------------------------------------------------------------
  473. # App initialization
  474. app = CarlaApplication("Carla2-Plugin")
  475. # -------------------------------------------------------------
  476. # Set-up custom signal handling
  477. setUpSignals()
  478. # -------------------------------------------------------------
  479. # Init host backend
  480. isPatchbay = sys.argv[0].rsplit(os.path.sep)[-1].lower().replace(".exe","") == "carla-plugin-patchbay"
  481. host = initHost("Carla-Plugin", None, False, True, True, PluginHost)
  482. host.processMode = ENGINE_PROCESS_MODE_PATCHBAY if isPatchbay else ENGINE_PROCESS_MODE_CONTINUOUS_RACK
  483. host.processModeForced = True
  484. host.nextProcessMode = host.processMode
  485. loadHostSettings(host)
  486. # -------------------------------------------------------------
  487. # Create GUI
  488. try:
  489. winId = int(os.getenv("CARLA_PLUGIN_EMBED_WINID"))
  490. except:
  491. winId = 0
  492. gCarla.utils.setenv("CARLA_PLUGIN_EMBED_WINID", "0")
  493. if LINUX and winId != 0:
  494. gui = CarlaEmbedW(host, winId, isPatchbay)
  495. else:
  496. gui = CarlaMiniW(host, isPatchbay)
  497. # -------------------------------------------------------------
  498. # App-Loop
  499. app.exit_exec()