Browse Source

MidiPattern: Create UI file, split core into pianoroll.py

tags/1.9.7
falkTX 10 years ago
parent
commit
a47bb2c131
5 changed files with 1454 additions and 954 deletions
  1. +1
    -0
      .gitignore
  2. +7
    -1
      Makefile
  3. +499
    -0
      resources/ui/midipattern.ui
  4. +60
    -953
      source/native-plugins/resources/midipattern-ui
  5. +887
    -0
      source/widgets/pianoroll.py

+ 1
- 0
.gitignore View File

@@ -60,6 +60,7 @@ source/carla_config.py
source/digitalpeakmeter.py
source/ledbutton.py
source/paramspinbox.py
source/pianoroll.py
source/pixmapbutton.py
source/pixmapdial.py
source/pixmapkeyboard.py


+ 7
- 1
Makefile View File

@@ -269,6 +269,7 @@ RES = \
bin/resources/paramspinbox.py \
bin/resources/patchcanvas.py \
bin/resources/patchcanvas_theme.py \
bin/resources/pianoroll.py \
bin/resources/pixmapbutton.py \
bin/resources/pixmapdial.py \
bin/resources/pixmapkeyboard.py \
@@ -290,6 +291,7 @@ RES = \
bin/resources/ui_carla_settings.py \
bin/resources/ui_carla_settings_driver.py \
bin/resources/ui_inputdialog_value.py \
bin/resources/ui_midipattern.py \
source/carla_config.py \
source/resources_rc.py

@@ -333,7 +335,8 @@ UIs = \
source/ui_carla_refresh.py \
source/ui_carla_settings.py \
source/ui_carla_settings_driver.py \
source/ui_inputdialog_value.py
source/ui_inputdialog_value.py \
source/ui_midipattern.py

UI: $(UIs)

@@ -351,6 +354,7 @@ WIDGETS = \
source/digitalpeakmeter.py \
source/ledbutton.py \
source/paramspinbox.py \
source/pianoroll.py \
source/pixmapbutton.py \
source/pixmapdial.py \
source/pixmapkeyboard.py \
@@ -593,6 +597,7 @@ endif
$(LINK) $(PREFIX)/share/carla/paramspinbox.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/patchcanvas.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/patchcanvas_theme.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/pianoroll.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/pixmapbutton.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/pixmapdial.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/pixmapkeyboard.py $(DESTDIR)$(PREFIX)/share/carla/resources/
@@ -614,6 +619,7 @@ endif
$(LINK) $(PREFIX)/share/carla/ui_carla_settings.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/ui_carla_settings_driver.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/ui_inputdialog_value.py $(DESTDIR)$(PREFIX)/share/carla/resources/
$(LINK) $(PREFIX)/share/carla/ui_midipattern.py $(DESTDIR)$(PREFIX)/share/carla/resources/

# Adjust PREFIX value in script files
sed -i "s?X-PREFIX-X?$(PREFIX)?" \


+ 499
- 0
resources/ui/midipattern.ui View File

@@ -0,0 +1,499 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MidiPatternW</class>
<widget class="QMainWindow" name="MidiPatternW">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>755</width>
<height>436</height>
</rect>
</property>
<property name="windowTitle">
<string>MIDI Pattern</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="ModeIndicator" name="modeIndicator" native="true">
<property name="minimumSize">
<size>
<width>30</width>
<height>20</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>20</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="timeSigLabel">
<property name="text">
<string>Time Signature:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="timeSigBox">
<property name="editable">
<bool>true</bool>
</property>
<property name="currentIndex">
<number>3</number>
</property>
<item>
<property name="text">
<string>1/4</string>
</property>
</item>
<item>
<property name="text">
<string>2/4</string>
</property>
</item>
<item>
<property name="text">
<string>3/4</string>
</property>
</item>
<item>
<property name="text">
<string>4/4</string>
</property>
</item>
<item>
<property name="text">
<string>5/4</string>
</property>
</item>
<item>
<property name="text">
<string>6/4</string>
</property>
</item>
<item>
<property name="text">
<string>12/8</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="measureLabel">
<property name="text">
<string>Measures:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="measureBox">
<property name="currentIndex">
<number>3</number>
</property>
<item>
<property name="text">
<string>1</string>
</property>
</item>
<item>
<property name="text">
<string>2</string>
</property>
</item>
<item>
<property name="text">
<string>3</string>
</property>
</item>
<item>
<property name="text">
<string>4</string>
</property>
</item>
<item>
<property name="text">
<string>5</string>
</property>
</item>
<item>
<property name="text">
<string>6</string>
</property>
</item>
<item>
<property name="text">
<string>7</string>
</property>
</item>
<item>
<property name="text">
<string>8</string>
</property>
</item>
<item>
<property name="text">
<string>9</string>
</property>
</item>
<item>
<property name="text">
<string>10</string>
</property>
</item>
<item>
<property name="text">
<string>11</string>
</property>
</item>
<item>
<property name="text">
<string>12</string>
</property>
</item>
<item>
<property name="text">
<string>13</string>
</property>
</item>
<item>
<property name="text">
<string>14</string>
</property>
</item>
<item>
<property name="text">
<string>15</string>
</property>
</item>
<item>
<property name="text">
<string>16</string>
</property>
</item>
<item>
<property name="text">
<string>17</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="defaultLengthLabel">
<property name="text">
<string>Default Length:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="defaultLengthBox">
<item>
<property name="text">
<string>1/16</string>
</property>
</item>
<item>
<property name="text">
<string>1/15</string>
</property>
</item>
<item>
<property name="text">
<string>1/12</string>
</property>
</item>
<item>
<property name="text">
<string>1/9</string>
</property>
</item>
<item>
<property name="text">
<string>1/8</string>
</property>
</item>
<item>
<property name="text">
<string>1/6</string>
</property>
</item>
<item>
<property name="text">
<string>1/4</string>
</property>
</item>
<item>
<property name="text">
<string>1/3</string>
</property>
</item>
<item>
<property name="text">
<string>1/2</string>
</property>
</item>
<item>
<property name="text">
<string>1</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="quantizeLabel">
<property name="text">
<string>Quantize:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="quantizeBox">
<item>
<property name="text">
<string>1/16</string>
</property>
</item>
<item>
<property name="text">
<string>1/15</string>
</property>
</item>
<item>
<property name="text">
<string>1/12</string>
</property>
</item>
<item>
<property name="text">
<string>1/9</string>
</property>
</item>
<item>
<property name="text">
<string>1/8</string>
</property>
</item>
<item>
<property name="text">
<string>1/6</string>
</property>
</item>
<item>
<property name="text">
<string>1/4</string>
</property>
</item>
<item>
<property name="text">
<string>1/3</string>
</property>
</item>
<item>
<property name="text">
<string>1/2</string>
</property>
</item>
<item>
<property name="text">
<string>1</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QSlider" name="hSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QSlider" name="vSlider">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="PianoRollView" name="graphicsView"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>755</width>
<height>20</height>
</rect>
</property>
<widget class="QMenu" name="menu_File">
<property name="title">
<string>&amp;File</string>
</property>
<addaction name="act_file_quit"/>
</widget>
<widget class="QMenu" name="menu_Edit">
<property name="title">
<string>&amp;Edit</string>
</property>
<addaction name="act_edit_insert"/>
<addaction name="act_edit_velocity"/>
<addaction name="act_edit_select_all"/>
</widget>
<addaction name="menu_File"/>
<addaction name="menu_Edit"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="act_file_quit">
<property name="text">
<string>&amp;Quit</string>
</property>
</action>
<action name="act_edit_insert">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>&amp;Insert Mode</string>
</property>
<property name="shortcut">
<string>F</string>
</property>
</action>
<action name="act_edit_velocity">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>&amp;Velocity Mode</string>
</property>
<property name="shortcut">
<string>D</string>
</property>
</action>
<action name="act_edit_select_all">
<property name="text">
<string>Select All</string>
</property>
<property name="shortcut">
<string>A</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>PianoRollView</class>
<extends>QGraphicsView</extends>
<header>pianoroll.h</header>
</customwidget>
<customwidget>
<class>ModeIndicator</class>
<extends>QWidget</extends>
<header>pianoroll.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>act_file_quit</sender>
<signal>triggered()</signal>
<receiver>MidiPatternW</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>377</x>
<y>217</y>
</hint>
</hints>
</connection>
</connections>
</ui>

+ 60
- 953
source/native-plugins/resources/midipattern-ui
File diff suppressed because it is too large
View File


+ 887
- 0
source/widgets/pianoroll.py View File

@@ -0,0 +1,887 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# A piano roll viewer/editor
# Copyright (C) 2012-2015 Filipe Coelho <falktx@falktx.com>
# Copyright (C) 2014-2015 Perry Nguyen
#
# 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 (Config)

from carla_config import *

# ------------------------------------------------------------------------------------------------------------
# Imports (Global)

if config_UseQt5:
from PyQt5.QtCore import Qt, QRectF, QPointF, pyqtSignal
from PyQt5.QtGui import QColor, QFont, QPen, QPainter
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
from PyQt5.QtWidgets import QWidget, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QStyle
else:
from PyQt4.QtCore import Qt, QRectF, QPointF, pyqtSignal
from PyQt4.QtGui import QColor, QFont, QPen, QPainter
from PyQt4.QtGui import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
from PyQt4.QtGui import QGraphicsScene, QGraphicsView
from PyQt4.QtGui import QWidget, QLabel, QComboBox, QSlider, QHBoxLayout, QVBoxLayout, QStyle

# ------------------------------------------------------------------------------------------------------------
# Imports (Custom)

from carla_shared import *

# ------------------------------------------------------------------------------------------------------------
# MIDI definitions, copied from CarlaMIDI.h

MAX_MIDI_CHANNELS = 16
MAX_MIDI_NOTE = 128
MAX_MIDI_VALUE = 128
MAX_MIDI_CONTROL = 120 # 0x77

MIDI_STATUS_BIT = 0xF0
MIDI_CHANNEL_BIT = 0x0F

# MIDI Messages List
MIDI_STATUS_NOTE_OFF = 0x80 # note (0-127), velocity (0-127)
MIDI_STATUS_NOTE_ON = 0x90 # note (0-127), velocity (0-127)
MIDI_STATUS_POLYPHONIC_AFTERTOUCH = 0xA0 # note (0-127), pressure (0-127)
MIDI_STATUS_CONTROL_CHANGE = 0xB0 # see 'Control Change Messages List'
MIDI_STATUS_PROGRAM_CHANGE = 0xC0 # program (0-127), none
MIDI_STATUS_CHANNEL_PRESSURE = 0xD0 # pressure (0-127), none
MIDI_STATUS_PITCH_WHEEL_CONTROL = 0xE0 # LSB (0-127), MSB (0-127)

# MIDI Message type
def MIDI_IS_CHANNEL_MESSAGE(status): return status >= MIDI_STATUS_NOTE_OFF and status < MIDI_STATUS_BIT
def MIDI_IS_SYSTEM_MESSAGE(status): return status >= MIDI_STATUS_BIT and status <= 0xFF
def MIDI_IS_OSC_MESSAGE(status): return status == '/' or status == '#'

# MIDI Channel message type
def MIDI_IS_STATUS_NOTE_OFF(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_OFF
def MIDI_IS_STATUS_NOTE_ON(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_ON
def MIDI_IS_STATUS_POLYPHONIC_AFTERTOUCH(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_POLYPHONIC_AFTERTOUCH
def MIDI_IS_STATUS_CONTROL_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CONTROL_CHANGE
def MIDI_IS_STATUS_PROGRAM_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PROGRAM_CHANGE
def MIDI_IS_STATUS_CHANNEL_PRESSURE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CHANNEL_PRESSURE
def MIDI_IS_STATUS_PITCH_WHEEL_CONTROL(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PITCH_WHEEL_CONTROL

# MIDI Utils
def MIDI_GET_STATUS_FROM_DATA(data): return data[0] & MIDI_STATUS_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else data[0]
def MIDI_GET_CHANNEL_FROM_DATA(data): return data[0] & MIDI_CHANNEL_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else 0

# ------------------------------------------------------------------------------------------------------------
# Graphics Items

class NoteExpander(QGraphicsRectItem):
def __init__(self, length, height, parent):
QGraphicsRectItem.__init__(self, 0, 0, length, height, parent)
self.parent = parent
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)

clearpen = QPen(QColor(0,0,0,0))
self.setPen(clearpen)

self.orig_brush = QColor(0, 0, 0, 0)
self.hover_brush = QColor(200, 200, 200)
self.stretch = False

def mousePressEvent(self, event):
QGraphicsRectItem.mousePressEvent(self, event)
self.stretch = True

def hoverEnterEvent(self, event):
QGraphicsRectItem.hoverEnterEvent(self, event)
if self.parent.isSelected():
self.parent.setBrush(self.parent.select_brush)
else:
self.parent.setBrush(self.parent.orig_brush)
self.setBrush(self.hover_brush)

def hoverLeaveEvent(self, event):
QGraphicsRectItem.hoverLeaveEvent(self, event)
if self.parent.isSelected():
self.parent.setBrush(self.parent.select_brush)
elif self.parent.hovering:
self.parent.setBrush(self.parent.hover_brush)
else:
self.parent.setBrush(self.parent.orig_brush)
self.setBrush(self.orig_brush)

class NoteItem(QGraphicsRectItem):
'''a note on the pianoroll sequencer'''
def __init__(self, height, length, note_info):
QGraphicsRectItem.__init__(self, 0, 0, length, height)

self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)

clearpen = QPen(QColor(0,0,0,0))
self.setPen(clearpen)
self.orig_brush = QColor(note_info[3], 0, 0)
self.hover_brush = QColor(note_info[3] + 100, 200, 100)
self.select_brush = QColor(note_info[3] + 100, 100, 100)
self.setBrush(self.orig_brush)

self.note = note_info
self.length = length
self.piano = self.scene

self.pressed = False
self.hovering = False
self.moving_diff = (0,0)
self.expand_diff = 0

l = 5
self.front = NoteExpander(l, height, self)
self.back = NoteExpander(l, height, self)
self.back.setPos(length - l, 0)

def paint(self, painter, option, widget=None):
paint_option = option
paint_option.state &= ~QStyle.State_Selected
QGraphicsRectItem.paint(self, painter, paint_option, widget)

def setSelected(self, boolean):
QGraphicsRectItem.setSelected(self, boolean)
if boolean: self.setBrush(self.select_brush)
else: self.setBrush(self.orig_brush)

def hoverEnterEvent(self, event):
self.hovering = True
QGraphicsRectItem.hoverEnterEvent(self, event)
if not self.isSelected():
self.setBrush(self.hover_brush)

def hoverLeaveEvent(self, event):
self.hovering = False
QGraphicsRectItem.hoverLeaveEvent(self, event)
if not self.isSelected():
self.setBrush(self.orig_brush)
elif self.isSelected():
self.setBrush(self.select_brush)

def mousePressEvent(self, event):
QGraphicsRectItem.mousePressEvent(self, event)
self.setSelected(True)
self.pressed = True

def mouseMoveEvent(self, event):
pass

def moveEvent(self, event):
offset = event.scenePos() - event.lastScenePos()

if self.back.stretch:
self.expand(self.back, offset)
else:
self.move_pos = self.scenePos() + offset \
+ QPointF(self.moving_diff[0],self.moving_diff[1])
pos = self.piano().enforce_bounds(self.move_pos)
pos_x, pos_y = pos.x(), pos.y()
pos_sx, pos_sy = self.piano().snap(pos_x, pos_y)
self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy)
if self.front.stretch:
right = self.rect().right() - offset.x() + self.expand_diff
if (self.scenePos().x() == self.piano().piano_width and offset.x() < 0) \
or right < 10:
self.expand_diff = 0
return
self.expand(self.front, offset)
self.setPos(pos_sx, self.scenePos().y())
else:
self.setPos(pos_sx, pos_sy)

def expand(self, rectItem, offset):
rect = self.rect()
right = rect.right() + self.expand_diff
if rectItem == self.back:
right += offset.x()
if right > self.piano().grid_width:
right = self.piano().grid_width
elif right < 10:
right = 10
new_x = self.piano().snap(right)
else:
right -= offset.x()
new_x = self.piano().snap(right+2.75)
if self.piano().snap_value: new_x -= 2.75 # where does this number come from?!
self.expand_diff = right - new_x
self.back.setPos(new_x - 5, 0)
rect.setRight(new_x)
self.setRect(rect)

def updateNoteInfo(self, pos_x, pos_y):
self.note[0] = self.piano().get_note_num_from_y(pos_y)
self.note[1] = self.piano().get_note_start_from_x(pos_x)
self.note[2] = self.piano().get_note_length_from_x(
self.rect().right() - self.rect().left())
#print("note: {}".format(self.note))

def mouseReleaseEvent(self, event):
QGraphicsRectItem.mouseReleaseEvent(self, event)
self.pressed = False
if event.button() == Qt.LeftButton:
self.moving_diff = (0,0)
self.expand_diff = 0
self.back.stretch = False
self.front.stretch = False
(pos_x, pos_y,) = self.piano().snap(self.pos().x(), self.pos().y())
self.setPos(pos_x, pos_y)
self.updateNoteInfo(pos_x, pos_y)

def updateVelocity(self, event):
offset = event.scenePos().x() - event.lastScenePos().x()
self.note[3] += int(offset/5)
if self.note[3] > 127:
self.note[3] = 127
elif self.note[3] < 0:
self.note[3] = 0
print("new vel: {}".format(self.note[3]))
self.orig_brush = QColor(self.note[3], 0, 0)
self.select_brush = QColor(self.note[3] + 100, 100, 100)
self.setBrush(self.orig_brush)


class PianoKeyItem(QGraphicsRectItem):
def __init__(self, width, height, parent):
QGraphicsRectItem.__init__(self, 0, 0, width, height, parent)
self.setPen(QPen(QColor(0,0,0,80)))
self.width = width
self.height = height
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
self.hover_brush = QColor(200, 0, 0)
self.click_brush = QColor(255, 100, 100)
self.pressed = False

def hoverEnterEvent(self, event):
QGraphicsRectItem.hoverEnterEvent(self, event)
self.orig_brush = self.brush()
self.setBrush(self.hover_brush)

def hoverLeaveEvent(self, event):
if self.pressed:
self.pressed = False
self.setBrush(self.hover_brush)
QGraphicsRectItem.hoverLeaveEvent(self, event)
self.setBrush(self.orig_brush)

#def mousePressEvent(self, event):
# self.pressed = True
# self.setBrush(self.click_brush)

def mouseMoveEvent(self, event):
"""this may eventually do something"""
pass

def mouseReleaseEvent(self, event):
self.pressed = False
QGraphicsRectItem.mouseReleaseEvent(self, event)
self.setBrush(self.hover_brush)

class PianoRoll(QGraphicsScene):
'''the piano roll'''

midievent = pyqtSignal(list)
measureupdate = pyqtSignal(int)
modeupdate = pyqtSignal(str)

def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
QGraphicsScene.__init__(self)
self.setBackgroundBrush(QColor(50, 50, 50))
self.mousePos = QPointF()

self.notes = []
self.selected_notes = []
self.piano_keys = []

self.marquee_select = False
self.insert_mode = False
self.velocity_mode = False
self.place_ghost = False
self.ghost_note = None
self.default_ghost_vel = 100
self.ghost_vel = self.default_ghost_vel

## dimensions
self.padding = 2

## piano dimensions
self.note_height = 10
self.start_octave = -2
self.end_octave = 8
self.notes_in_octave = 12
self.total_notes = (self.end_octave - self.start_octave) \
* self.notes_in_octave + 1
self.piano_height = self.note_height * self.total_notes
self.octave_height = self.notes_in_octave * self.note_height

self.piano_width = 34

## height
self.header_height = 20
self.total_height = self.piano_height - self.note_height + self.header_height
#not sure why note_height is subtracted

## width
self.full_note_width = 250 # i.e. a 4/4 note
self.snap_value = None
self.quantize_val = quantize_val

### dummy vars that will be changed
self.time_sig = 0
self.measure_width = 0
self.num_measures = 0
self.max_note_length = 0
self.grid_width = 0
self.value_width = 0
self.grid_div = 0
self.piano = None
self.header = None
self.play_head = None

self.setTimeSig(time_sig)
self.setMeasures(num_measures)
self.setGridDiv()
self.default_length = 1. / self.grid_div


# -------------------------------------------------------------------------
# Callbacks

def movePlayHead(self, transport_info):
# TODO: need conversion between frames and PPQ
x = 105. # works for 120bpm
total_duration = self.time_sig[0] * self.num_measures * x
pos = transport_info['frame'] / x
frac = (pos % total_duration) / total_duration
self.play_head.setPos(QPointF(frac * self.grid_width, 0))

def setTimeSig(self, time_sig):
try:
new_time_sig = list(map(float, time_sig.split('/')))
if len(new_time_sig)==2:
self.time_sig = new_time_sig

self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1]
self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
self.grid_width = self.measure_width * self.num_measures
self.setGridDiv()
except ValueError:
pass

def setMeasures(self, measures):
try:
self.num_measures = float(measures)
self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
self.grid_width = self.measure_width * self.num_measures
self.refreshScene()
except:
pass

def setDefaultLength(self, length):
try:
v = list(map(float, length.split('/')))
if len(v) < 3:
self.default_length = \
v[0] if len(v)==1 else \
v[0] / v[1]
pos = self.enforce_bounds(self.mousePos)
if self.insert_mode: self.makeGhostNote(pos.x(), pos.y())
except ValueError:
pass

def setGridDiv(self, div=None):
if not div: div = self.quantize_val
try:
val = list(map(int, div.split('/')))
if len(val) < 3:
self.quantize_val = div
self.grid_div = val[0] if len(val)==1 else val[1]
self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None
self.setQuantize(div)

self.refreshScene()
except ValueError:
pass

def setQuantize(self, value):
try:
val = list(map(float, value.split('/')))
if len(val) == 1:
self.quantize(val[0])
self.quantize_val = value
elif len(val) == 2:
self.quantize(val[0] / val[1])
self.quantize_val = value
except ValueError:
pass

# -------------------------------------------------------------------------
# Event Callbacks

def keyPressEvent(self, event):
QGraphicsScene.keyPressEvent(self, event)
if event.key() == Qt.Key_F:
if not self.insert_mode:
self.velocity_mode = False
self.insert_mode = True
self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
self.modeupdate.emit('insert_mode')
elif self.insert_mode:
self.insert_mode = False
if self.place_ghost: self.place_ghost = False
self.removeItem(self.ghost_note)
self.ghost_note = None
self.modeupdate.emit('')
elif event.key() == Qt.Key_D:
if self.velocity_mode:
self.velocity_mode = False
self.modeupdate.emit('')
else:
if self.insert_mode:
self.removeItem(self.ghost_note)
self.ghost_note = None
self.insert_mode = False
self.place_ghost = False
self.velocity_mode = True
self.modeupdate.emit('velocity_mode')
elif event.key() == Qt.Key_A:
if all((note.isSelected() for note in self.notes)):
for note in self.notes:
note.setSelected(False)
self.selected_notes = []
else:
for note in self.notes:
note.setSelected(True)
self.selected_notes = self.notes[:]
elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
self.notes = [note for note in self.notes if note not in self.selected_notes]
for note in self.selected_notes:
self.removeItem(note)
self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
del note
self.selected_notes = []

def mousePressEvent(self, event):
QGraphicsScene.mousePressEvent(self, event)
if not (any(key.pressed for key in self.piano_keys)
or any(note.pressed for note in self.notes)):
for note in self.selected_notes:
note.setSelected(False)
self.selected_notes = []

if event.button() == Qt.LeftButton:
if self.insert_mode:
self.place_ghost = True
else:
self.marquee_select = True
self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1)
self.marquee = QGraphicsRectItem(self.marquee_rect)
self.marquee.setBrush(QColor(255, 255, 255, 100))
self.addItem(self.marquee)
else:
for s_note in self.notes:
if s_note.pressed and s_note in self.selected_notes:
break
elif s_note.pressed and s_note not in self.selected_notes:
for note in self.selected_notes:
note.setSelected(False)
self.selected_notes = [s_note]
break
for note in self.selected_notes:
if not self.velocity_mode:
note.mousePressEvent(event)

def mouseMoveEvent(self, event):
QGraphicsScene.mouseMoveEvent(self, event)
self.mousePos = event.scenePos()
if not (any((key.pressed for key in self.piano_keys))):
m_pos = event.scenePos()
if self.insert_mode and self.place_ghost: #placing a note
m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
if m_pos.x() > m_width:
m_new_x = self.snap(m_pos.x())
self.ghost_rect.setRight(m_new_x)
self.ghost_note.setRect(self.ghost_rect)
#self.adjust_note_vel(event)
else:
m_pos = self.enforce_bounds(m_pos)

if self.insert_mode: #ghostnote follows mouse around
(m_new_x, m_new_y) = self.snap(m_pos.x(), m_pos.y())
self.ghost_rect.moveTo(m_new_x, m_new_y)
try:
self.ghost_note.setRect(self.ghost_rect)
except RuntimeError:
self.ghost_note = None
self.makeGhostNote(m_new_x, m_new_y)

elif self.marquee_select:
marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton)
if marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() < m_pos.y():
self.marquee_rect.setBottomRight(m_pos)
elif marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() > m_pos.y():
self.marquee_rect.setTopRight(m_pos)
elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() < m_pos.y():
self.marquee_rect.setBottomLeft(m_pos)
elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() > m_pos.y():
self.marquee_rect.setTopLeft(m_pos)
self.marquee.setRect(self.marquee_rect)
self.selected_notes = []
for item in self.collidingItems(self.marquee):
if item in self.notes:
self.selected_notes.append(item)

for note in self.notes:
if note in self.selected_notes: note.setSelected(True)
else: note.setSelected(False)

elif self.velocity_mode:
if Qt.LeftButton == event.buttons():
for note in self.selected_notes:
note.updateVelocity(event)

elif not self.marquee_select: #move selected
if Qt.LeftButton == event.buttons():
x = y = False
if any(note.back.stretch for note in self.selected_notes):
x = True
elif any(note.front.stretch for note in self.selected_notes):
y = True
for note in self.selected_notes:
note.back.stretch = x
note.front.stretch = y
note.moveEvent(event)

def mouseReleaseEvent(self, event):
if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))):
if event.button() == Qt.LeftButton:
if self.place_ghost and self.insert_mode:
self.place_ghost = False
note_start = self.get_note_start_from_x(self.ghost_rect.x())
note_num = self.get_note_num_from_y(self.ghost_rect.y())
note_length = self.get_note_length_from_x(self.ghost_rect.width())
self.drawNote(note_num, note_start, note_length, self.ghost_vel)
self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel])
self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
elif self.marquee_select:
self.marquee_select = False
self.removeItem(self.marquee)
elif not self.marquee_select:
for note in self.selected_notes:
old_info = note.note[:]
note.mouseReleaseEvent(event)
if self.velocity_mode:
note.setSelected(True)
if not old_info == note.note:
self.midievent.emit(["midievent-remove", old_info[0], old_info[1], old_info[2], old_info[3]])
self.midievent.emit(["midievent-add", note.note[0], note.note[1], note.note[2], note.note[3]])

# -------------------------------------------------------------------------
# Internal Functions

def drawHeader(self):
self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height)
#self.header.setZValue(1.0)
self.header.setPos(self.piano_width, 0)
self.addItem(self.header)

def drawPiano(self):
piano_keys_width = self.piano_width - self.padding
labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C')
black_notes = (2,4,6,9,11)
piano_label = QFont()
piano_label.setPointSize(6)
self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height)
self.piano.setPos(0, self.header_height)
self.addItem(self.piano)

key = PianoKeyItem(piano_keys_width, self.note_height, self.piano)
label = QGraphicsSimpleTextItem('C8', key)
label.setPos(18, 1)
label.setFont(piano_label)
key.setBrush(QColor(255, 255, 255))
for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
for j in range(self.notes_in_octave, 0, -1):
if j in black_notes:
key = PianoKeyItem(piano_keys_width/1.4, self.note_height, self.piano)
key.setBrush(QColor(0, 0, 0))
key.setZValue(1.0)
key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
elif (j - 1) and (j + 1) in black_notes:
key = PianoKeyItem(piano_keys_width, self.note_height * 2, self.piano)
key.setBrush(QColor(255, 255, 255))
key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
elif (j - 1) in black_notes:
key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano)
key.setBrush(QColor(255, 255, 255))
key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
elif (j + 1) in black_notes:
key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano)
key.setBrush(QColor(255, 255, 255))
key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
if j == 12:
label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i), key )
label.setPos(18, 6)
label.setFont(piano_label)
self.piano_keys.append(key)

def drawGrid(self):
black_notes = [2,4,6,9,11]
scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
scale_bar.setPos(self.piano_width, 0)
scale_bar.setBrush(QColor(100,100,100))
clearpen = QPen(QColor(0,0,0,0))
for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
for j in range(self.notes_in_octave, 0, -1):
scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1))
scale_bar.setPen(clearpen)
if j not in black_notes:
scale_bar.setBrush(QColor(120,120,120))
else:
scale_bar.setBrush(QColor(100,100,100))

measure_pen = QPen(QColor(0, 0, 0, 120), 3)
half_measure_pen = QPen(QColor(0, 0, 0, 40), 2)
line_pen = QPen(QColor(0, 0, 0, 40))
for i in range(0, int(self.num_measures) + 1):
measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header)
measure.setPos(self.measure_width * i, 0.5 * measure_pen.width())
measure.setPen(measure_pen)
if i < self.num_measures:
number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
number.setPos(self.measure_width * i + 5, 2)
number.setBrush(Qt.white)
for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.):
line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
line.setZValue(1.0)
line.setPos(self.measure_width * i + self.value_width * j, self.header_height)
if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0:
line.setPen(half_measure_pen)
else:
line.setPen(line_pen)

def drawPlayHead(self):
self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height)
self.play_head.setPen(QPen(QColor(255,255,255,50), 2))
self.play_head.setZValue(1.)
self.addItem(self.play_head)

def refreshScene(self):
list(map(self.removeItem, self.notes))
self.selected_notes = []
self.piano_keys = []
self.clear()
self.drawPiano()
self.drawHeader()
self.drawGrid()
self.drawPlayHead()
for note in self.notes[:]:
if note.note[1] >= (self.num_measures * self.time_sig[0]):
self.notes.remove(note)
self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
elif note.note[2] > self.max_note_length:
new_note = note.note[:]
new_note[2] = self.max_note_length
self.notes.remove(note)
self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False)
self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
list(map(self.addItem, self.notes))
if self.views():
self.views()[0].setSceneRect(self.itemsBoundingRect())

def clearNotes(self):
self.clear()
self.notes = []
self.selected_notes = []
self.drawPiano()
self.drawHeader()
self.drawGrid()

def makeGhostNote(self, pos_x, pos_y):
"""creates the ghostnote that is placed on the scene before the real one is."""
if self.ghost_note:
self.removeItem(self.ghost_note)
length = self.full_note_width * self.default_length
(start, note) = self.snap(pos_x, pos_y)
self.ghost_vel = self.default_ghost_vel
self.ghost_rect = QRectF(start, note, length, self.note_height)
self.ghost_rect_orig_width = self.ghost_rect.width()
self.ghost_note = QGraphicsRectItem(self.ghost_rect)
self.ghost_note.setBrush(QColor(230, 221, 45, 100))
self.addItem(self.ghost_note)

def drawNote(self, note_num, note_start=None, note_length=None, note_velocity=None, add=True):
"""
note_num: midi number, 0 - 127
note_start: 0 - (num_measures * time_sig[0]) so this is in beats
note_length: 0 - (num_measures * time_sig[0]/time_sig[1]) this is in measures
note_velocity: 0 - 127
"""

info = [note_num, note_start, note_length, note_velocity]

if not note_start % (self.num_measures * self.time_sig[0]) == note_start:
#self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity])
while not note_start % (self.num_measures * self.time_sig[0]) == note_start:
self.setMeasures(self.num_measures+1)
self.measureupdate.emit(self.num_measures)
self.refreshScene()

x_start = self.get_note_x_start(note_start)
if note_length > self.max_note_length:
note_length = self.max_note_length + 0.25
x_length = self.get_note_x_length(note_length)
y_pos = self.get_note_y_pos(note_num)

note = NoteItem(self.note_height, x_length, info)
note.setPos(x_start, y_pos)

self.notes.append(note)
if add:
self.addItem(note)

# -------------------------------------------------------------------------
# Helper Functions

def frange(self, x, y, t):
while x < y:
yield x
x += t

def quantize(self, value):
self.snap_value = float(self.full_note_width) * value if value else None

def snap(self, pos_x, pos_y = None):
if self.snap_value:
pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) \
* self.snap_value + self.piano_width
if pos_y:
pos_y = int((pos_y - self.header_height) / self.note_height) \
* self.note_height + self.header_height
return (pos_x, pos_y) if pos_y else pos_x

def adjust_note_vel(self, event):
m_pos = event.scenePos()
#bind velocity to vertical mouse movement
self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10
if self.ghost_vel < 0:
self.ghost_vel = 0
elif self.ghost_vel > 127:
self.ghost_vel = 127

m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
if m_pos.x() < m_width:
m_pos.setX(m_width)
m_new_x = self.snap(m_pos.x())
self.ghost_rect.setRight(m_new_x)
self.ghost_note.setRect(self.ghost_rect)


def enforce_bounds(self, pos):
if pos.x() < self.piano_width:
pos.setX(self.piano_width)
elif pos.x() > self.grid_width + self.piano_width:
pos.setX(self.grid_width + self.piano_width)
if pos.y() < self.header_height + self.padding:
pos.setY(self.header_height + self.padding)
return pos

def get_note_start_from_x(self, note_x):
return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0])


def get_note_x_start(self, note_start):
return self.piano_width + \
(self.grid_width / self.num_measures / self.time_sig[0]) * note_start

def get_note_x_length(self, note_length):
return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures

def get_note_length_from_x(self, note_x):
return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width \
* note_x


def get_note_y_pos(self, note_num):
return self.header_height + self.note_height * (self.total_notes - note_num - 1)

def get_note_num_from_y(self, note_y_pos):
return -(((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)

class PianoRollView(QGraphicsView):
def __init__(self, parent, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
QGraphicsView.__init__(self, parent)
self.piano = PianoRoll(time_sig, num_measures, quantize_val)
self.setScene(self.piano)
#self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

x = 0 * self.sceneRect().width() + self.sceneRect().left()
y = 0.4 * self.sceneRect().height() + self.sceneRect().top()
self.centerOn(x, y)

self.setAlignment(Qt.AlignLeft)
self.o_transform = self.transform()
self.zoom_x = 1
self.zoom_y = 1

def setZoomX(self, scale_x):
self.setTransform(self.o_transform)
self.zoom_x = 1 + scale_x / float(99) * 2
self.scale(self.zoom_x, self.zoom_y)

def setZoomY(self, scale_y):
self.setTransform(self.o_transform)
self.zoom_y = 1 + scale_y / float(99)
self.scale(self.zoom_x, self.zoom_y)

# ------------------------------------------------------------------------------------------------------------

class ModeIndicator(QWidget):
def __init__(self, parent):
QWidget.__init__(self, parent)
#self.setGeometry(0, 0, 30, 20)
self.setFixedSize(30,20)
self.mode = None

def paintEvent(self, event):
painter = QPainter(self)
event.accept()

painter.begin(self)

painter.setPen(QPen(QColor(0, 0, 0, 0)))
if self.mode == 'velocity_mode':
painter.setBrush(QColor(127, 0, 0))
elif self.mode == 'insert_mode':
painter.setBrush(QColor(0, 100, 127))
else:
painter.setBrush(QColor(0, 0, 0, 0))
painter.drawRect(0, 0, 30, 20)

# FIXME
painter.end()

def changeMode(self, new_mode):
self.mode = new_mode
self.update()

# ------------------------------------------------------------------------------------------------------------

Loading…
Cancel
Save