diff --git a/src/jacklib_helpers.py b/src/jacklib_helpers.py index 196b6db..60e8cdf 100644 --- a/src/jacklib_helpers.py +++ b/src/jacklib_helpers.py @@ -76,3 +76,17 @@ def c_char_p_p_to_list(c_char_p_p): # C cast void* -> jack_default_audio_sample_t* def translate_audio_port_buffer(void_p): return jacklib.cast(void_p, jacklib.POINTER(jacklib.jack_default_audio_sample_t)) + +def translate_midi_event_buffer(void_p, size): + if (not void_p): + return list() + elif (size == 1): + return (void_p[0],) + elif (size == 2): + return (void_p[0], void_p[1]) + elif (size == 3): + return (void_p[0], void_p[1], void_p[2]) + elif (size == 4): + return (void_p[0], void_p[1], void_p[2], void_p[3]) + else: + return list() diff --git a/src/logs.py b/src/logs.py index e492b7e..8224c8f 100644 --- a/src/logs.py +++ b/src/logs.py @@ -347,7 +347,7 @@ class LogsW(QDialog, ui_logs.Ui_LogsW): def closeEvent(self, event): self.m_readThread.quit() - return QDialog.closeEvent(self, event) + QDialog.closeEvent(self, event) # ------------------------------------------------------------- # Allow to use this as a standalone app diff --git a/src/pixmapkeyboard.py b/src/pixmapkeyboard.py new file mode 100644 index 0000000..a68e8dd --- /dev/null +++ b/src/pixmapkeyboard.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Pixmap Keyboard, a custom Qt4 widget +# Copyright (C) 2012 Filipe Coelho +# +# 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, qCritical, Qt, QPointF, QRectF, QTimer, SIGNAL, SLOT +from PyQt4.QtGui import QFont, QPainter, QPixmap, QWidget + +# Imports (Custom Stuff) +import icons_rc + +midi_key2rect_map_horizontal = { + '0': QRectF( 0, 0, 18, 64), # C + '1': QRectF( 13, 0, 11, 42), # C# + '2': QRectF( 18, 0, 25, 64), # D + '3': QRectF( 37, 0, 11, 42), # D# + '4': QRectF( 42, 0, 18, 64), # E + '5': QRectF( 60, 0, 18, 64), # F + '6': QRectF( 73, 0, 11, 42), # F# + '7': QRectF( 78, 0, 25, 64), # G + '8': QRectF( 97, 0, 11, 42), # G# + '9': QRectF(102, 0, 25, 64), # A + '10': QRectF(121, 0, 11, 42), # A# + '11': QRectF(126, 0, 18, 64) # B +} + +midi_key2rect_map_vertical = { + '11': QRectF(0, 0, 64, 18), # B + '10': QRectF(0, 14, 42, 7), # A# + '9': QRectF(0, 18, 64, 24), # A + '8': QRectF(0, 38, 42, 7), # G# + '7': QRectF(0, 42, 64, 24), # G + '6': QRectF(0, 62, 42, 7), # F# + '5': QRectF(0, 66, 64, 18), # F + '4': QRectF(0, 84, 64, 18), # E + '3': QRectF(0, 98, 42, 7), # D# + '2': QRectF(0, 102, 64, 24), # D + '1': QRectF(0, 122, 42, 7), # C# + '0': QRectF(0, 126, 64, 18) # C +} + +midi_keyboard2key_map = { + # 3th octave + '%i' % (Qt.Key_Z): 48, + '%i' % (Qt.Key_S): 49, + '%i' % (Qt.Key_X): 50, + '%i' % (Qt.Key_D): 51, + '%i' % (Qt.Key_C): 52, + '%i' % (Qt.Key_V): 53, + '%i' % (Qt.Key_G): 54, + '%i' % (Qt.Key_B): 55, + '%i' % (Qt.Key_H): 56, + '%i' % (Qt.Key_N): 57, + '%i' % (Qt.Key_J): 58, + '%i' % (Qt.Key_M): 59, + # 4th octave + '%i' % (Qt.Key_Q): 60, + '%i' % (Qt.Key_2): 61, + '%i' % (Qt.Key_W): 62, + '%i' % (Qt.Key_3): 63, + '%i' % (Qt.Key_E): 64, + '%i' % (Qt.Key_R): 65, + '%i' % (Qt.Key_5): 66, + '%i' % (Qt.Key_T): 67, + '%i' % (Qt.Key_6): 68, + '%i' % (Qt.Key_Y): 69, + '%i' % (Qt.Key_7): 70, + '%i' % (Qt.Key_U): 71, +} + +# MIDI Keyboard, using a pixmap for painting +class PixmapKeyboard(QWidget): + + COLOR_CLASSIC = 0 + COLOR_ORANGE = 1 + + HORIZONTAL = 0 + VERTICAL = 1 + + def __init__(self, parent): + super(PixmapKeyboard, self).__init__(parent) + + self.m_octaves = 6 + self.m_lastMouseNote = -1 + + self.m_needsUpdate = False + self.m_enabledKeys = [] + + self.m_font = QFont("Monospace", 8, QFont.Normal) + self.m_pixmap = QPixmap("") + + self.setMode(self.HORIZONTAL) + + def noteOn(self, note, sendSignal=True): + if (note >= 0 and note <= 127 and note not in self.m_enabledKeys): + self.m_enabledKeys.append(note) + if (sendSignal): + self.emit(SIGNAL("noteOn(int)"), note) + + self.m_needsUpdate = True + QTimer.singleShot(0, self, SLOT("slot_updateOnce()")) + + if (len(self.m_enabledKeys) == 1): + self.emit(SIGNAL("notesOn()")) + + def noteOff(self, note, sendSignal=True): + if (note >= 0 and note <= 127 and note in self.m_enabledKeys): + self.m_enabledKeys.remove(note) + if (sendSignal): + self.emit(SIGNAL("noteOff(int)"), note) + + self.m_needsUpdate = True + QTimer.singleShot(0, self, SLOT("slot_updateOnce()")) + + if (len(self.m_enabledKeys) == 0): + self.emit(SIGNAL("notesOff()")) + + def _isNoteBlack(self, note): + baseNote = note % 12 + return bool(baseNote in (1, 3, 6, 8, 10)) + + def _getRectFromMidiNote(self, note): + return self.m_midi_map.get(str(note % 12)) + + def setMode(self, mode, color=COLOR_ORANGE): + if (color == self.COLOR_CLASSIC): + self.m_colorStr = "classic" + elif (color == self.COLOR_ORANGE): + self.m_colorStr = "orange" + else: + qCritical("PixmapKeyboard::setMode(%i, %i) - Invalid keyboard color", mode, color) + self.setMode(mode) + return + + if (mode == self.HORIZONTAL): + self.m_midi_map = midi_key2rect_map_horizontal + self.m_pixmap.load(":/bitmaps/kbd_h_%s.png" % (self.m_colorStr)) + self.m_pixmap_mode = self.HORIZONTAL + self.p_width = self.m_pixmap.width() + self.p_height = self.m_pixmap.height()/2 + elif (mode == self.VERTICAL): + self.m_midi_map = midi_key2rect_map_vertical + self.m_pixmap.load(":/bitmaps/kbd_v_%s.png" % (self.m_colorStr)) + self.m_pixmap_mode = self.VERTICAL + self.p_width = self.m_pixmap.width()/2 + self.p_height = self.m_pixmap.height() + else: + qCritical("PixmapKeyboard::setMode(%i, %i) - Invalid keyboard mode", mode, color) + self.setMode(self.HORIZONTAL) + return + + self.setOctaves(self.m_octaves) + + def setOctaves(self, octaves): + if (octaves < 1): + octaves = 1 + elif (octaves > 6): + octaves = 6 + self.m_octaves = octaves + + if (self.m_pixmap_mode == self.HORIZONTAL): + self.setMinimumSize(self.p_width*self.m_octaves, self.p_height) + self.setMaximumSize(self.p_width*self.m_octaves, self.p_height) + elif (self.m_pixmap_mode == self.VERTICAL): + self.setMinimumSize(self.p_width, self.p_height*self.m_octaves) + self.setMaximumSize(self.p_width, self.p_height*self.m_octaves) + + self.update() + + @pyqtSlot() + def slot_updateOnce(self): + if (self.m_needsUpdate): + self.update() + self.m_needsUpdate = False + + def keyPressEvent(self, event): + qKey = str(event.key()) + + if (qKey in midi_keyboard2key_map.keys()): + self.noteOn(midi_keyboard2key_map.get(qKey)) + + QWidget.keyPressEvent(self, event) + + def keyReleaseEvent(self, event): + qKey = str(event.key()) + + if (qKey in midi_keyboard2key_map.keys()): + self.noteOff(midi_keyboard2key_map.get(qKey)) + + QWidget.keyReleaseEvent(self, event) + + def mousePressEvent(self, event): + self.m_lastMouseNote = -1 + self.handleMousePos(event.pos()) + self.setFocus() + QWidget.mousePressEvent(self, event) + + def mouseMoveEvent(self, event): + self.handleMousePos(event.pos()) + QWidget.mousePressEvent(self, event) + + def mouseReleaseEvent(self, event): + if (self.m_lastMouseNote != -1): + self.noteOff(self.m_lastMouseNote) + self.m_lastMouseNote = -1 + QWidget.mouseReleaseEvent(self, event) + + def handleMousePos(self, pos): + if (self.m_pixmap_mode == self.HORIZONTAL): + if (pos.x() < 0 or pos.x() > self.m_octaves*144): + return + octave = int(pos.x()/self.p_width) + n_pos = QPointF(pos.x()%self.p_width, pos.y()) + elif (self.m_pixmap_mode == self.VERTICAL): + if (pos.y() < 0 or pos.y() > self.m_octaves*144): + return + octave = int(self.m_octaves-pos.y()/self.p_height) + n_pos = QPointF(pos.x(), pos.y()%self.p_height) + else: + return + + octave += 3 + + if (self.m_midi_map['1'].contains(n_pos)): # C# + note = 1 + elif (self.m_midi_map['3'].contains(n_pos)): # D# + note = 3 + elif (self.m_midi_map['6'].contains(n_pos)): # F# + note = 6 + elif (self.m_midi_map['8'].contains(n_pos)): # G# + note = 8 + elif (self.m_midi_map['10'].contains(n_pos)):# A# + note = 10 + elif (self.m_midi_map['0'].contains(n_pos)): # C + note = 0 + elif (self.m_midi_map['2'].contains(n_pos)): # D + note = 2 + elif (self.m_midi_map['4'].contains(n_pos)): # E + note = 4 + elif (self.m_midi_map['5'].contains(n_pos)): # F + note = 5 + elif (self.m_midi_map['7'].contains(n_pos)): # G + note = 7 + elif (self.m_midi_map['9'].contains(n_pos)): # A + note = 9 + elif (self.m_midi_map['11'].contains(n_pos)):# B + note = 11 + else: + note = -1 + + if (note != -1): + note += octave*12 + if (self.m_lastMouseNote != note): + self.noteOff(self.m_lastMouseNote) + self.noteOn(note) + else: + self.noteOff(self.m_lastMouseNote) + + self.m_lastMouseNote = note + + def paintEvent(self, event): + painter = QPainter(self) + + # ------------------------------------------------------------- + # Paint clean keys (as background) + + for octave in range(self.m_octaves): + if (self.m_pixmap_mode == self.HORIZONTAL): + target = QRectF(self.p_width*octave, 0, self.p_width, self.p_height) + elif (self.m_pixmap_mode == self.VERTICAL): + target = QRectF(0, self.p_height*octave, self.p_width, self.p_height) + else: + return + + source = QRectF(0, 0, self.p_width, self.p_height) + painter.drawPixmap(target, self.m_pixmap, source) + + # ------------------------------------------------------------- + # Paint (white) pressed keys + + paintedWhite = False + + for i in range(len(self.m_enabledKeys)): + note = self.m_enabledKeys[i] + pos = self._getRectFromMidiNote(note) + + if (self._isNoteBlack(note)): + continue + + if (note < 35): + # cannot paint this note + continue + elif (note < 48): + octave = 0 + elif (note < 60): + octave = 1 + elif (note < 72): + octave = 2 + elif (note < 84): + octave = 3 + elif (note < 96): + octave = 4 + elif (note < 108): + octave = 5 + else: + # cannot paint this note either + continue + + if (self.m_pixmap_mode == self.VERTICAL): + octave = self.m_octaves - octave - 1 + + if (self.m_pixmap_mode == self.HORIZONTAL): + target = QRectF(pos.x()+(self.p_width*octave), 0, pos.width(), pos.height()) + source = QRectF(pos.x(), self.p_height, pos.width(), pos.height()) + elif (self.m_pixmap_mode == self.VERTICAL): + target = QRectF(pos.x(), pos.y()+(self.p_height*octave), pos.width(), pos.height()) + source = QRectF(self.p_width, pos.y(), pos.width(), pos.height()) + else: + return + + paintedWhite = True + painter.drawPixmap(target, self.m_pixmap, source) + + # ------------------------------------------------------------- + # Clear white keys border + + if (paintedWhite): + for octave in range(self.m_octaves): + for note in (1, 3, 6, 8, 10): + pos = self._getRectFromMidiNote(note) + if (self.m_pixmap_mode == self.HORIZONTAL): + target = QRectF(pos.x()+(self.p_width*octave), 0, pos.width(), pos.height()) + source = QRectF(pos.x(), 0, pos.width(), pos.height()) + elif (self.m_pixmap_mode == self.VERTICAL): + target = QRectF(pos.x(), pos.y()+(self.p_height*octave), pos.width(), pos.height()) + source = QRectF(0, pos.y(), pos.width(), pos.height()) + else: + return + + painter.drawPixmap(target, self.m_pixmap, source) + + # ------------------------------------------------------------- + # Paint (black) pressed keys + + for i in range(len(self.m_enabledKeys)): + note = self.m_enabledKeys[i] + pos = self._getRectFromMidiNote(note) + + if (self._isNoteBlack(note) == False): + continue + + if (note < 35): + # cannot paint this note + continue + elif (note < 48): + octave = 0 + elif (note < 60): + octave = 1 + elif (note < 72): + octave = 2 + elif (note < 84): + octave = 3 + elif (note < 96): + octave = 4 + elif (note < 108): + octave = 5 + else: + # cannot paint this note either + continue + + if (self.m_pixmap_mode == self.VERTICAL): + octave = self.m_octaves - octave - 1 + + if (self.m_pixmap_mode == self.HORIZONTAL): + target = QRectF(pos.x()+(self.p_width*octave), 0, pos.width(), pos.height()) + source = QRectF(pos.x(), self.p_height, pos.width(), pos.height()) + elif (self.m_pixmap_mode == self.VERTICAL): + target = QRectF(pos.x(), pos.y()+(self.p_height*octave), pos.width(), pos.height()) + source = QRectF(self.p_width, pos.y(), pos.width(), pos.height()) + else: + return + + painter.drawPixmap(target, self.m_pixmap, source) + + # Paint C-number note info + painter.setFont(self.m_font) + painter.setPen(Qt.black) + for i in range(self.m_octaves): + if (self.m_pixmap_mode == self.HORIZONTAL): + painter.drawText(i*144, 48, 18, 18, Qt.AlignCenter, "C%s" % (i+2)) + elif (self.m_pixmap_mode == self.VERTICAL): + painter.drawText(45, (self.m_octaves*144)-(i*144)-16, 18, 18, Qt.AlignCenter, "C%s" % (i+2)) diff --git a/src/shared.py b/src/shared.py index 59207ff..ab250c2 100644 --- a/src/shared.py +++ b/src/shared.py @@ -58,6 +58,99 @@ else: PATH = PATH_env.split(os.pathsep) del PATH_env +MIDI_CC_LIST = ( + #"0x00 Bank Select", + "0x01 Modulation", + "0x02 Breath", + "0x03 (Undefined)", + "0x04 Foot", + "0x05 Portamento", + #"0x06 (Data Entry MSB)", + "0x07 Volume", + "0x08 Balance", + "0x09 (Undefined)", + "0x0A Pan", + "0x0B Expression", + "0x0C FX Control 1", + "0x0D FX Control 2", + "0x0E (Undefined)", + "0x0F (Undefined)", + "0x10 General Purpose 1", + "0x11 General Purpose 2", + "0x12 General Purpose 3", + "0x13 General Purpose 4", + "0x14 (Undefined)", + "0x15 (Undefined)", + "0x16 (Undefined)", + "0x17 (Undefined)", + "0x18 (Undefined)", + "0x19 (Undefined)", + "0x1A (Undefined)", + "0x1B (Undefined)", + "0x1C (Undefined)", + "0x1D (Undefined)", + "0x1E (Undefined)", + "0x1F (Undefined)", + #"0x20 *Bank Select", + #"0x21 *Modulation", + #"0x22 *Breath", + #"0x23 *(Undefined)", + #"0x24 *Foot", + #"0x25 *Portamento", + #"0x26 *(Data Entry MSB)", + #"0x27 *Volume", + #"0x28 *Balance", + #"0x29 *(Undefined)", + #"0x2A *Pan", + #"0x2B *Expression", + #"0x2C *FX *Control 1", + #"0x2D *FX *Control 2", + #"0x2E *(Undefined)", + #"0x2F *(Undefined)", + #"0x30 *General Purpose 1", + #"0x31 *General Purpose 2", + #"0x32 *General Purpose 3", + #"0x33 *General Purpose 4", + #"0x34 *(Undefined)", + #"0x35 *(Undefined)", + #"0x36 *(Undefined)", + #"0x37 *(Undefined)", + #"0x38 *(Undefined)", + #"0x39 *(Undefined)", + #"0x3A *(Undefined)", + #"0x3B *(Undefined)", + #"0x3C *(Undefined)", + #"0x3D *(Undefined)", + #"0x3E *(Undefined)", + #"0x3F *(Undefined)", + #"0x40 Damper On/Off", # <63 off, >64 on + #"0x41 Portamento On/Off", # <63 off, >64 on + #"0x42 Sostenuto On/Off", # <63 off, >64 on + #"0x43 Soft Pedal On/Off", # <63 off, >64 on + #"0x44 Legato Footswitch", # <63 Normal, >64 Legato + #"0x45 Hold 2", # <63 off, >64 on + "0x46 Control 1 [Variation]", + "0x47 Control 2 [Timbre]", + "0x48 Control 3 [Release]", + "0x49 Control 4 [Attack]", + "0x4A Control 5 [Brightness]", + "0x4B Control 6 [Decay]", + "0x4C Control 7 [Vib Rate]", + "0x4D Control 8 [Vib Depth]", + "0x4E Control 9 [Vib Delay]", + "0x4F Control 10 [Undefined]", + "0x50 General Purpose 5", + "0x51 General Purpose 6", + "0x52 General Purpose 8", + "0x53 General Purpose 9", + "0x54 Portamento Control", + "0x5B FX 1 Depth [Reverb]", + "0x5C FX 2 Depth [Tremolo]", + "0x5D FX 3 Depth [Chorus]", + "0x5E FX 4 Depth [Detune]", + "0x5F FX 5 Depth [Phaser]" + ) + # Get Icon from user theme, using our own as backup (Oxygen) def getIcon(icon, size=16): return QIcon.fromTheme(icon, QIcon(":/%ix%i/%s.png" % (size, size, icon)))