|
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
-
- # XY Controller for JACK, using jacklib
- # Copyright (C) 2012 Filipe Coelho <falktx@gmail.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 COPYING file
-
- # Imports (Global)
- from PyQt4.QtCore import pyqtSlot, Qt, QPointF, QRectF, QSettings, QTimer, QVariant
- from PyQt4.QtGui import QApplication, QColor, QIcon, QPainter, QPen, QGraphicsItem, QGraphicsScene, QMainWindow
- from queue import Queue, Empty as QuequeEmpty
-
- # Imports (Custom)
- import ui_xycontroller
- from shared import *
- from jacklib_helpers import *
-
- # Globals
- global jack_client, jack_midi_in_port, jack_midi_out_port, jack_midi_in_data, jack_midi_out_data
- jack_client = None
- jack_midi_in_port = None
- jack_midi_out_port = None
- jack_midi_in_data = Queue(512)
- jack_midi_out_data = Queue(512)
-
- # XY Controller Scene
- class XYGraphicsScene(QGraphicsScene):
- def __init__(self, parent):
- QGraphicsScene.__init__(self, parent)
-
- self.cc_x = 1
- self.cc_y = 2
- self.m_channels = []
- self.m_mouseLock = False
- self.m_smooth = False
- self.m_smooth_x = 0
- self.m_smooth_y = 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):
- self.cc_x = x
-
- def setControlY(self, y):
- self.cc_y = y
-
- def setChannels(self, channels):
- self.m_channels = channels
-
- def setPosX(self, x, forward=True):
- if (self.m_mouseLock == False):
- pos_x = x*(self.p_size.x()+self.p_size.width())
- self.m_cursor.setPos(pos_x, self.m_cursor.y())
- self.m_lineV.setX(pos_x)
-
- if (forward):
- self.sendMIDI(pos_x/(self.p_size.x()+self.p_size.width()), None)
- else:
- self.m_smooth_x = pos_x
-
- def setPosY(self, y, forward=True):
- if (self.m_mouseLock == False):
- pos_y = y*(self.p_size.y()+self.p_size.height())
- self.m_cursor.setPos(self.m_cursor.x(), pos_y)
- self.m_lineH.setY(pos_y)
-
- if (forward):
- self.sendMIDI(None, pos_y/(self.p_size.y()+self.p_size.height()))
- else:
- self.m_smooth_y = pos_y
-
- def setSmooth(self, smooth):
- self.m_smooth = smooth
-
- def handleCC(self, param, value):
- if (param == self.cc_x):
- xp = (float(value)/63)-1.025
- yp = self.m_cursor.y()/(self.p_size.y()+self.p_size.height())
- self.setPosX(xp, False)
-
- elif (param == self.cc_y):
- xp = self.m_cursor.x()/(self.p_size.x()+self.p_size.width())
- yp = (float(value)/63)-1.025
- self.setPosY(yp, False)
-
- else:
- return
-
- self.emit(SIGNAL("cursorMoved(float, float)"), xp, yp)
-
- def handleMousePos(self, pos):
- 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 (self.m_smooth == False):
- 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.emit(SIGNAL("cursorMoved(float, float)"), xp, yp)
-
- def sendMIDI(self, xp=None, yp=None):
- global jack_midi_out_data
- rate = float(0xff)/4
-
- if (xp != None):
- value = int((xp*rate)+rate)
- for channel in self.m_channels:
- jack_midi_out_data.put_nowait((0xB0+channel-1, self.cc_x, value))
-
- if (yp != None):
- value = int((yp*rate)+rate)
- for channel in self.m_channels:
- jack_midi_out_data.put_nowait((0xB0+channel-1, self.cc_y, value))
-
- def updateSize(self, size):
- self.p_size.setRect(-(size.width()/2), -(size.height()/2), size.width(), size.height())
-
- def updateSmooth(self):
- if (self.m_smooth):
- if (self.m_cursor.x() != self.m_smooth_x or self.m_cursor.y() != self.m_smooth_y):
- new_x = (self.m_smooth_x+self.m_cursor.x()*3)/4
- new_y = (self.m_smooth_y+self.m_cursor.y()*3)/4
- pos = QPointF(new_x, new_y)
-
- 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.emit(SIGNAL("cursorMoved(float, float)"), xp, yp)
-
- def keyPressEvent(self, event):
- event.accept()
-
- def wheelEvent(self, event):
- event.accept()
-
- def mousePressEvent(self, event):
- self.m_mouseLock = True
- self.handleMousePos(event.scenePos())
- QGraphicsScene.mousePressEvent(self, event)
-
- def mouseMoveEvent(self, event):
- self.handleMousePos(event.scenePos())
- QGraphicsScene.mouseMoveEvent(self, event)
-
- def mouseReleaseEvent(self, event):
- self.m_mouseLock = False
- QGraphicsScene.mouseReleaseEvent(self, event)
-
- # XY Controller Window
- class XYControllerW(QMainWindow, ui_xycontroller.Ui_XYControllerW):
- def __init__(self, parent):
- QMainWindow.__init__(self, parent)
- self.setupUi(self)
-
- # -------------------------------------------------------------
- # Internal stuff
-
- self.cc_x = 1
- self.cc_y = 2
- self.m_channels = []
-
- # -------------------------------------------------------------
- # Set-up GUI stuff
-
- self.dial_x.setPixmap(2)
- self.dial_y.setPixmap(2)
- self.dial_x.setLabel("X")
- self.dial_y.setLabel("Y")
- self.keyboard.setOctaves(6)
-
- self.scene = XYGraphicsScene(self)
- self.graphicsView.setScene(self.scene)
- self.graphicsView.setRenderHints(QPainter.Antialiasing)
-
- for MIDI_CC in MIDI_CC_LIST:
- self.cb_control_x.addItem(MIDI_CC)
- self.cb_control_y.addItem(MIDI_CC)
-
- # -------------------------------------------------------------
- # Load Settings
-
- self.settings = QSettings("Cadence", "XY-Controller")
- self.loadSettings()
-
- # -------------------------------------------------------------
- # Connect actions to functions
-
- self.connect(self.keyboard, SIGNAL("noteOn(int)"), SLOT("slot_noteOn(int)"))
- self.connect(self.keyboard, SIGNAL("noteOff(int)"), SLOT("slot_noteOff(int)"))
-
- self.connect(self.cb_smooth, SIGNAL("clicked(bool)"), SLOT("slot_setSmooth(bool)"))
-
- self.connect(self.dial_x, SIGNAL("valueChanged(int)"), SLOT("slot_updateSceneX(int)"))
- self.connect(self.dial_y, SIGNAL("valueChanged(int)"), SLOT("slot_updateSceneY(int)"))
-
- self.connect(self.cb_control_x, SIGNAL("currentIndexChanged(QString)"), SLOT("slot_checkCC_X(QString)"))
- self.connect(self.cb_control_y, SIGNAL("currentIndexChanged(QString)"), SLOT("slot_checkCC_Y(QString)"))
-
- # FIXME
- self.connect(self.scene, SIGNAL("cursorMoved(float, float)"), self.slot_sceneCursorMoved)
- #self.connect(self.scene, SIGNAL("cursorMoved(float, float)"), SLOT("slot_sceneCursorMoved(float, float)"))
-
- self.connect(self.act_ch_01, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_02, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_03, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_04, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_05, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_06, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_07, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_08, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_09, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_10, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_11, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_12, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_13, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_14, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_15, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_16, SIGNAL("triggered(bool)"), SLOT("slot_checkChannel(bool)"))
- self.connect(self.act_ch_all, SIGNAL("triggered()"), SLOT("slot_checkChannel_all()"))
- self.connect(self.act_ch_none, SIGNAL("triggered()"), SLOT("slot_checkChannel_none()"))
-
- self.connect(self.act_show_keyboard, SIGNAL("triggered(bool)"), SLOT("slot_showKeyboard(bool)"))
- self.connect(self.act_about, SIGNAL("triggered()"), SLOT("slot_about()"))
-
- # -------------------------------------------------------------
- # Final stuff
-
- self.m_midiInTimerId = self.startTimer(50)
- QTimer.singleShot(0, self, SLOT("slot_updateScreen()"))
-
- def updateScreen(self):
- self.scene.updateSize(self.graphicsView.size())
- self.graphicsView.centerOn(0, 0)
-
- self.slot_updateSceneX(self.dial_x.value())
- self.slot_updateSceneY(self.dial_y.value())
-
- @pyqtSlot(int)
- def slot_noteOn(self, note):
- global jack_midi_out_data
- for channel in self.m_channels:
- jack_midi_out_data.put_nowait((0x90+channel-1, note, 100))
-
- @pyqtSlot(int)
- def slot_noteOff(self, note):
- global jack_midi_out_data
- for channel in self.m_channels:
- jack_midi_out_data.put_nowait((0x80+channel-1, note, 0))
-
- @pyqtSlot(int)
- def slot_updateSceneX(self, x):
- self.scene.setPosX(float(x)/100)
-
- @pyqtSlot(int)
- def slot_updateSceneY(self, y):
- self.scene.setPosY(float(y)/100)
-
- @pyqtSlot(str)
- def slot_checkCC_X(self, text):
- if (text):
- self.cc_x = int(text.split(" ")[0], 16)
- self.scene.setControlX(self.cc_x)
-
- @pyqtSlot(str)
- def slot_checkCC_Y(self, text):
- if (text):
- self.cc_y = int(text.split(" ")[0], 16)
- self.scene.setControlY(self.cc_y)
-
- @pyqtSlot(bool)
- def slot_checkChannel(self, clicked):
- 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)
-
- @pyqtSlot()
- def slot_checkChannel_all(self):
- self.act_ch_01.setChecked(True)
- self.act_ch_02.setChecked(True)
- self.act_ch_03.setChecked(True)
- self.act_ch_04.setChecked(True)
- self.act_ch_05.setChecked(True)
- self.act_ch_06.setChecked(True)
- self.act_ch_07.setChecked(True)
- self.act_ch_08.setChecked(True)
- self.act_ch_09.setChecked(True)
- self.act_ch_10.setChecked(True)
- self.act_ch_11.setChecked(True)
- self.act_ch_12.setChecked(True)
- self.act_ch_13.setChecked(True)
- self.act_ch_14.setChecked(True)
- self.act_ch_15.setChecked(True)
- self.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)
-
- @pyqtSlot()
- def slot_checkChannel_none(self):
- self.act_ch_01.setChecked(False)
- self.act_ch_02.setChecked(False)
- self.act_ch_03.setChecked(False)
- self.act_ch_04.setChecked(False)
- self.act_ch_05.setChecked(False)
- self.act_ch_06.setChecked(False)
- self.act_ch_07.setChecked(False)
- self.act_ch_08.setChecked(False)
- self.act_ch_09.setChecked(False)
- self.act_ch_10.setChecked(False)
- self.act_ch_11.setChecked(False)
- self.act_ch_12.setChecked(False)
- self.act_ch_13.setChecked(False)
- self.act_ch_14.setChecked(False)
- self.act_ch_15.setChecked(False)
- self.act_ch_16.setChecked(False)
- self.m_channels = []
- self.scene.setChannels(self.m_channels)
-
- @pyqtSlot(bool)
- def slot_setSmooth(self, yesno):
- self.scene.setSmooth(yesno)
-
- @pyqtSlot(float, float)
- def slot_sceneCursorMoved(self, xp, yp):
- self.dial_x.setValue(xp*100)
- self.dial_y.setValue(yp*100)
-
- @pyqtSlot(bool)
- def slot_showKeyboard(self, yesno):
- self.scrollArea.setVisible(yesno)
- QTimer.singleShot(0, self, SLOT("slot_updateScreen()"))
-
- @pyqtSlot()
- def slot_about(self):
- QMessageBox.about(self, self.tr("About XY Controller"), self.tr("<h3>XY Controller</h3>"
- "<br>Version %s"
- "<br>XY Controller is a simple XY widget that sends and receives data from Jack MIDI.<br>"
- "<br>Copyright (C) 2012 falkTX" % (VERSION)))
-
- @pyqtSlot()
- def slot_updateScreen(self):
- self.updateScreen()
-
- def saveSettings(self):
- self.settings.setValue("Geometry", self.saveGeometry())
- self.settings.setValue("ShowKeyboard", self.scrollArea.isVisible())
- self.settings.setValue("Smooth", self.cb_smooth.isChecked())
- self.settings.setValue("DialX", self.dial_x.value())
- self.settings.setValue("DialY", self.dial_y.value())
- self.settings.setValue("ControlX", self.cc_x)
- self.settings.setValue("ControlY", self.cc_y)
- self.settings.setValue("Channels", self.m_channels)
-
- def loadSettings(self):
- self.restoreGeometry(self.settings.value("Geometry", ""))
-
- showKeyboard = self.settings.value("ShowKeyboard", False, type=bool)
- self.act_show_keyboard.setChecked(showKeyboard)
- self.scrollArea.setVisible(showKeyboard)
-
- smooth = self.settings.value("Smooth", False, type=bool)
- self.cb_smooth.setChecked(smooth)
- self.scene.setSmooth(smooth)
-
- self.dial_x.setValue(self.settings.value("DialX", 50, type=int))
- self.dial_y.setValue(self.settings.value("DialY", 50, type=int))
- self.cc_x = self.settings.value("ControlX", 1, type=int)
- self.cc_y = self.settings.value("ControlY", 2, type=int)
- self.m_channels = toList(self.settings.value("Channels", [1]))
-
- for i in range(len(self.m_channels)):
- self.m_channels[i] = int(self.m_channels[i])
-
- self.scene.setChannels(self.m_channels)
-
- for i in range(len(MIDI_CC_LIST)):
- cc = int(MIDI_CC_LIST[i].split(" ")[0], 16)
- if (self.cc_x == cc):
- self.cb_control_x.setCurrentIndex(i)
- if (self.cc_y == cc):
- self.cb_control_y.setCurrentIndex(i)
-
- if (1 in self.m_channels):
- self.act_ch_01.setChecked(True)
- if (2 in self.m_channels):
- self.act_ch_02.setChecked(True)
- if (3 in self.m_channels):
- self.act_ch_03.setChecked(True)
- if (4 in self.m_channels):
- self.act_ch_04.setChecked(True)
- if (5 in self.m_channels):
- self.act_ch_05.setChecked(True)
- if (6 in self.m_channels):
- self.act_ch_06.setChecked(True)
- if (7 in self.m_channels):
- self.act_ch_07.setChecked(True)
- if (8 in self.m_channels):
- self.act_ch_08.setChecked(True)
- if (9 in self.m_channels):
- self.act_ch_09.setChecked(True)
- if (10 in self.m_channels):
- self.act_ch_10.setChecked(True)
- if (11 in self.m_channels):
- self.act_ch_11.setChecked(True)
- if (12 in self.m_channels):
- self.act_ch_12.setChecked(True)
- if (13 in self.m_channels):
- self.act_ch_13.setChecked(True)
- if (14 in self.m_channels):
- self.act_ch_14.setChecked(True)
- if (15 in self.m_channels):
- self.act_ch_15.setChecked(True)
- if (16 in self.m_channels):
- self.act_ch_16.setChecked(True)
-
- def timerEvent(self, event):
- if (event.timerId() == self.m_midiInTimerId):
- global jack_midi_in_data
- if (jack_midi_in_data.empty() == False):
- while (True):
- try:
- mode, note, velo = jack_midi_in_data.get_nowait()
- except QuequeEmpty:
- break
-
- # TODO - filter by channel here
- #channel = mode - 0xB0+1
- #if (channel in self.m_channels):
- if (0x80 <= mode and mode <= 0x8F):
- self.keyboard.noteOff(note, False)
- elif (0x90 <= mode and mode < 0x9F):
- self.keyboard.noteOn(note, False)
- elif (0xB0 <= mode and mode < 0xBF):
- self.scene.handleCC(note, velo)
-
- jack_midi_in_data.task_done()
-
- self.scene.updateSmooth()
-
- QMainWindow.timerEvent(self, event)
-
- def resizeEvent(self, event):
- self.updateScreen()
- QMainWindow.resizeEvent(self, event)
-
- def closeEvent(self, event):
- self.saveSettings()
- QMainWindow.closeEvent(self, event)
-
- # -------------------------------------------------------------
- # JACK Stuff
-
- static_event = jacklib.jack_midi_event_t()
- static_mtype = jacklib.c_ubyte*3
-
- def jack_process_callback(nframes, arg):
- global jack_midi_in_port, jack_midi_out_port, jack_midi_in_data, jack_midi_out_data
-
- # MIDI In
- midi_in_buffer = jacklib.port_get_buffer(jack_midi_in_port, nframes)
-
- if (midi_in_buffer):
- event_count = jacklib.midi_get_event_count(midi_in_buffer)
-
- for i in range(event_count):
- if (jacklib.midi_event_get(jacklib.pointer(static_event), midi_in_buffer, i) == 0):
- if (static_event.size == 1):
- jack_midi_in_data.put_nowait((static_event.buffer[0], 0, 0))
- elif (static_event.size == 2):
- jack_midi_in_data.put_nowait((static_event.buffer[0], static_event.buffer[1], 0))
- elif (static_event.size >= 3):
- jack_midi_in_data.put_nowait((static_event.buffer[0], static_event.buffer[1], static_event.buffer[2]))
-
- if (jack_midi_in_data.full()):
- break
-
- # MIDI Out
- midi_out_buffer = jacklib.port_get_buffer(jack_midi_out_port, nframes)
-
- if (midi_out_buffer):
- jacklib.midi_clear_buffer(midi_out_buffer)
-
- if (jack_midi_out_data.empty() == False):
- while (True):
- try:
- mode, note, velo = jack_midi_out_data.get_nowait()
- except QuequeEmpty:
- break
-
- data = static_mtype(mode, note, velo)
- jacklib.midi_event_write(midi_out_buffer, 0, data, 3)
-
- jack_midi_out_data.task_done()
-
- return 0
-
-
- #--------------- main ------------------
- if __name__ == '__main__':
-
- # App initialization
- app = QApplication(sys.argv)
- app.setApplicationName("XY-Controller")
- app.setApplicationVersion(VERSION)
- app.setOrganizationName("falkTX")
- #app.setWindowIcon(QIcon(":/48x48/xy-controller.png"))
-
- # Start jack
- jack_status = jacklib.jack_status_t(0)
- jack_client = jacklib.client_open("XY-Controller", jacklib.JackNullOption, jacklib.pointer(jack_status))
-
- if not jack_client:
- QMessageBox.critical(None, app.translate("RenderW", "Error"), app.translate("RenderW", "Could not connect to JACK, possible errors:\n%s" % (get_jack_status_error_string(jack_status))))
- sys.exit(1)
-
- jack_midi_in_port = jacklib.port_register(jack_client, "midi_in", jacklib.JACK_DEFAULT_MIDI_TYPE, jacklib.JackPortIsInput, 0)
- jack_midi_out_port = jacklib.port_register(jack_client, "midi_out", jacklib.JACK_DEFAULT_MIDI_TYPE, jacklib.JackPortIsOutput, 0)
- jacklib.set_process_callback(jack_client, jack_process_callback, None)
- jacklib.activate(jack_client)
-
- # Show GUI
- gui = XYControllerW(None)
- gui.show()
-
- # Set-up custom signal handling
- set_up_signals(gui)
-
- # App-Loop
- ret = app.exec_()
-
- # Close Jack
- if (jack_client):
- jacklib.deactivate(jack_client)
- jacklib.client_close(jack_client)
-
- # Exit properly
- sys.exit(ret)
|