Nils
2 years ago
10 changed files with 1017 additions and 393 deletions
@ -0,0 +1,330 @@ |
|||
#! /usr/bin/env python3 |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) |
|||
|
|||
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), |
|||
|
|||
This 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 3 of the License, or |
|||
(at your option) 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. |
|||
|
|||
You should have received a copy of the GNU General Public License |
|||
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
""" |
|||
import logging; logger = logging.getLogger(__name__); logger.info("import") |
|||
|
|||
#Standard Library Modules |
|||
|
|||
#Third Party Modules |
|||
from PyQt5 import QtWidgets, QtCore, QtGui, QtOpenGL |
|||
|
|||
#Template Modules |
|||
from template.qtgui.helper import stretchRect |
|||
from template.engine.duration import baseDurationToTraditionalNumber |
|||
|
|||
#User modules |
|||
import engine.api as api |
|||
|
|||
|
|||
MAX_DURATION = 200 #to keep the code copy/paste compatible with piano grid we use the same constant but use our own value |
|||
STAFFLINEGAP = 20 #cannot be changed during runtime |
|||
SCOREHEIGHT = STAFFLINEGAP * 128 #notes |
|||
|
|||
|
|||
class VerticalPiano(QtWidgets.QGraphicsView): |
|||
def __init__(self, mainWindow): |
|||
super().__init__(mainWindow) |
|||
self.mainWindow = mainWindow |
|||
|
|||
viewport = QtWidgets.QOpenGLWidget() |
|||
viewportFormat = QtGui.QSurfaceFormat() |
|||
viewportFormat.setSwapInterval(0) #disable VSync |
|||
#viewportFormat.setSamples(2**8) #By default, the highest number of samples available is used. |
|||
viewportFormat.setDefaultFormat(viewportFormat) |
|||
viewport.setFormat(viewportFormat) |
|||
self.setViewport(viewport) |
|||
|
|||
self.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) |
|||
self.setDragMode(QtWidgets.QGraphicsView.NoDrag) |
|||
|
|||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) |
|||
#self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) |
|||
|
|||
self.pianoScene = _VerticalPianoScene(self) |
|||
self.setScene(self.pianoScene) |
|||
|
|||
self.setSceneRect(QtCore.QRectF(0, 0, MAX_DURATION/2, SCOREHEIGHT)) #x,y,w,h |
|||
#self.setFixedHeight(SCOREHEIGHT+3) # Don't set to scoreheight. Then we don't get a scrollbar. We need to set the sceneRect to 100%, not the view. |
|||
#self.mainWindow.ui.verticalPianoFrame.setFixedHeight(SCOREHEIGHT+3) #Don't. This makes the whole window a fixed size! |
|||
#self.setFixedWidth(MAX_DURATION) #Also done by parent widget in mainWindow |
|||
|
|||
self.setLineWidth(0) |
|||
|
|||
self.centerOn(0, 64*STAFFLINEGAP) |
|||
|
|||
|
|||
class _VerticalPianoScene(QtWidgets.QGraphicsScene): |
|||
"""Most of this is copy paste from piano grid""" |
|||
|
|||
def __init__(self, parentView): |
|||
super().__init__() |
|||
|
|||
self.parentView = parentView |
|||
#Set color, otherwise it will be transparent in window managers or wayland that want that. |
|||
self.backColor = QtGui.QColor() |
|||
self.backColor.setNamedColor("#fdfdff") |
|||
self.setBackgroundBrush(self.backColor) |
|||
|
|||
self.linesHorizontal = [] |
|||
self.highlights = {} |
|||
self.colorKeys = {} |
|||
self.blackKeys = [] |
|||
self.numberLabels = [] #index is pitch |
|||
|
|||
|
|||
self._selectedInstrument = None #tuple instrumentStatus, instrumentData |
|||
self._leftMouseDown = False #For note preview |
|||
|
|||
self.gridPen = QtGui.QPen(QtCore.Qt.SolidLine) |
|||
self.gridPen.setCosmetic(True) |
|||
|
|||
#Create two lines for the upper/lower boundaries first. They are just cosmetic |
|||
boldPen = QtGui.QPen(QtCore.Qt.SolidLine) |
|||
boldPen.setCosmetic(True) |
|||
boldPen.setWidth(1) |
|||
|
|||
hlineUp = QtWidgets.QGraphicsLineItem(0, 0, MAX_DURATION*2, 0) #x1, y1, x2, y2 |
|||
hlineUp.setPen(boldPen) |
|||
self.addItem(hlineUp) |
|||
hlineUp.setPos(0, 0) |
|||
hlineUp.setEnabled(False) |
|||
hlineUp.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
|||
hlineUp.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
|||
self.linesHorizontal.append(hlineUp) |
|||
|
|||
hlineDown = QtWidgets.QGraphicsLineItem(0, 0, MAX_DURATION*2, 0) #x1, y1, x2, y2 |
|||
hlineDown.setPen(boldPen) |
|||
self.addItem(hlineDown) |
|||
hlineDown.setPos(0, 128 * STAFFLINEGAP) |
|||
hlineDown.setEnabled(False) |
|||
hlineDown.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
|||
hlineDown.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
|||
self.linesHorizontal.append(hlineDown) |
|||
|
|||
for i in range(128): |
|||
hline = QtWidgets.QGraphicsLineItem(0, 0, MAX_DURATION*2, 0) #x1, y1, x2, y2 |
|||
hline.setPen(self.gridPen) |
|||
self.addItem(hline) |
|||
hline.setPos(0, i * STAFFLINEGAP) |
|||
hline.setEnabled(False) |
|||
hline.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
|||
self.linesHorizontal.append(hline) |
|||
|
|||
blackKey = i % 12 in (1, 3, 6, 8, 10) |
|||
if blackKey: |
|||
bk = BlackKey(self) |
|||
self.blackKeys.append(bk) |
|||
self.addItem(bk) |
|||
bk.setPos(0, (127-i) * STAFFLINEGAP) |
|||
|
|||
#Various purpose color keys. They are opaque and are on top of white/black keys |
|||
ck = ColorKey(self, QtGui.QColor("cyan")) |
|||
self.addItem(ck) |
|||
self.colorKeys[i] = ck |
|||
ck.setPos(0, (127-i) * STAFFLINEGAP) |
|||
|
|||
#Highlights on top of colors. Indication if note is played. |
|||
hl = Highlight(self) |
|||
self.addItem(hl) |
|||
self.highlights[i] = hl |
|||
hl.setPos(0, (127-i) * STAFFLINEGAP) |
|||
|
|||
#Numbers last so they are on top. |
|||
numberLabel = NumberLabel(self, 127-i) |
|||
self.addItem(numberLabel) |
|||
self.numberLabels.append(numberLabel) #index is pitch |
|||
numberLabel.setPos(0, i * STAFFLINEGAP + 2) |
|||
numberLabel.setZValue(10) |
|||
|
|||
self.numberLabels.reverse() |
|||
|
|||
self.fakeDeactivationOverlay = QtWidgets.QGraphicsRectItem(0,0,MAX_DURATION,SCOREHEIGHT) |
|||
self.fakeDeactivationOverlay.setBrush(QtGui.QColor("black")) |
|||
self.fakeDeactivationOverlay.setOpacity(0.6) |
|||
self.fakeDeactivationOverlay.setEnabled(False) |
|||
self.addItem(self.fakeDeactivationOverlay) |
|||
self.fakeDeactivationOverlay.setPos(0,0) |
|||
self.fakeDeactivationOverlay.show() |
|||
#Keyboard Creation Done |
|||
|
|||
api.callbacks.instrumentMidiNoteOnActivity.append(self.highlightNoteOn) |
|||
api.callbacks.instrumentMidiNoteOffActivity.append(self.highlightNoteOff) |
|||
api.callbacks.instrumentStatusChanged.append(self.instrumentStatusChanged) |
|||
|
|||
def clearVerticalPiano(self): |
|||
self.allHighlightsOff() |
|||
for nl in self.numberLabels: |
|||
nl.setLabel("") #reset to just number |
|||
self.fakeDeactivationOverlay.show() |
|||
|
|||
def instrumentStatusChanged(self, instrumentStatus:dict): |
|||
"""GUI callback. Data is live""" |
|||
#Is this for us? |
|||
if instrumentStatus and self._selectedInstrument and not instrumentStatus["idKey"] == self._selectedInstrument[0]["idKey"]: |
|||
return |
|||
#else: |
|||
# print ("not for us", instrumentStatus["idKey"]) |
|||
|
|||
self.clearVerticalPiano() |
|||
if not instrumentStatus["state"]: |
|||
self.fakeDeactivationOverlay.show() |
|||
return |
|||
|
|||
self.fakeDeactivationOverlay.hide() |
|||
|
|||
for keyPitch, keyObject in self.colorKeys.items(): |
|||
#self.numberLabels[keyPitch].show() |
|||
keyObject.show() |
|||
|
|||
if keyPitch in instrumentStatus["keySwitches"]: |
|||
opcode, keyswitchLabel = instrumentStatus["keySwitches"][keyPitch] |
|||
self.numberLabels[keyPitch].setLabel(keyswitchLabel) |
|||
keyObject.setBrush(QtGui.QColor("red")) |
|||
elif keyPitch in instrumentStatus["playableKeys"]: |
|||
keyObject.setBrush(QtGui.QColor("orange")) |
|||
else: |
|||
#self.numberLabels[keyPitch].hide() |
|||
keyObject.hide() |
|||
|
|||
|
|||
|
|||
def selectedInstrumentChanged(self, instrumentStatus, instrumentData): |
|||
"""GUI click to different instrument. The arguments are cached GUI data""" |
|||
self._selectedInstrument = (instrumentStatus, instrumentData) |
|||
self.instrumentStatusChanged(instrumentStatus) |
|||
|
|||
def highlightNoteOn(self, idKey:tuple, pitch:int, velocity:int): |
|||
highlight = self.highlights[pitch] |
|||
highlight.show() |
|||
|
|||
def highlightNoteOff(self, idKey:tuple, pitch:int, velocity:int): |
|||
highlight = self.highlights[pitch] |
|||
highlight.hide() |
|||
|
|||
def allHighlightsOff(self): |
|||
for pitch, highlight in self.highlights.items(): |
|||
highlight.hide() |
|||
|
|||
def mousePressEvent(self, event): |
|||
self._leftMouseDown = False |
|||
if event.button() == QtCore.Qt.LeftButton: |
|||
self._leftMouseDown = True |
|||
self._lastPlayPitch = None #this must not be in _play, otherwise you can't move the mouse while pressed down |
|||
self._play(event) |
|||
super().mousePressEvent(event) |
|||
|
|||
def _off(self): |
|||
if self._selectedInstrument and not self._lastPlayPitch is None: |
|||
status, data = self._selectedInstrument |
|||
libId, instrId = status["idKey"] |
|||
api.sendNoteOffToInstrument(status["idKey"], self._lastPlayPitch) |
|||
self._lastPlayPitch = None |
|||
|
|||
def _play(self, event): |
|||
assert self._leftMouseDown |
|||
|
|||
pitch = 127 - int(event.scenePos().y() / STAFFLINEGAP) |
|||
if pitch < 0 or pitch > 127: |
|||
pitch = None |
|||
|
|||
if self._selectedInstrument and not pitch == self._lastPlayPitch: |
|||
#TODO: Play note on at a different instrument than note off? Possible? |
|||
status, data = self._selectedInstrument |
|||
|
|||
if not self._lastPlayPitch is None: |
|||
#Force a note off that is currently playing but not under the cursor anymore |
|||
#User did some input tricks with keyboard and mouse combined etc. |
|||
api.sendNoteOffToInstrument(status["idKey"], self._lastPlayPitch) |
|||
|
|||
if not pitch is None: |
|||
#This is the normal note-on click |
|||
api.sendNoteOnToInstrument(status["idKey"], pitch) |
|||
|
|||
self._lastPlayPitch = pitch |
|||
|
|||
def mouseMoveEvent(self, event): |
|||
"""Event button is always 0 in a mouse move event""" |
|||
if self._leftMouseDown: |
|||
self._play(event) |
|||
super().mouseMoveEvent(event) |
|||
|
|||
def mouseReleaseEvent(self, event): |
|||
if self._leftMouseDown: |
|||
self._off() |
|||
self._leftMouseDown = False |
|||
if event.button() == QtCore.Qt.LeftButton: |
|||
self._lastPlayPitch = None |
|||
super().mouseReleaseEvent(event) |
|||
|
|||
|
|||
class NumberLabel(QtWidgets.QGraphicsSimpleTextItem): |
|||
|
|||
def __init__(self, parentGrid, number:int): |
|||
|
|||
super().__init__() |
|||
self.parentGrid = parentGrid |
|||
self.number = number |
|||
self.currentLabel = "" |
|||
self.setText(str(number)) |
|||
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
|||
self.setScale(1) |
|||
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
|||
blackKey = number % 12 in (1, 3, 6, 8, 10) |
|||
if blackKey: |
|||
self.setBrush(QtGui.QColor("white")) |
|||
else: |
|||
self.setBrush(QtGui.QColor("black")) |
|||
|
|||
def setLabel(self, label:str): |
|||
"""Each key can have an optional text label for keyswitches, percussion names etc. |
|||
Use with empty string to reset to just the midi pitch number.""" |
|||
self.currentLabel = label |
|||
self.setText(f"{self.number} {label}") |
|||
|
|||
|
|||
class Highlight(QtWidgets.QGraphicsRectItem): |
|||
def __init__(self, parentGrid): |
|||
super().__init__(0, 0, MAX_DURATION, STAFFLINEGAP) #x, y, w, h |
|||
self.setEnabled(False) #Not clickable, still visible. |
|||
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) #we still need this otherwise no rubberband. |
|||
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
|||
self.setBrush(QtGui.QColor("cyan")) |
|||
self.setOpacity(0.5) |
|||
self.hide() |
|||
|
|||
|
|||
class ColorKey(QtWidgets.QGraphicsRectItem): |
|||
def __init__(self, parentGrid, color:QtGui.QColor): |
|||
super().__init__(0, 0, MAX_DURATION, STAFFLINEGAP) #x, y, w, h |
|||
self.setEnabled(False) #Not clickable, still visible. |
|||
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) #we still need this otherwise no rubberband. |
|||
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
|||
self.setBrush(color) |
|||
self.setOpacity(0.2) #just a tint |
|||
self.hide() |
|||
|
|||
class BlackKey(QtWidgets.QGraphicsRectItem): |
|||
def __init__(self, parentGrid): |
|||
super().__init__(0, 0, MAX_DURATION, STAFFLINEGAP) #x, y, w, h |
|||
self.parentGrid = parentGrid |
|||
self.setPen(QtGui.QPen(QtCore.Qt.NoPen)) |
|||
self.setBrush(QtGui.QColor("black")) |
|||
self.setEnabled(False) |
|||
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) #we still need this otherwise no rubberband. |
Loading…
Reference in new issue