|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
-
- # XY Controller UI, taken from Cadence
- # Copyright (C) 2011-2020 Filipe Coelho <falktx@falktx.com>
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License as
- # published by the Free Software Foundation; either version 2 of
- # the License, or any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # For a full copy of the GNU General Public License see the doc/GPL.txt file.
-
- # ------------------------------------------------------------------------------------------------------------
- # Imports (Global)
-
- from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF, QSize, QTimer
- from PyQt5.QtGui import QColor, QPainter, QPen
- from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow
-
- # -----------------------------------------------------------------------
- # Imports (Custom)
-
- from carla_shared import *
- from carla_utils import *
-
- import ui_xycontroller
-
- # -----------------------------------------------------------------------
- # Imports (ExternalUI)
-
- from carla_app import CarlaApplication
- from externalui import ExternalUI
- from widgets.paramspinbox import ParamSpinBox
-
- # ------------------------------------------------------------------------------------------------------------
-
- XYCONTROLLER_PARAMETER_X = 0
- XYCONTROLLER_PARAMETER_Y = 1
-
- # ------------------------------------------------------------------------------------------------------------
-
- class XYGraphicsScene(QGraphicsScene):
- # signals
- cursorMoved = pyqtSignal(float,float)
-
- def __init__(self, parent):
- QGraphicsScene.__init__(self, parent)
-
- self.cc_x = 1
- self.cc_y = 2
- self.rparent = parent
-
- self.m_channels = []
- self.m_mouseLock = False
- self.m_smooth = False
- self.m_smooth_x = 0.0
- self.m_smooth_y = 0.0
-
- self.setBackgroundBrush(Qt.black)
-
- cursorPen = QPen(QColor(255, 255, 255), 2)
- cursorBrush = QColor(255, 255, 255, 50)
- self.m_cursor = self.addEllipse(QRectF(-10, -10, 20, 20), cursorPen, cursorBrush)
-
- linePen = QPen(QColor(200, 200, 200, 100), 1, Qt.DashLine)
- self.m_lineH = self.addLine(-9999, 0, 9999, 0, linePen)
- self.m_lineV = self.addLine(0, -9999, 0, 9999, linePen)
-
- self.p_size = QRectF(-100, -100, 100, 100)
-
- # -------------------------------------------------------------------
-
- def setControlX(self, x: int):
- self.cc_x = x
-
- def setControlY(self, y: int):
- self.cc_y = y
-
- def setChannels(self, channels):
- self.m_channels = channels
-
- def setPosX(self, x: float, forward: bool = True):
- if self.m_mouseLock:
- return
-
- posX = x * (self.p_size.x() + self.p_size.width())
- self.m_cursor.setPos(posX, self.m_cursor.y())
- self.m_lineV.setX(posX)
-
- if forward:
- value = posX / (self.p_size.x() + self.p_size.width());
- self.sendMIDI(value, None)
- else:
- self.m_smooth_x = posX;
-
- def setPosY(self, y: float, forward: bool = True):
- if self.m_mouseLock:
- return;
-
- posY = y * (self.p_size.y() + self.p_size.height())
- self.m_cursor.setPos(self.m_cursor.x(), posY)
- self.m_lineH.setY(posY)
-
- if forward:
- value = posY / (self.p_size.y() + self.p_size.height())
- self.sendMIDI(None, value)
- else:
- self.m_smooth_y = posY
-
- def setSmooth(self, smooth: bool):
- self.m_smooth = smooth
-
- def setSmoothValues(self, x: float, y: float):
- self.m_smooth_x = x * (self.p_size.x() + self.p_size.width());
- self.m_smooth_y = y * (self.p_size.y() + self.p_size.height());
-
- # -------------------------------------------------------------------
-
- def updateSize(self, size: QSize):
- self.p_size.setRect(-(float(size.width())/2),
- -(float(size.height())/2),
- size.width(),
- size.height());
-
- def updateSmooth(self):
- if not self.m_smooth:
- return
-
- if self.m_cursor.x() == self.m_smooth_x and self.m_cursor.y() == self.m_smooth_y:
- return
-
- same = 0
- if abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005:
- self.m_smooth_x = self.m_cursor.x()
- same += 1
-
- if abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005:
- self.m_smooth_y = self.m_cursor.y()
- same += 1
-
- if same == 2:
- return
-
- newX = float(self.m_smooth_x + self.m_cursor.x()*7) / 8
- newY = float(self.m_smooth_y + self.m_cursor.y()*7) / 8
- pos = QPointF(newX, newY)
-
- self.m_cursor.setPos(pos)
- self.m_lineH.setY(pos.y())
- self.m_lineV.setX(pos.x())
-
- xp = pos.x() / (self.p_size.x() + self.p_size.width())
- yp = pos.y() / (self.p_size.y() + self.p_size.height())
-
- self.sendMIDI(xp, yp)
- self.cursorMoved.emit(xp, yp)
-
- # -------------------------------------------------------------------
-
- def handleMousePos(self, pos: QPointF):
- if not self.p_size.contains(pos):
- if pos.x() < self.p_size.x():
- pos.setX(self.p_size.x())
- elif pos.x() > (self.p_size.x() + self.p_size.width()):
- pos.setX(self.p_size.x() + self.p_size.width());
-
- if pos.y() < self.p_size.y():
- pos.setY(self.p_size.y())
- elif pos.y() > (self.p_size.y() + self.p_size.height()):
- pos.setY(self.p_size.y() + self.p_size.height())
-
- self.m_smooth_x = pos.x()
- self.m_smooth_y = pos.y()
-
- if not self.m_smooth:
- self.m_cursor.setPos(pos)
- self.m_lineH.setY(pos.y())
- self.m_lineV.setX(pos.x())
-
- xp = pos.x() / (self.p_size.x() + self.p_size.width());
- yp = pos.y() / (self.p_size.y() + self.p_size.height());
-
- self.sendMIDI(xp, yp)
- self.cursorMoved.emit(xp, yp)
-
- def sendMIDI(self, xp, yp):
- rate = float(0xff) / 4
- msgd = ["cc2" if xp is not None and yp is not None else "cc"]
-
- if xp is not None:
- msgd.append(self.cc_x)
- msgd.append(int(xp * rate + rate))
-
- if yp is not None:
- msgd.append(self.cc_y)
- msgd.append(int(yp * rate + rate))
-
- self.rparent.send(msgd)
-
- # -------------------------------------------------------------------
-
- def keyPressEvent(self, event): # QKeyEvent
- event.accept()
-
- def wheelEvent(self, event): # QGraphicsSceneWheelEvent
- event.accept()
-
- def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
- self.m_mouseLock = True
- self.handleMousePos(event.scenePos())
- self.rparent.setCursor(Qt.CrossCursor)
- QGraphicsScene.mousePressEvent(self, event);
-
- def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
- self.handleMousePos(event.scenePos())
- QGraphicsScene.mouseMoveEvent(self, event);
-
- def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent):
- self.m_mouseLock = False
- self.rparent.setCursor(Qt.ArrowCursor)
- QGraphicsScene.mouseReleaseEvent(self, event)
-
- # -----------------------------------------------------------------------
- # External UI
-
- class XYControllerUI(ExternalUI, QMainWindow):
- def __init__(self):
- ExternalUI.__init__(self)
- QMainWindow.__init__(self)
- self.ui = ui_xycontroller.Ui_XYControllerW()
- self.ui.setupUi(self)
-
- self.fSaveSizeNowChecker = -1
-
- # ---------------------------------------------------------------
- # Set-up GUI stuff
-
- self.scene = XYGraphicsScene(self)
-
- self.ui.dial_x.setImage(2)
- self.ui.dial_y.setImage(2)
- self.ui.dial_x.setLabel("X")
- self.ui.dial_y.setLabel("Y")
- self.ui.keyboard.setOctaves(10)
-
- self.ui.graphicsView.setScene(self.scene)
- self.ui.graphicsView.setRenderHints(QPainter.Antialiasing)
-
- for MIDI_CC in MIDI_CC_LIST:
- self.ui.cb_control_x.addItem(MIDI_CC)
- self.ui.cb_control_y.addItem(MIDI_CC)
-
- # ---------------------------------------------------------------
- # Initial state
-
- self.m_channels = [1]
-
- self.ui.act_ch_01.setChecked(True)
- self.ui.act_show_keyboard.setChecked(True)
- self.ui.cb_control_y.setCurrentIndex(1)
-
- # ---------------------------------------------------------------
- # Connect actions to functions
-
- self.scene.cursorMoved.connect(self.slot_sceneCursorMoved)
-
- self.ui.keyboard.noteOn.connect(self.slot_noteOn)
- self.ui.keyboard.noteOff.connect(self.slot_noteOff)
-
- self.ui.cb_smooth.clicked.connect(self.slot_setSmooth)
-
- self.ui.dial_x.realValueChanged.connect(self.slot_knobValueChangedX)
- self.ui.dial_y.realValueChanged.connect(self.slot_knobValueChangedY)
-
- self.ui.cb_control_x.currentIndexChanged[str].connect(self.slot_checkCC_X)
- self.ui.cb_control_y.currentIndexChanged[str].connect(self.slot_checkCC_Y)
-
- self.ui.act_ch_01.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_02.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_03.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_04.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_05.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_06.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_07.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_08.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_09.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_10.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_11.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_12.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_13.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_14.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_15.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_16.triggered.connect(self.slot_checkChannel)
- self.ui.act_ch_all.triggered.connect(self.slot_checkChannel_all)
- self.ui.act_ch_none.triggered.connect(self.slot_checkChannel_none)
-
- self.ui.act_show_keyboard.triggered.connect(self.slot_showKeyboard)
-
- # ---------------------------------------------------------------
- # Final stuff
-
- self.fIdleTimer = self.startTimer(60)
- self.setWindowTitle(self.fUiName)
- self.ready()
-
- # -------------------------------------------------------------------
-
- @pyqtSlot()
- def slot_updateScreen(self):
- self.ui.graphicsView.centerOn(0, 0)
- self.scene.updateSize(self.ui.graphicsView.size())
-
- dial_x = self.ui.dial_x.rvalue()
- dial_y = self.ui.dial_y.rvalue()
- self.scene.setPosX(dial_x / 100, False)
- self.scene.setPosY(dial_y / 100, False)
- self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
-
- @pyqtSlot(int)
- def slot_noteOn(self, note):
- self.send(["note", True, note])
-
- @pyqtSlot(int)
- def slot_noteOff(self, note):
- self.send(["note", False, note])
-
- @pyqtSlot(float)
- def slot_knobValueChangedX(self, x: float):
- self.sendControl(XYCONTROLLER_PARAMETER_X, x)
- self.scene.setPosX(x / 100, True)
- self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100)
-
- @pyqtSlot(float)
- def slot_knobValueChangedY(self, y: float):
- self.sendControl(XYCONTROLLER_PARAMETER_Y, y)
- self.scene.setPosY(y / 100, True)
- self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100)
-
- @pyqtSlot(str)
- def slot_checkCC_X(self, text: str):
- if not text:
- return
-
- cc_x = int(text.split(" ",1)[0])
-
- self.scene.setControlX(cc_x)
- self.sendConfigure("cc_x", str(cc_x))
-
- @pyqtSlot(str)
- def slot_checkCC_Y(self, text: str):
- if not text:
- return
-
- cc_y = int(text.split(" ",1)[0])
-
- self.scene.setControlY(cc_y)
- self.sendConfigure("cc_y", str(cc_y))
-
- @pyqtSlot(bool)
- def slot_checkChannel(self, clicked):
- if not self.sender():
- return
-
- channel = int(self.sender().text())
-
- if clicked and channel not in self.m_channels:
- self.m_channels.append(channel)
- elif not clicked and channel in self.m_channels:
- self.m_channels.remove(channel)
-
- self.scene.setChannels(self.m_channels)
- self.sendConfigure("channels", ",".join(str(c) for c in self.m_channels))
-
- @pyqtSlot()
- def slot_checkChannel_all(self):
- self.ui.act_ch_01.setChecked(True)
- self.ui.act_ch_02.setChecked(True)
- self.ui.act_ch_03.setChecked(True)
- self.ui.act_ch_04.setChecked(True)
- self.ui.act_ch_05.setChecked(True)
- self.ui.act_ch_06.setChecked(True)
- self.ui.act_ch_07.setChecked(True)
- self.ui.act_ch_08.setChecked(True)
- self.ui.act_ch_09.setChecked(True)
- self.ui.act_ch_10.setChecked(True)
- self.ui.act_ch_11.setChecked(True)
- self.ui.act_ch_12.setChecked(True)
- self.ui.act_ch_13.setChecked(True)
- self.ui.act_ch_14.setChecked(True)
- self.ui.act_ch_15.setChecked(True)
- self.ui.act_ch_16.setChecked(True)
-
- self.m_channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
- self.scene.setChannels(self.m_channels)
- self.sendConfigure("channels", ",".join(str(c) for c in self.m_channels))
-
- @pyqtSlot()
- def slot_checkChannel_none(self):
- self.ui.act_ch_01.setChecked(False)
- self.ui.act_ch_02.setChecked(False)
- self.ui.act_ch_03.setChecked(False)
- self.ui.act_ch_04.setChecked(False)
- self.ui.act_ch_05.setChecked(False)
- self.ui.act_ch_06.setChecked(False)
- self.ui.act_ch_07.setChecked(False)
- self.ui.act_ch_08.setChecked(False)
- self.ui.act_ch_09.setChecked(False)
- self.ui.act_ch_10.setChecked(False)
- self.ui.act_ch_11.setChecked(False)
- self.ui.act_ch_12.setChecked(False)
- self.ui.act_ch_13.setChecked(False)
- self.ui.act_ch_14.setChecked(False)
- self.ui.act_ch_15.setChecked(False)
- self.ui.act_ch_16.setChecked(False)
-
- self.m_channels = []
- self.scene.setChannels(self.m_channels)
- self.sendConfigure("channels", "")
-
- @pyqtSlot(bool)
- def slot_setSmooth(self, smooth):
- self.scene.setSmooth(smooth)
- self.sendConfigure("smooth", "yes" if smooth else "no")
-
- if smooth:
- dial_x = self.ui.dial_x.rvalue()
- dial_y = self.ui.dial_y.rvalue()
- self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
-
- @pyqtSlot(float, float)
- def slot_sceneCursorMoved(self, xp: float, yp: float):
- self.ui.dial_x.setValue(xp * 100, False)
- self.ui.dial_y.setValue(yp * 100, False)
- self.sendControl(XYCONTROLLER_PARAMETER_X, xp * 100)
- self.sendControl(XYCONTROLLER_PARAMETER_Y, yp * 100)
-
- @pyqtSlot(bool)
- def slot_showKeyboard(self, yesno):
- self.ui.scrollArea.setVisible(yesno)
- self.sendConfigure("show-midi-keyboard", "yes" if yesno else "no")
- QTimer.singleShot(0, self.slot_updateScreen)
-
- # -------------------------------------------------------------------
- # DSP Callbacks
-
- def dspParameterChanged(self, index: int, value: float):
- if index == XYCONTROLLER_PARAMETER_X:
- self.ui.dial_x.setValue(value, False)
- self.scene.setPosX(value / 100, False)
- elif index == XYCONTROLLER_PARAMETER_Y:
- self.ui.dial_y.setValue(value, False)
- self.scene.setPosY(value / 100, False)
- else:
- return
-
- self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100,
- self.ui.dial_y.rvalue() / 100)
-
- def dspStateChanged(self, key: str, value: str):
- if key == "guiWidth":
- try:
- width = int(value)
- except:
- width = 0
-
- if width > 0:
- self.resize(width, self.height())
-
- elif key == "guiHeight":
- try:
- height = int(value)
- except:
- height = 0
-
- if height > 0:
- self.resize(self.width(), height)
-
- elif key == "smooth":
- smooth = (value == "yes")
- self.ui.cb_smooth.blockSignals(True)
- self.ui.cb_smooth.setChecked(smooth)
- self.ui.cb_smooth.blockSignals(False)
- self.scene.setSmooth(smooth)
-
- if smooth:
- dial_x = self.ui.dial_x.rvalue()
- dial_y = self.ui.dial_y.rvalue()
- self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
-
- elif key == "show-midi-keyboard":
- show = (value == "yes")
- self.ui.act_show_keyboard.blockSignals(True)
- self.ui.act_show_keyboard.setChecked(show)
- self.ui.act_show_keyboard.blockSignals(False)
- self.ui.scrollArea.setVisible(show)
-
- elif key == "channels":
- if value:
- self.m_channels = [int(c) for c in value.split(",")]
- else:
- self.m_channels = []
- self.scene.setChannels(self.m_channels)
- self.ui.act_ch_01.setChecked(bool(1 in self.m_channels))
- self.ui.act_ch_02.setChecked(bool(2 in self.m_channels))
- self.ui.act_ch_03.setChecked(bool(3 in self.m_channels))
- self.ui.act_ch_04.setChecked(bool(4 in self.m_channels))
- self.ui.act_ch_05.setChecked(bool(5 in self.m_channels))
- self.ui.act_ch_06.setChecked(bool(6 in self.m_channels))
- self.ui.act_ch_07.setChecked(bool(7 in self.m_channels))
- self.ui.act_ch_08.setChecked(bool(8 in self.m_channels))
- self.ui.act_ch_09.setChecked(bool(9 in self.m_channels))
- self.ui.act_ch_10.setChecked(bool(10 in self.m_channels))
- self.ui.act_ch_11.setChecked(bool(11 in self.m_channels))
- self.ui.act_ch_12.setChecked(bool(12 in self.m_channels))
- self.ui.act_ch_13.setChecked(bool(13 in self.m_channels))
- self.ui.act_ch_14.setChecked(bool(14 in self.m_channels))
- self.ui.act_ch_15.setChecked(bool(15 in self.m_channels))
- self.ui.act_ch_16.setChecked(bool(16 in self.m_channels))
-
- elif key == "cc_x":
- cc_x = int(value)
- self.scene.setControlX(cc_x)
-
- for cc_index in range(len(MIDI_CC_LIST)):
- if cc_x == int(MIDI_CC_LIST[cc_index].split(" ",1)[0]):
- self.ui.cb_control_x.blockSignals(True)
- self.ui.cb_control_x.setCurrentIndex(cc_index)
- self.ui.cb_control_x.blockSignals(False)
- break
-
- elif key == "cc_y":
- cc_y = int(value)
- self.scene.setControlY(cc_y)
-
- for cc_index in range(len(MIDI_CC_LIST)):
- if cc_y == int(MIDI_CC_LIST[cc_index].split(" ",1)[0]):
- self.ui.cb_control_y.blockSignals(True)
- self.ui.cb_control_y.setCurrentIndex(cc_index)
- self.ui.cb_control_y.blockSignals(False)
- break
-
- def dspNoteReceived(self, onOff, channel, note, velocity):
- if channel+1 not in self.m_channels:
- return
- if onOff:
- self.ui.keyboard.sendNoteOn(note, False)
- else:
- self.ui.keyboard.sendNoteOff(note, False)
-
- # -------------------------------------------------------------------
- # ExternalUI Callbacks
-
- def uiShow(self):
- self.show()
-
- def uiFocus(self):
- self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
- self.show()
-
- self.raise_()
- self.activateWindow()
-
- def uiHide(self):
- self.hide()
-
- def uiQuit(self):
- self.closeExternalUI()
- self.close()
- app.quit()
-
- def uiTitleChanged(self, uiTitle):
- self.setWindowTitle(uiTitle)
-
- # -------------------------------------------------------------------
- # Qt events
-
- def showEvent(self, event):
- self.slot_updateScreen()
- QMainWindow.showEvent(self, event)
-
- def resizeEvent(self, event):
- self.fSaveSizeNowChecker = 0
- self.slot_updateScreen()
- QMainWindow.resizeEvent(self, event)
-
- def timerEvent(self, event):
- if event.timerId() == self.fIdleTimer:
- self.idleExternalUI()
- self.scene.updateSmooth()
-
- if self.fSaveSizeNowChecker == 11:
- self.sendConfigure("guiWidth", str(self.width()))
- self.sendConfigure("guiHeight", str(self.height()))
- self.fSaveSizeNowChecker = -1
- elif self.fSaveSizeNowChecker >= 0:
- self.fSaveSizeNowChecker += 1
-
- QMainWindow.timerEvent(self, event)
-
- def closeEvent(self, event):
- self.closeExternalUI()
- QMainWindow.closeEvent(self, event)
-
- # there might be other qt windows open which will block the UI from quitting
- app.quit()
-
- #--------------- main ------------------
- if __name__ == '__main__':
- import resources_rc
-
- pathBinaries, _ = getPaths()
- gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
- gCarla.utils.set_process_name("XYController")
-
- app = CarlaApplication("XYController")
- gui = XYControllerUI()
- app.exit_exec()
|