|
|
@@ -0,0 +1,675 @@ |
|
|
|
#!/usr/bin/env python3 |
|
|
|
# -*- coding: utf-8 -*- |
|
|
|
|
|
|
|
# A piano roll viewer/editor |
|
|
|
# Copyright (C) 2015 Filipe Coelho <falktx@falktx.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 doc/GPL.txt file. |
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------------ |
|
|
|
# Imports (Config) |
|
|
|
|
|
|
|
from carla_config import * |
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------------ |
|
|
|
# Imports (Global) |
|
|
|
|
|
|
|
if config_UseQt5: |
|
|
|
from PyQt5.QtCore import Qt, QRectF |
|
|
|
from PyQt5.QtGui import QColor, QFont, QPen |
|
|
|
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem |
|
|
|
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView |
|
|
|
else: |
|
|
|
from PyQt4.QtCore import Qt, QRectF |
|
|
|
from PyQt4.QtGui import QColor, QFont, QPen |
|
|
|
from PyQt4.QtGui import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem |
|
|
|
from PyQt4.QtGui import QGraphicsScene, QGraphicsView |
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------------ |
|
|
|
# Imports (Custom) |
|
|
|
|
|
|
|
from carla_shared import * |
|
|
|
from carla_utils import * |
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------------ |
|
|
|
# Imports (ExternalUI) |
|
|
|
|
|
|
|
from carla_app import CarlaApplication |
|
|
|
from externalui import ExternalUI |
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------------ |
|
|
|
# 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 |
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------------ |
|
|
|
# Global variables and functions |
|
|
|
|
|
|
|
global_piano_roll_snap = True |
|
|
|
global_piano_roll_grid_width = 1000.0 |
|
|
|
global_piano_roll_grid_max_start_time = 999.0 |
|
|
|
global_piano_roll_note_height = 10 |
|
|
|
global_piano_roll_snap_value = global_piano_roll_grid_width / 64.0 |
|
|
|
global_piano_roll_note_count = 120 |
|
|
|
global_piano_keys_width = 34 |
|
|
|
global_piano_roll_header_height = 20 |
|
|
|
global_piano_roll_total_height = 1000 #this gets changed |
|
|
|
|
|
|
|
global_piano_roll_quantize_choices = ['None','1/4','1/3','1/2','1'] |
|
|
|
|
|
|
|
def piano_roll_quantize(a_index): |
|
|
|
global global_piano_roll_snap_value |
|
|
|
global global_piano_roll_snap |
|
|
|
if a_index == 0: |
|
|
|
global_piano_roll_snap = False |
|
|
|
elif a_index == 1: |
|
|
|
global_piano_roll_snap_value = global_piano_roll_grid_width / 16.0 |
|
|
|
global_piano_roll_snap = True |
|
|
|
elif a_index == 2: |
|
|
|
global_piano_roll_snap_value = global_piano_roll_grid_width / 12.0 |
|
|
|
global_piano_roll_snap = True |
|
|
|
elif a_index == 3: |
|
|
|
global_piano_roll_snap_value = global_piano_roll_grid_width / 8.0 |
|
|
|
global_piano_roll_snap = True |
|
|
|
elif a_index == 4: |
|
|
|
global_piano_roll_snap_value = global_piano_roll_grid_width / 4.0 |
|
|
|
global_piano_roll_snap = True |
|
|
|
|
|
|
|
def snap(a_pos_x, a_pos_y = None): |
|
|
|
if global_piano_roll_snap: |
|
|
|
f_pos_x = int((a_pos_x - global_piano_keys_width) / global_piano_roll_snap_value) * global_piano_roll_snap_value + global_piano_keys_width |
|
|
|
if a_pos_y: |
|
|
|
f_pos_y = int((a_pos_y - global_piano_roll_header_height) / global_piano_roll_note_height) * global_piano_roll_note_height + global_piano_roll_header_height |
|
|
|
return (f_pos_x, f_pos_y) |
|
|
|
else: |
|
|
|
return f_pos_x |
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------------ |
|
|
|
# Graphics Items |
|
|
|
|
|
|
|
class note_item(QGraphicsRectItem): |
|
|
|
'''a note on the pianoroll sequencer''' |
|
|
|
def __init__(self, a_length, a_note_height, a_note_num): |
|
|
|
QGraphicsRectItem.__init__(self, 0, 0, a_length, a_note_height) |
|
|
|
self.setFlag(QGraphicsItem.ItemIsMovable) |
|
|
|
self.setFlag(QGraphicsItem.ItemIsSelectable) |
|
|
|
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) |
|
|
|
self.setAcceptHoverEvents(True) |
|
|
|
self.o_brush = QColor(100, 0, 0) |
|
|
|
self.hover_brush = QColor(200, 200, 100) |
|
|
|
self.s_brush = QColor(200, 100, 100) |
|
|
|
self.setBrush(self.o_brush) |
|
|
|
self.note_height = a_note_height |
|
|
|
self.note_num = a_note_num |
|
|
|
self.pressed = False |
|
|
|
self.selected = False |
|
|
|
|
|
|
|
def select(self): |
|
|
|
self.setSelected(True) |
|
|
|
self.selected = True |
|
|
|
self.setBrush(self.s_brush) |
|
|
|
|
|
|
|
def deselect(self): |
|
|
|
self.setSelected(False) |
|
|
|
self.selected = False |
|
|
|
self.setBrush(self.o_brush) |
|
|
|
|
|
|
|
def hoverEnterEvent(self, a_event): |
|
|
|
QGraphicsRectItem.hoverEnterEvent(self, a_event) |
|
|
|
if not self.selected: |
|
|
|
self.setBrush(self.hover_brush) |
|
|
|
|
|
|
|
def hoverLeaveEvent(self, a_event): |
|
|
|
QGraphicsRectItem.hoverLeaveEvent(self, a_event) |
|
|
|
if not self.selected: |
|
|
|
self.setBrush(self.o_brush) |
|
|
|
elif self.selected: |
|
|
|
self.setBrush(self.s_brush) |
|
|
|
|
|
|
|
def mousePressEvent(self, a_event): |
|
|
|
a_event.setAccepted(True) |
|
|
|
QGraphicsRectItem.mousePressEvent(self, a_event) |
|
|
|
self.select() |
|
|
|
self.pressed = True |
|
|
|
|
|
|
|
def mouseMoveEvent(self, a_event): |
|
|
|
QGraphicsRectItem.mouseMoveEvent(self, a_event) |
|
|
|
f_pos_x = self.pos().x() |
|
|
|
f_pos_y = self.pos().y() |
|
|
|
if f_pos_x < global_piano_keys_width: |
|
|
|
f_pos_x = global_piano_keys_width |
|
|
|
elif f_pos_x > global_piano_roll_grid_max_start_time: |
|
|
|
f_pos_x = global_piano_roll_grid_max_start_time |
|
|
|
if f_pos_y < global_piano_roll_header_height: |
|
|
|
f_pos_y = global_piano_roll_header_height |
|
|
|
elif f_pos_y > global_piano_roll_total_height: |
|
|
|
f_pos_y = global_piano_roll_total_height |
|
|
|
(f_pos_x, f_pos_y,) = snap(f_pos_x, f_pos_y) |
|
|
|
self.setPos(f_pos_x, f_pos_y) |
|
|
|
|
|
|
|
def mouseReleaseEvent(self, a_event): |
|
|
|
a_event.setAccepted(True) |
|
|
|
QGraphicsRectItem.mouseReleaseEvent(self, a_event) |
|
|
|
self.pressed = False |
|
|
|
if a_event.button() == Qt.LeftButton: |
|
|
|
(f_pos_x, f_pos_y,) = snap(self.pos().x(), self.pos().y()) |
|
|
|
self.setPos(f_pos_x, f_pos_y) |
|
|
|
f_new_note_start = (f_pos_x - global_piano_keys_width) * 0.001 * 4.0 |
|
|
|
f_new_note_num = int(global_piano_roll_note_count - (f_pos_y - global_piano_roll_header_height) / global_piano_roll_note_height) |
|
|
|
#print("note start: ", str(f_new_note_start)) |
|
|
|
print("MIDI number: ", str(f_new_note_num)) |
|
|
|
|
|
|
|
|
|
|
|
class piano_key_item(QGraphicsRectItem): |
|
|
|
def __init__(self, width, height, parent): |
|
|
|
QGraphicsRectItem.__init__(self, 0, 0, width, height, parent) |
|
|
|
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, a_event): |
|
|
|
QGraphicsRectItem.hoverEnterEvent(self, a_event) |
|
|
|
self.o_brush = self.brush() |
|
|
|
self.setBrush(self.hover_brush) |
|
|
|
|
|
|
|
def hoverLeaveEvent(self, a_event): |
|
|
|
if self.pressed: |
|
|
|
self.pressed = False |
|
|
|
self.setBrush(self.hover_brush) |
|
|
|
QGraphicsRectItem.hoverLeaveEvent(self, a_event) |
|
|
|
self.setBrush(self.o_brush) |
|
|
|
|
|
|
|
def mousePressEvent(self, a_event): |
|
|
|
self.pressed = True |
|
|
|
self.setBrush(self.click_brush) |
|
|
|
|
|
|
|
def mouseMoveEvent(self, a_event): |
|
|
|
"""this may eventually do something""" |
|
|
|
pass |
|
|
|
|
|
|
|
def mouseReleaseEvent(self, a_event): |
|
|
|
self.pressed = False |
|
|
|
QGraphicsRectItem.mouseReleaseEvent(self, a_event) |
|
|
|
self.setBrush(self.hover_brush) |
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------------ |
|
|
|
# External UI |
|
|
|
|
|
|
|
class piano_roll(ExternalUI, QGraphicsView): |
|
|
|
'''the piano roll''' |
|
|
|
def __init__(self, a_item_length = 4, a_grid_div = 4): |
|
|
|
ExternalUI.__init__(self) |
|
|
|
QGraphicsView.__init__(self) |
|
|
|
|
|
|
|
global global_piano_roll_total_height |
|
|
|
self.item_length = float(a_item_length) |
|
|
|
self.viewer_width = 1000 |
|
|
|
self.grid_div = a_grid_div |
|
|
|
self.end_octave = 8 |
|
|
|
self.start_octave = -2 |
|
|
|
self.notes_in_octave = 12 |
|
|
|
self.total_notes = (self.end_octave - self.start_octave) * self.notes_in_octave + 1 |
|
|
|
self.note_height = global_piano_roll_note_height |
|
|
|
self.octave_height = self.notes_in_octave * self.note_height |
|
|
|
self.header_height = global_piano_roll_header_height |
|
|
|
self.piano_height = self.note_height * self.total_notes |
|
|
|
self.padding = 2 |
|
|
|
self.piano_width = global_piano_keys_width - self.padding |
|
|
|
self.piano_height = self.note_height * self.total_notes |
|
|
|
global_piano_roll_total_height = self.piano_height - self.note_height + global_piano_roll_header_height |
|
|
|
self.scene = QGraphicsScene(self) |
|
|
|
self.scene.mousePressEvent = self.sceneMousePressEvent |
|
|
|
self.scene.mouseReleaseEvent = self.sceneMouseReleaseEvent |
|
|
|
self.scene.mouseMoveEvent = self.sceneMouseMoveEvent |
|
|
|
self.scene.setBackgroundBrush(QColor(100, 100, 100)) |
|
|
|
self.setScene(self.scene) |
|
|
|
self.setAlignment(Qt.AlignLeft) |
|
|
|
self.notes = [] |
|
|
|
self.selected_notes = [] |
|
|
|
self.piano_keys = [] |
|
|
|
self.ghost_vel = 100 |
|
|
|
self.show_ghost = True |
|
|
|
self.marquee_select = False |
|
|
|
self.insert_mode = False |
|
|
|
self.place_ghost = False |
|
|
|
self.draw_piano() |
|
|
|
self.draw_header() |
|
|
|
self.draw_grid() |
|
|
|
|
|
|
|
# to be filled with note-on events, while waiting for their matching note-off |
|
|
|
self.fPendingNoteOns = [] # (channel, note, velocity, time) |
|
|
|
|
|
|
|
self.fIdleTimer = self.startTimer(30) |
|
|
|
self.setWindowTitle(self.fUiName) |
|
|
|
self.ready() |
|
|
|
|
|
|
|
# ------------------------------------------------------------------- |
|
|
|
# DSP Callbacks |
|
|
|
|
|
|
|
def dspParameterChanged(self, index, value): |
|
|
|
pass |
|
|
|
|
|
|
|
def dspStateChanged(self, key, value): |
|
|
|
pass |
|
|
|
|
|
|
|
# ------------------------------------------------------------------- |
|
|
|
# ExternalUI Callbacks |
|
|
|
|
|
|
|
def uiShow(self): |
|
|
|
self.show() |
|
|
|
|
|
|
|
def uiFocus(self): |
|
|
|
self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive) |
|
|
|
self.show() |
|
|
|
|
|
|
|
self.raise_() |
|
|
|
self.activateWindow() |
|
|
|
|
|
|
|
def uiHide(self): |
|
|
|
self.hide() |
|
|
|
|
|
|
|
def uiQuit(self): |
|
|
|
self.closeExternalUI() |
|
|
|
self.close() |
|
|
|
app.quit() |
|
|
|
|
|
|
|
def uiTitleChanged(self, uiTitle): |
|
|
|
self.setWindowTitle(uiTitle) |
|
|
|
|
|
|
|
# ------------------------------------------------------------------- |
|
|
|
# Qt events |
|
|
|
|
|
|
|
def timerEvent(self, event): |
|
|
|
if event.timerId() == self.fIdleTimer: |
|
|
|
self.idleExternalUI() |
|
|
|
QGraphicsView.timerEvent(self, event) |
|
|
|
|
|
|
|
def closeEvent(self, event): |
|
|
|
self.closeExternalUI() |
|
|
|
QGraphicsView.closeEvent(self, event) |
|
|
|
|
|
|
|
# ------------------------------------------------------------------- |
|
|
|
# Custom callback |
|
|
|
|
|
|
|
def msgCallback(self, msg): |
|
|
|
#try: |
|
|
|
self.msgCallback2(msg) |
|
|
|
#except: |
|
|
|
#print("Custom msgCallback error, skipped for", msg) |
|
|
|
|
|
|
|
def msgCallback2(self, msg): |
|
|
|
msg = charPtrToString(msg) |
|
|
|
|
|
|
|
if msg == "midi-clear-all": |
|
|
|
# clear all notes |
|
|
|
self.clear_drawn_items() |
|
|
|
|
|
|
|
elif msg == "midievent-add": |
|
|
|
# adds single midi event |
|
|
|
time = int(self.readlineblock()) |
|
|
|
size = int(self.readlineblock()) |
|
|
|
data = [] |
|
|
|
|
|
|
|
for x in range(size): |
|
|
|
data.append(int(self.readlineblock())) |
|
|
|
|
|
|
|
self.handleMidiEvent(time, size, data) |
|
|
|
|
|
|
|
elif msg == "show": |
|
|
|
self.uiShow() |
|
|
|
|
|
|
|
elif msg == "focus": |
|
|
|
self.uiFocus() |
|
|
|
|
|
|
|
elif msg == "hide": |
|
|
|
self.uiHide() |
|
|
|
|
|
|
|
elif msg == "quit": |
|
|
|
self.fQuitReceived = True |
|
|
|
self.uiQuit() |
|
|
|
|
|
|
|
elif msg == "uiTitle": |
|
|
|
uiTitle = self.readlineblock() |
|
|
|
self.uiTitleChanged(uiTitle) |
|
|
|
|
|
|
|
else: |
|
|
|
print("unknown message: \"" + msg + "\"") |
|
|
|
|
|
|
|
# ------------------------------------------------------------------- |
|
|
|
# Internal stuff |
|
|
|
|
|
|
|
def handleMidiEvent(self, time, size, data): |
|
|
|
print("Got MIDI Event on UI", time, size, data) |
|
|
|
|
|
|
|
# NOTE: for now time comes in frames, which might not be desirable |
|
|
|
# we'll convert it to a smaller value for now (seconds) |
|
|
|
# later on we can have time as PPQ or similar |
|
|
|
time /= self.getSampleRate() |
|
|
|
|
|
|
|
status = MIDI_GET_STATUS_FROM_DATA(data) |
|
|
|
channel = MIDI_GET_CHANNEL_FROM_DATA(data) |
|
|
|
|
|
|
|
if status == MIDI_STATUS_NOTE_ON: |
|
|
|
note = data[1] |
|
|
|
velo = data[2] |
|
|
|
|
|
|
|
# append (channel, note, velo, time) for later |
|
|
|
self.fPendingNoteOns.append((channel, note, velo, time)) |
|
|
|
|
|
|
|
elif status == MIDI_STATUS_NOTE_OFF: |
|
|
|
note = data[1] |
|
|
|
velo = data[2] |
|
|
|
|
|
|
|
# find previous note-on that matches this note and channel |
|
|
|
for noteOnMsg in self.fPendingNoteOns: |
|
|
|
channel_, note_, velo_, time_ = noteOnMsg |
|
|
|
|
|
|
|
if channel_ != channel: |
|
|
|
continue |
|
|
|
if note_ != note: |
|
|
|
continue |
|
|
|
|
|
|
|
# found it |
|
|
|
self.draw_note(note, time_, time - time_, velo_) |
|
|
|
|
|
|
|
# remove from list |
|
|
|
self.fPendingNoteOns.remove(noteOnMsg) |
|
|
|
break |
|
|
|
|
|
|
|
def make_ghostnote(self, pos_x, pos_y): |
|
|
|
"""creates the ghostnote that is placed on the scene before the real one is.""" |
|
|
|
f_length = self.beat_width / 4 |
|
|
|
(f_start, f_note,) = snap(pos_x, pos_y) |
|
|
|
self.ghost_rect_orig = QRectF(f_start, f_note, f_length, self.note_height) |
|
|
|
self.ghost_rect = QRectF(self.ghost_rect_orig) |
|
|
|
self.ghost_note = QGraphicsRectItem(self.ghost_rect) |
|
|
|
self.ghost_note.setBrush(QColor(230, 221, 45, 100)) |
|
|
|
self.scene.addItem(self.ghost_note) |
|
|
|
|
|
|
|
def keyPressEvent(self, a_event): |
|
|
|
QGraphicsView.keyPressEvent(self, a_event) |
|
|
|
if a_event.key() == Qt.Key_B: |
|
|
|
if not self.insert_mode: |
|
|
|
self.insert_mode = True |
|
|
|
if self.show_ghost: |
|
|
|
self.make_ghostnote(self.piano_width, self.header_height) |
|
|
|
elif self.insert_mode: |
|
|
|
self.insert_mode = False |
|
|
|
if self.place_ghost: |
|
|
|
self.place_ghost = False |
|
|
|
self.scene.removeItem(self.ghost_note) |
|
|
|
elif self.show_ghost: |
|
|
|
self.scene.removeItem(self.ghost_note) |
|
|
|
if a_event.key() == Qt.Key_Delete or a_event.key() == Qt.Key_Backspace: |
|
|
|
for note in self.selected_notes: |
|
|
|
note.deselect() |
|
|
|
self.scene.removeItem(note) |
|
|
|
|
|
|
|
def sceneMousePressEvent(self, a_event): |
|
|
|
QGraphicsScene.mousePressEvent(self.scene, a_event) |
|
|
|
if not (any(key.pressed for key in self.piano_keys) or any(note.pressed for note in self.notes)): |
|
|
|
#if not self.scene.mouseGrabberItem(): |
|
|
|
for note in self.selected_notes: |
|
|
|
note.deselect() |
|
|
|
|
|
|
|
self.selected_notes = [] |
|
|
|
self.f_pos = a_event.scenePos() |
|
|
|
if a_event.button() == Qt.LeftButton: |
|
|
|
if self.insert_mode: |
|
|
|
self.place_ghost = True |
|
|
|
else: |
|
|
|
self.marquee_select = True |
|
|
|
self.marquee_rect = QRectF(self.f_pos.x(), self.f_pos.y(), 1, 1) |
|
|
|
self.marquee = QGraphicsRectItem(self.marquee_rect) |
|
|
|
self.marquee.setBrush(QColor(255, 255, 255, 100)) |
|
|
|
self.scene.addItem(self.marquee) |
|
|
|
else: |
|
|
|
out = False |
|
|
|
for note in self.notes: |
|
|
|
if note.pressed and note in self.selected_notes: |
|
|
|
break |
|
|
|
elif note.pressed and note not in self.selected_notes: |
|
|
|
s_note = note |
|
|
|
out = True |
|
|
|
break |
|
|
|
if out == True: |
|
|
|
for note in self.selected_notes: |
|
|
|
note.deselect() |
|
|
|
self.selected_notes = [s_note] |
|
|
|
elif out == False: |
|
|
|
for note in self.selected_notes: |
|
|
|
note.mousePressEvent(a_event) |
|
|
|
|
|
|
|
def sceneMouseMoveEvent(self, a_event): |
|
|
|
QGraphicsScene.mouseMoveEvent(self.scene, a_event) |
|
|
|
#if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))): |
|
|
|
if not (any((key.pressed for key in self.piano_keys))): |
|
|
|
#if not self.scene.mouseGrabberItem(): |
|
|
|
m_pos = a_event.scenePos() |
|
|
|
if self.insert_mode and self.place_ghost: #placing a note |
|
|
|
#bind velocity to vertical mouse movement |
|
|
|
self.ghost_vel = self.ghost_vel + (a_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 |
|
|
|
#print "velocity:", self.ghost_vel |
|
|
|
|
|
|
|
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 = snap(m_pos.x()) |
|
|
|
self.ghost_rect.setRight(m_new_x) |
|
|
|
self.ghost_note.setRect(self.ghost_rect) |
|
|
|
else: |
|
|
|
if m_pos.x() < self.piano_width: |
|
|
|
m_pos.setX(self.piano_width) |
|
|
|
elif m_pos.x() > self.viewer_width: |
|
|
|
m_pos.setX(self.viewer_width) |
|
|
|
if m_pos.y() < self.header_height: |
|
|
|
m_pos.setY(self.header_height) |
|
|
|
|
|
|
|
if self.insert_mode and self.show_ghost: #ghostnote follows mouse around |
|
|
|
(m_new_x, m_new_y,) = snap(m_pos.x(), m_pos.y()) |
|
|
|
self.ghost_rect.moveTo(m_new_x, m_new_y) |
|
|
|
self.ghost_note.setRect(self.ghost_rect) |
|
|
|
elif self.marquee_select: |
|
|
|
if self.f_pos.x() < m_pos.x() and self.f_pos.y() < m_pos.y(): |
|
|
|
self.marquee_rect.setBottomRight(m_pos) |
|
|
|
elif self.f_pos.x() < m_pos.x() and self.f_pos.y() > m_pos.y(): |
|
|
|
self.marquee_rect.setTopRight(m_pos) |
|
|
|
elif self.f_pos.x() > m_pos.x() and self.f_pos.y() < m_pos.y(): |
|
|
|
self.marquee_rect.setBottomLeft(m_pos) |
|
|
|
elif self.f_pos.x() > m_pos.x() and self.f_pos.y() > m_pos.y(): |
|
|
|
self.marquee_rect.setTopLeft(m_pos) |
|
|
|
self.marquee.setRect(self.marquee_rect) |
|
|
|
self.selected_notes = [] |
|
|
|
for item in self.scene.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.select() |
|
|
|
else: |
|
|
|
note.deselect() |
|
|
|
|
|
|
|
elif not self.marquee_select: |
|
|
|
for note in self.selected_notes: |
|
|
|
note.mouseMoveEvent(a_event) |
|
|
|
|
|
|
|
def sceneMouseReleaseEvent(self, a_event): |
|
|
|
QGraphicsScene.mouseReleaseEvent(self.scene, a_event) |
|
|
|
if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))): |
|
|
|
#if not self.scene.mouseGrabberItem(): |
|
|
|
if a_event.button() == Qt.LeftButton: |
|
|
|
if self.place_ghost and self.insert_mode: |
|
|
|
self.place_ghost = False |
|
|
|
f_final_scenePos = a_event.scenePos().x() |
|
|
|
f_final_scenePos = snap(f_final_scenePos) |
|
|
|
f_delta = f_final_scenePos - self.ghost_rect.x() |
|
|
|
if f_delta < self.beat_width / 8: |
|
|
|
f_delta = self.beat_width / 4 |
|
|
|
f_length = f_delta / self.viewer_width * 4 |
|
|
|
f_start = (self.ghost_rect.x() - self.piano_width - self.padding) / self.beat_width |
|
|
|
f_note = self.total_notes - (self.ghost_rect.y() - self.header_height) / self.note_height - 1 |
|
|
|
self.draw_note(f_note, f_start, f_length, self.ghost_vel) |
|
|
|
self.ghost_rect = QRectF(self.ghost_rect_orig) |
|
|
|
self.ghost_note.setRect(self.ghost_rect) |
|
|
|
elif self.marquee_select: |
|
|
|
self.marquee_select = False |
|
|
|
self.scene.removeItem(self.marquee) |
|
|
|
elif not self.marquee_select: |
|
|
|
for n in self.selected_notes: |
|
|
|
n.mouseReleaseEvent(a_event) |
|
|
|
|
|
|
|
def draw_header(self): |
|
|
|
self.header = QGraphicsRectItem(0, 0, self.viewer_width, self.header_height) |
|
|
|
#self.header.setZValue(1.0) |
|
|
|
self.header.setPos(self.piano_width + self.padding, 0) |
|
|
|
self.scene.addItem(self.header) |
|
|
|
self.beat_width = self.viewer_width / self.item_length |
|
|
|
self.value_width = self.beat_width / self.grid_div |
|
|
|
|
|
|
|
def draw_piano(self): |
|
|
|
f_labels = ['B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C'] |
|
|
|
f_black_notes = [2,4,6,9,11] |
|
|
|
f_piano_label = QFont() |
|
|
|
f_piano_label.setPointSize(8) |
|
|
|
self.piano = QGraphicsRectItem(0, 0, self.piano_width, self.piano_height) |
|
|
|
self.piano.setPos(0, self.header_height) |
|
|
|
self.scene.addItem(self.piano) |
|
|
|
f_key = piano_key_item(self.piano_width, self.note_height, self.piano) |
|
|
|
f_label = QGraphicsSimpleTextItem('C8', f_key) |
|
|
|
f_label.setPos(4, 0) |
|
|
|
f_label.setFont(f_piano_label) |
|
|
|
f_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): |
|
|
|
f_key = piano_key_item(self.piano_width, self.note_height, self.piano) |
|
|
|
f_key.setPos(0, self.note_height * j + self.octave_height * (i - 1)) |
|
|
|
if j == 12: |
|
|
|
f_label = QGraphicsSimpleTextItem('%s%d' % (f_labels[(j - 1)], self.end_octave - i), f_key) |
|
|
|
f_label.setPos(4, 0) |
|
|
|
f_label.setFont(f_piano_label) |
|
|
|
if j in f_black_notes: |
|
|
|
f_key.setBrush(QColor(0, 0, 0)) |
|
|
|
else: |
|
|
|
f_key.setBrush(QColor(255, 255, 255)) |
|
|
|
self.piano_keys.append(f_key) |
|
|
|
|
|
|
|
def draw_grid(self): |
|
|
|
f_black_notes = [2,4,6,9,11] |
|
|
|
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): |
|
|
|
f_scale_bar = QGraphicsRectItem(0, 0, self.viewer_width, self.note_height, self.piano) |
|
|
|
f_scale_bar.setPos(self.piano_width + self.padding, self.note_height * j + self.octave_height * (i - 1)) |
|
|
|
if j not in f_black_notes: |
|
|
|
f_scale_bar.setBrush(QColor(230, 230, 230, 100)) |
|
|
|
f_beat_pen = QPen() |
|
|
|
f_beat_pen.setWidth(2) |
|
|
|
f_line_pen = QPen() |
|
|
|
f_line_pen.setColor(QColor(0, 0, 0, 40)) |
|
|
|
for i in range(0, int(self.item_length) + 1): |
|
|
|
f_beat = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - f_beat_pen.width(), self.header) |
|
|
|
f_beat.setPos(self.beat_width * i, 0.5 * f_beat_pen.width()) |
|
|
|
f_beat.setPen(f_beat_pen) |
|
|
|
if i < self.item_length: |
|
|
|
f_number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header) |
|
|
|
f_number.setPos(self.beat_width * i + 5, 2) |
|
|
|
f_number.setBrush(Qt.white) |
|
|
|
for j in range(0, self.grid_div): |
|
|
|
f_line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header) |
|
|
|
f_line.setZValue(1.0) |
|
|
|
if float(j) == self.grid_div / 2.0: |
|
|
|
f_line.setLine(0, 0, 0, self.piano_height) |
|
|
|
f_line.setPos(self.beat_width * i + self.value_width * j, self.header_height) |
|
|
|
else: |
|
|
|
f_line.setPos(self.beat_width * i + self.value_width * j, self.header_height) |
|
|
|
f_line.setPen(f_line_pen) |
|
|
|
|
|
|
|
def set_zoom(self, scale_x, scale_y): |
|
|
|
self.scale(scale_x, scale_y) |
|
|
|
|
|
|
|
def clear_drawn_items(self): |
|
|
|
self.scene.clear() |
|
|
|
self.draw_header() |
|
|
|
self.draw_piano() |
|
|
|
self.draw_grid() |
|
|
|
|
|
|
|
def draw_item(self, a_item): |
|
|
|
""" Draw all notes in an instance of the item class""" |
|
|
|
for f_notes in a_item.notes: |
|
|
|
self.draw_note(f_note) |
|
|
|
|
|
|
|
def draw_note(self, a_note, a_start=None, a_length=None, a_velocity=None): |
|
|
|
"""a_note is the midi number, a_start is 1-4.99, a_length is in beats, a_velocity is 0-127""" |
|
|
|
f_start = self.piano_width + self.padding + self.beat_width * a_start |
|
|
|
f_length = self.beat_width * (a_length * 0.25 * self.item_length) |
|
|
|
f_note = self.header_height + self.note_height * (self.total_notes - a_note - 1) |
|
|
|
f_note_item = note_item(f_length, self.note_height, a_note) |
|
|
|
f_note_item.setPos(f_start, f_note) |
|
|
|
f_vel_opacity = QGraphicsOpacityEffect() |
|
|
|
f_vel_opacity.setOpacity(a_velocity * 0.007874016 * 0.6 + 0.3) |
|
|
|
f_note_item.setGraphicsEffect(f_vel_opacity) |
|
|
|
self.scene.addItem(f_note_item) |
|
|
|
self.notes.append(f_note_item) |
|
|
|
|
|
|
|
#--------------- main ------------------ |
|
|
|
if __name__ == '__main__': |
|
|
|
import resources_rc |
|
|
|
|
|
|
|
pathBinaries, pathResources = getPaths() |
|
|
|
gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION)) |
|
|
|
gCarla.utils.set_process_name("MidiSequencer") |
|
|
|
|
|
|
|
app = CarlaApplication("MidiSequencer") |
|
|
|
gui = piano_roll() |
|
|
|
app.exit_exec() |