#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Pixmap Keyboard, a custom Qt4 widget # Copyright (C) 2011-2013 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 GPL.txt file # ------------------------------------------------------------------------------------------------------------ # Imports (Global) from PyQt4.QtCore import pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer, SIGNAL, SLOT from PyQt4.QtGui import QFont, QPainter, QPixmap, QWidget # ------------------------------------------------------------------------------------------------------------ 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): # enum Color COLOR_CLASSIC = 0 COLOR_ORANGE = 1 # enum Orientation HORIZONTAL = 0 VERTICAL = 1 def __init__(self, parent): QWidget.__init__(self, 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.setCursor(Qt.PointingHandCursor) self.setMode(self.HORIZONTAL) def allNotesOff(self): self.m_enabledKeys = [] self.m_needsUpdate = True QTimer.singleShot(0, self, SLOT("slot_updateOnce()")) self.emit(SIGNAL("notesOff()")) def sendNoteOn(self, note, sendSignal=True): if 0 <= 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 sendNoteOff(self, note, sendSignal=True): if 0 <= 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 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 color" % (mode, color)) return self.setMode(mode) if mode == self.HORIZONTAL: self.m_midiMap = midi_key2rect_map_horizontal self.m_pixmap.load(":/bitmaps/kbd_h_%s.png" % self.m_colorStr) self.m_pixmapMode = self.HORIZONTAL self.p_width = self.m_pixmap.width() self.p_height = self.m_pixmap.height() / 2 elif mode == self.VERTICAL: self.m_midiMap = midi_key2rect_map_vertical self.m_pixmap.load(":/bitmaps/kbd_v_%s.png" % self.m_colorStr) self.m_pixmapMode = self.VERTICAL self.p_width = self.m_pixmap.width() / 2 self.p_height = self.m_pixmap.height() else: qCritical("PixmapKeyboard::setMode(%i, %i) - invalid mode" % (mode, color)) return self.setMode(self.HORIZONTAL) self.setOctaves(self.m_octaves) def setOctaves(self, octaves): if octaves < 1: octaves = 1 elif octaves > 8: octaves = 8 self.m_octaves = octaves if self.m_pixmapMode == 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_pixmapMode == 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() def handleMousePos(self, pos): if self.m_pixmapMode == self.HORIZONTAL: if pos.x() < 0 or pos.x() > self.m_octaves * 144: return posX = pos.x() - 1 octave = int(posX / self.p_width) n_pos = QPointF(posX % self.p_width, pos.y()) elif self.m_pixmapMode == self.VERTICAL: if pos.y() < 0 or pos.y() > self.m_octaves * 144: return posY = pos.y() - 1 octave = int(self.m_octaves - posY / self.p_height) n_pos = QPointF(pos.x(), posY % self.p_height) else: return octave += 3 if self.m_midiMap['1'].contains(n_pos): # C# note = 1 elif self.m_midiMap['3'].contains(n_pos): # D# note = 3 elif self.m_midiMap['6'].contains(n_pos): # F# note = 6 elif self.m_midiMap['8'].contains(n_pos): # G# note = 8 elif self.m_midiMap['10'].contains(n_pos):# A# note = 10 elif self.m_midiMap['0'].contains(n_pos): # C note = 0 elif self.m_midiMap['2'].contains(n_pos): # D note = 2 elif self.m_midiMap['4'].contains(n_pos): # E note = 4 elif self.m_midiMap['5'].contains(n_pos): # F note = 5 elif self.m_midiMap['7'].contains(n_pos): # G note = 7 elif self.m_midiMap['9'].contains(n_pos): # A note = 9 elif self.m_midiMap['11'].contains(n_pos):# B note = 11 else: note = -1 if note != -1: note += octave * 12 if self.m_lastMouseNote != note: self.sendNoteOff(self.m_lastMouseNote) self.sendNoteOn(note) else: self.sendNoteOff(self.m_lastMouseNote) self.m_lastMouseNote = note def keyPressEvent(self, event): if not event.isAutoRepeat(): qKey = str(event.key()) if qKey in midi_keyboard2key_map.keys(): self.sendNoteOn(midi_keyboard2key_map.get(qKey)) QWidget.keyPressEvent(self, event) def keyReleaseEvent(self, event): if not event.isAutoRepeat(): qKey = str(event.key()) if qKey in midi_keyboard2key_map.keys(): self.sendNoteOff(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.sendNoteOff(self.m_lastMouseNote) self.m_lastMouseNote = -1 QWidget.mouseReleaseEvent(self, event) def paintEvent(self, event): painter = QPainter(self) # ------------------------------------------------------------- # Paint clean keys (as background) for octave in range(self.m_octaves): if self.m_pixmapMode == self.HORIZONTAL: target = QRectF(self.p_width * octave, 0, self.p_width, self.p_height) elif self.m_pixmapMode == 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 < 36: # 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 elif note < 120: octave = 6 elif note < 132: octave = 7 else: # cannot paint this note either continue if self.m_pixmapMode == self.VERTICAL: octave = self.m_octaves - octave - 1 if self.m_pixmapMode == 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_pixmapMode == 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_pixmapMode == 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_pixmapMode == 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 not self._isNoteBlack(note): continue if note < 36: # 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 elif note < 120: octave = 6 elif note < 132: octave = 7 else: # cannot paint this note either continue if self.m_pixmapMode == self.VERTICAL: octave = self.m_octaves - octave - 1 if self.m_pixmapMode == 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_pixmapMode == 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_pixmapMode == self.HORIZONTAL: painter.drawText(i * 144, 48, 18, 18, Qt.AlignCenter, "C%i" % int(i + 2)) elif self.m_pixmapMode == self.VERTICAL: painter.drawText(45, (self.m_octaves * 144) - (i * 144) - 16, 18, 18, Qt.AlignCenter, "C%i" % int(i + 2)) event.accept() @pyqtSlot() def slot_updateOnce(self): if self.m_needsUpdate: self.update() self.m_needsUpdate = False def _isNoteBlack(self, note): baseNote = note % 12 return bool(baseNote in (1, 3, 6, 8, 10)) def _getRectFromMidiNote(self, note): return self.m_midiMap.get(str(note % 12))