#!/usr/bin/env python3 # -*- coding: utf-8 -*- # A piano roll viewer/editor # Copyright (C) 2012-2015 Filipe Coelho # 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 pyqtSlot, Qt, QEvent from PyQt5.QtGui import QKeyEvent from PyQt5.QtWidgets import QMainWindow else: from PyQt4.QtCore import pyqtSlot, Qt, QEvent from PyQt4.QtGui import QKeyEvent, QMainWindow # ------------------------------------------------------------------------------------------------------------ # Imports (Custom) from carla_shared import * from carla_utils import * from pianoroll import * import ui_midipattern # ------------------------------------------------------------------------------------------------------------ # Imports (ExternalUI) from carla_app import CarlaApplication from externalui import ExternalUI # ------------------------------------------------------------------------------------------------------------ class MidiPatternW(ExternalUI, QMainWindow): PPQ = 48.0 def __init__(self): ExternalUI.__init__(self) QMainWindow.__init__(self) self.ui = ui_midipattern.Ui_MidiPatternW() self.ui.setupUi(self) self.ui.piano = self.ui.graphicsView.piano # to be filled with note-on events, while waiting for their matching note-off self.fPendingNoteOns = [] # (channel, note, velocity, time) self.fTransportInfo = { "playing": False, "frame": 0, "bar": 0, "beat": 0, "tick": 0, "bpm": 120.0, "sigNum": 4.0, "sigDenom": 4.0 } self.ui.act_edit_insert.triggered.connect(self.slot_editInsertMode) self.ui.act_edit_velocity.triggered.connect(self.slot_editVelocityMode) self.ui.act_edit_select_all.triggered.connect(self.slot_editSelectAll) self.ui.piano.midievent.connect(self.sendMsg) self.ui.piano.measureupdate.connect(self.updateMeasureBox) self.ui.piano.modeupdate.connect(self.ui.modeIndicator.changeMode) self.ui.piano.modeupdate.connect(self.slot_modeChanged) self.ui.timeSigBox.currentIndexChanged[int].connect(self.slot_paramChanged) self.ui.measureBox.currentIndexChanged[int].connect(self.slot_paramChanged) self.ui.defaultLengthBox.currentIndexChanged[int].connect(self.slot_paramChanged) self.ui.quantizeBox.currentIndexChanged[int].connect(self.slot_paramChanged) self.ui.timeSigBox.currentIndexChanged[str].connect(self.ui.piano.setTimeSig) self.ui.measureBox.currentIndexChanged[str].connect(self.ui.piano.setMeasures) self.ui.defaultLengthBox.currentIndexChanged[str].connect(self.ui.piano.setDefaultLength) self.ui.quantizeBox.currentIndexChanged[str].connect(self.ui.piano.setGridDiv) self.ui.hSlider.valueChanged.connect(self.ui.graphicsView.setZoomX) self.ui.vSlider.valueChanged.connect(self.ui.graphicsView.setZoomY) self.ui.graphicsView.setFocus() self.fIdleTimer = self.startTimer(30) self.setWindowTitle(self.fUiName) self.ready() def slot_editInsertMode(self): ev = QKeyEvent(QEvent.User, Qt.Key_F, Qt.NoModifier) self.ui.piano.keyPressEvent(ev) def slot_editVelocityMode(self): ev = QKeyEvent(QEvent.User, Qt.Key_D, Qt.NoModifier) self.ui.piano.keyPressEvent(ev) def slot_editSelectAll(self): ev = QKeyEvent(QEvent.User, Qt.Key_A, Qt.NoModifier) self.ui.piano.keyPressEvent(ev) def slot_modeChanged(self, mode): if mode == "insert_mode": self.ui.act_edit_insert.setChecked(True) self.ui.act_edit_velocity.setChecked(False) elif mode == "velocity_mode": self.ui.act_edit_insert.setChecked(False) self.ui.act_edit_velocity.setChecked(True) else: self.ui.act_edit_insert.setChecked(False) self.ui.act_edit_velocity.setChecked(False) def slot_paramChanged(self, index): sender = self.sender() if sender == self.ui.timeSigBox: param = 0 elif sender == self.ui.measureBox: param = 1 index += 1 elif sender == self.ui.defaultLengthBox: param = 2 elif sender == self.ui.quantizeBox: param = 3 else: return self.sendControl(param, index) # ------------------------------------------------------------------- # DSP Callbacks def dspParameterChanged(self, index, value): value = int(value) if index == 0: # TimeSig self.ui.timeSigBox.setCurrentIndex(value) elif index == 1: # Measures self.ui.measureBox.setCurrentIndex(value-1) elif index == 2: # DefLength self.ui.defaultLengthBox.setCurrentIndex(value) elif index == 3: # Quantize self.ui.quantizeBox.setCurrentIndex(value) 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() QMainWindow.timerEvent(self, event) def closeEvent(self, event): self.closeExternalUI() QMainWindow.closeEvent(self, event) # there might be other qt windows open which will block the UI from quitting app.quit() # ------------------------------------------------------------------- # Custom callback def updateMeasureBox(self, index): self.measureBox.setCurrentIndex(index-1) def sendMsg(self, data): msg = data[0] if msg == "midievent-remove": note, start, length, vel = data[1:5] note_start = start * 60. / self.fTransportInfo["bpm"] * 4. / self.fTransportInfo["sigDenom"] * self.PPQ note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel]) self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel]) elif msg == "midievent-add": note, start, length, vel = data[1:5] note_start = start * 60. / self.fTransportInfo["bpm"] * self.PPQ note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel]) self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel]) def msgCallback(self, msg): msg = charPtrToString(msg) if msg == "midi-clear-all": # clear all notes self.ui.piano.clearNotes() 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 == "transport": playing = bool(self.readlineblock() == "true") frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")] bpm, sigNum, sigDenom = [float(i) for i in self.readlineblock().split(":")] if beat != self.fTransportInfo["beat"]: print(beat) old_frame = self.fTransportInfo['frame'] self.fTransportInfo = { "playing": playing, "frame": frame, "bar": bar, "beat": beat, "tick": tick, "bpm": bpm, "sigNum": sigNum, "sigDenom": sigDenom } if old_frame != frame: self.ui.piano.movePlayHead(self.fTransportInfo) else: ExternalUI.msgCallback(self, 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.PPQ 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 #print("{} {} {} {}\n".format(note, time_, time-time_, velo_)) start = time_ / 60. * self.fTransportInfo["bpm"] / 4. * self.fTransportInfo["sigDenom"] length = (time - time_) / 60. * self.fTransportInfo["bpm"] / 4. / self.fTransportInfo["sigNum"] * self.fTransportInfo["sigDenom"] self.ui.piano.drawNote(note, start, length, velo_) # remove from list self.fPendingNoteOns.remove(noteOnMsg) break #--------------- 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("MidiPattern") app = CarlaApplication("MidiPattern") gui = MidiPatternW() app.exit_exec()