Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

707 lines
29KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # A piano roll viewer/editor
  4. # Copyright (C) 2012-2015 Filipe Coelho <falktx@falktx.com>
  5. # Copyright (C) 2014-2015 tatch
  6. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU General Public License as
  9. # published by the Free Software Foundation; either version 2 of
  10. # the License, or any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # For a full copy of the GNU General Public License see the doc/GPL.txt file.
  18. # ------------------------------------------------------------------------------------------------------------
  19. # Imports (Config)
  20. from carla_config import *
  21. # ------------------------------------------------------------------------------------------------------------
  22. # Imports (Global)
  23. if config_UseQt5:
  24. from PyQt5.QtCore import Qt, QRectF
  25. from PyQt5.QtGui import QColor, QFont, QPen
  26. from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
  27. from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
  28. else:
  29. from PyQt4.QtCore import Qt, QRectF
  30. from PyQt4.QtGui import QColor, QFont, QPen
  31. from PyQt4.QtGui import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
  32. from PyQt4.QtGui import QGraphicsScene, QGraphicsView
  33. # ------------------------------------------------------------------------------------------------------------
  34. # Imports (Custom)
  35. from carla_shared import *
  36. from carla_utils import *
  37. # ------------------------------------------------------------------------------------------------------------
  38. # Imports (ExternalUI)
  39. from carla_app import CarlaApplication
  40. from externalui import ExternalUI
  41. # ------------------------------------------------------------------------------------------------------------
  42. # MIDI definitions, copied from CarlaMIDI.h
  43. MAX_MIDI_CHANNELS = 16
  44. MAX_MIDI_NOTE = 128
  45. MAX_MIDI_VALUE = 128
  46. MAX_MIDI_CONTROL = 120 # 0x77
  47. MIDI_STATUS_BIT = 0xF0
  48. MIDI_CHANNEL_BIT = 0x0F
  49. # MIDI Messages List
  50. MIDI_STATUS_NOTE_OFF = 0x80 # note (0-127), velocity (0-127)
  51. MIDI_STATUS_NOTE_ON = 0x90 # note (0-127), velocity (0-127)
  52. MIDI_STATUS_POLYPHONIC_AFTERTOUCH = 0xA0 # note (0-127), pressure (0-127)
  53. MIDI_STATUS_CONTROL_CHANGE = 0xB0 # see 'Control Change Messages List'
  54. MIDI_STATUS_PROGRAM_CHANGE = 0xC0 # program (0-127), none
  55. MIDI_STATUS_CHANNEL_PRESSURE = 0xD0 # pressure (0-127), none
  56. MIDI_STATUS_PITCH_WHEEL_CONTROL = 0xE0 # LSB (0-127), MSB (0-127)
  57. # MIDI Message type
  58. def MIDI_IS_CHANNEL_MESSAGE(status): return status >= MIDI_STATUS_NOTE_OFF and status < MIDI_STATUS_BIT
  59. def MIDI_IS_SYSTEM_MESSAGE(status): return status >= MIDI_STATUS_BIT and status <= 0xFF
  60. def MIDI_IS_OSC_MESSAGE(status): return status == '/' or status == '#'
  61. # MIDI Channel message type
  62. def MIDI_IS_STATUS_NOTE_OFF(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_OFF
  63. def MIDI_IS_STATUS_NOTE_ON(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_ON
  64. def MIDI_IS_STATUS_POLYPHONIC_AFTERTOUCH(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_POLYPHONIC_AFTERTOUCH
  65. def MIDI_IS_STATUS_CONTROL_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CONTROL_CHANGE
  66. def MIDI_IS_STATUS_PROGRAM_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PROGRAM_CHANGE
  67. def MIDI_IS_STATUS_CHANNEL_PRESSURE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CHANNEL_PRESSURE
  68. def MIDI_IS_STATUS_PITCH_WHEEL_CONTROL(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PITCH_WHEEL_CONTROL
  69. # MIDI Utils
  70. def MIDI_GET_STATUS_FROM_DATA(data): return data[0] & MIDI_STATUS_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else data[0]
  71. def MIDI_GET_CHANNEL_FROM_DATA(data): return data[0] & MIDI_CHANNEL_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else 0
  72. # ------------------------------------------------------------------------------------------------------------
  73. # Global variables and functions
  74. global_piano_roll_snap = True
  75. global_piano_roll_grid_width = 1000.0
  76. global_piano_roll_grid_max_start_time = 999.0
  77. global_piano_roll_note_height = 10
  78. global_piano_roll_snap_value = global_piano_roll_grid_width / 64.0
  79. global_piano_roll_note_count = 120
  80. global_piano_keys_width = 34
  81. global_piano_roll_header_height = 20
  82. global_piano_roll_total_height = 1000 #this gets changed
  83. global_piano_roll_quantize_choices = ['None','1/4','1/3','1/2','1']
  84. def piano_roll_quantize(a_index):
  85. global global_piano_roll_snap_value
  86. global global_piano_roll_snap
  87. if a_index == 0:
  88. global_piano_roll_snap = False
  89. elif a_index == 1:
  90. global_piano_roll_snap_value = global_piano_roll_grid_width / 16.0
  91. global_piano_roll_snap = True
  92. elif a_index == 2:
  93. global_piano_roll_snap_value = global_piano_roll_grid_width / 12.0
  94. global_piano_roll_snap = True
  95. elif a_index == 3:
  96. global_piano_roll_snap_value = global_piano_roll_grid_width / 8.0
  97. global_piano_roll_snap = True
  98. elif a_index == 4:
  99. global_piano_roll_snap_value = global_piano_roll_grid_width / 4.0
  100. global_piano_roll_snap = True
  101. def snap(a_pos_x, a_pos_y = None):
  102. if global_piano_roll_snap:
  103. 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
  104. if a_pos_y:
  105. 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
  106. return (f_pos_x, f_pos_y)
  107. else:
  108. return f_pos_x
  109. # ------------------------------------------------------------------------------------------------------------
  110. # Graphics Items
  111. class note_item(QGraphicsRectItem):
  112. '''a note on the pianoroll sequencer'''
  113. def __init__(self, a_length, a_note_height, a_note_num):
  114. QGraphicsRectItem.__init__(self, 0, 0, a_length, a_note_height)
  115. self.setFlag(QGraphicsItem.ItemIsMovable)
  116. self.setFlag(QGraphicsItem.ItemIsSelectable)
  117. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  118. self.setAcceptHoverEvents(True)
  119. self.o_brush = QColor(100, 0, 0)
  120. self.hover_brush = QColor(200, 200, 100)
  121. self.s_brush = QColor(200, 100, 100)
  122. self.setBrush(self.o_brush)
  123. self.note_height = a_note_height
  124. self.note_num = a_note_num
  125. self.pressed = False
  126. self.selected = False
  127. def select(self):
  128. self.setSelected(True)
  129. self.selected = True
  130. self.setBrush(self.s_brush)
  131. def deselect(self):
  132. self.setSelected(False)
  133. self.selected = False
  134. self.setBrush(self.o_brush)
  135. def hoverEnterEvent(self, a_event):
  136. QGraphicsRectItem.hoverEnterEvent(self, a_event)
  137. if not self.selected:
  138. self.setBrush(self.hover_brush)
  139. def hoverLeaveEvent(self, a_event):
  140. QGraphicsRectItem.hoverLeaveEvent(self, a_event)
  141. if not self.selected:
  142. self.setBrush(self.o_brush)
  143. elif self.selected:
  144. self.setBrush(self.s_brush)
  145. def mousePressEvent(self, a_event):
  146. a_event.setAccepted(True)
  147. QGraphicsRectItem.mousePressEvent(self, a_event)
  148. self.select()
  149. self.pressed = True
  150. def mouseMoveEvent(self, a_event):
  151. QGraphicsRectItem.mouseMoveEvent(self, a_event)
  152. f_pos_x = self.pos().x()
  153. f_pos_y = self.pos().y()
  154. if f_pos_x < global_piano_keys_width:
  155. f_pos_x = global_piano_keys_width
  156. elif f_pos_x > global_piano_roll_grid_max_start_time:
  157. f_pos_x = global_piano_roll_grid_max_start_time
  158. if f_pos_y < global_piano_roll_header_height:
  159. f_pos_y = global_piano_roll_header_height
  160. elif f_pos_y > global_piano_roll_total_height:
  161. f_pos_y = global_piano_roll_total_height
  162. (f_pos_x, f_pos_y,) = snap(f_pos_x, f_pos_y)
  163. self.setPos(f_pos_x, f_pos_y)
  164. def mouseReleaseEvent(self, a_event):
  165. a_event.setAccepted(True)
  166. QGraphicsRectItem.mouseReleaseEvent(self, a_event)
  167. self.pressed = False
  168. if a_event.button() == Qt.LeftButton:
  169. (f_pos_x, f_pos_y,) = snap(self.pos().x(), self.pos().y())
  170. self.setPos(f_pos_x, f_pos_y)
  171. f_new_note_start = (f_pos_x - global_piano_keys_width) * 0.001 * 4.0
  172. f_new_note_num = int(global_piano_roll_note_count - (f_pos_y - global_piano_roll_header_height) / global_piano_roll_note_height)
  173. #print("note start: ", str(f_new_note_start))
  174. print("MIDI number: ", str(f_new_note_num))
  175. class piano_key_item(QGraphicsRectItem):
  176. def __init__(self, width, height, parent):
  177. QGraphicsRectItem.__init__(self, 0, 0, width, height, parent)
  178. self.width = width
  179. self.height = height
  180. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  181. self.setAcceptHoverEvents(True)
  182. self.hover_brush = QColor(200, 0, 0)
  183. self.click_brush = QColor(255, 100, 100)
  184. self.pressed = False
  185. def hoverEnterEvent(self, a_event):
  186. QGraphicsRectItem.hoverEnterEvent(self, a_event)
  187. self.o_brush = self.brush()
  188. self.setBrush(self.hover_brush)
  189. def hoverLeaveEvent(self, a_event):
  190. if self.pressed:
  191. self.pressed = False
  192. self.setBrush(self.hover_brush)
  193. QGraphicsRectItem.hoverLeaveEvent(self, a_event)
  194. self.setBrush(self.o_brush)
  195. def mousePressEvent(self, a_event):
  196. self.pressed = True
  197. self.setBrush(self.click_brush)
  198. def mouseMoveEvent(self, a_event):
  199. """this may eventually do something"""
  200. pass
  201. def mouseReleaseEvent(self, a_event):
  202. self.pressed = False
  203. QGraphicsRectItem.mouseReleaseEvent(self, a_event)
  204. self.setBrush(self.hover_brush)
  205. # ------------------------------------------------------------------------------------------------------------
  206. # External UI
  207. class piano_roll(ExternalUI, QGraphicsView):
  208. '''the piano roll'''
  209. def __init__(self, a_item_length = 4, a_grid_div = 4):
  210. ExternalUI.__init__(self)
  211. QGraphicsView.__init__(self)
  212. global global_piano_roll_total_height
  213. self.item_length = float(a_item_length)
  214. self.viewer_width = 1000
  215. self.grid_div = a_grid_div
  216. self.end_octave = 8
  217. self.start_octave = -2
  218. self.notes_in_octave = 12
  219. self.total_notes = (self.end_octave - self.start_octave) * self.notes_in_octave + 1
  220. self.note_height = global_piano_roll_note_height
  221. self.octave_height = self.notes_in_octave * self.note_height
  222. self.header_height = global_piano_roll_header_height
  223. self.piano_height = self.note_height * self.total_notes
  224. self.padding = 2
  225. self.piano_width = global_piano_keys_width - self.padding
  226. self.piano_height = self.note_height * self.total_notes
  227. global_piano_roll_total_height = self.piano_height - self.note_height + global_piano_roll_header_height
  228. self.scene = QGraphicsScene(self)
  229. self.scene.mousePressEvent = self.sceneMousePressEvent
  230. self.scene.mouseReleaseEvent = self.sceneMouseReleaseEvent
  231. self.scene.mouseMoveEvent = self.sceneMouseMoveEvent
  232. self.scene.setBackgroundBrush(QColor(100, 100, 100))
  233. self.setScene(self.scene)
  234. self.setAlignment(Qt.AlignLeft)
  235. self.notes = []
  236. self.selected_notes = []
  237. self.piano_keys = []
  238. self.ghost_vel = 100
  239. self.show_ghost = True
  240. self.marquee_select = False
  241. self.insert_mode = False
  242. self.place_ghost = False
  243. # to be filled with note-on events, while waiting for their matching note-off
  244. self.fPendingNoteOns = [] # (channel, note, velocity, time)
  245. # transport info
  246. self.fTransportInfo = {
  247. "playing": False,
  248. "frame": 0,
  249. "bar": 0,
  250. "beat": 0,
  251. "tick": 0,
  252. "bpm": 120.0,
  253. "sigNum": 4.0,
  254. "sigDenom": 4.0
  255. }
  256. self.fIdleTimer = self.startTimer(30)
  257. self.draw_piano()
  258. self.draw_header()
  259. self.draw_grid()
  260. self.setWindowTitle(self.fUiName)
  261. self.ready()
  262. # -------------------------------------------------------------------
  263. # DSP Callbacks
  264. def dspParameterChanged(self, index, value):
  265. pass
  266. def dspStateChanged(self, key, value):
  267. pass
  268. # -------------------------------------------------------------------
  269. # ExternalUI Callbacks
  270. def uiShow(self):
  271. self.show()
  272. def uiFocus(self):
  273. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  274. self.show()
  275. self.raise_()
  276. self.activateWindow()
  277. def uiHide(self):
  278. self.hide()
  279. def uiQuit(self):
  280. self.closeExternalUI()
  281. self.close()
  282. app.quit()
  283. def uiTitleChanged(self, uiTitle):
  284. self.setWindowTitle(uiTitle)
  285. # -------------------------------------------------------------------
  286. # Qt events
  287. def timerEvent(self, event):
  288. if event.timerId() == self.fIdleTimer:
  289. self.idleExternalUI()
  290. QGraphicsView.timerEvent(self, event)
  291. def closeEvent(self, event):
  292. self.closeExternalUI()
  293. QGraphicsView.closeEvent(self, event)
  294. # -------------------------------------------------------------------
  295. # Custom callback
  296. def msgCallback(self, msg):
  297. #try:
  298. self.msgCallback2(msg)
  299. #except:
  300. #print("Custom msgCallback error, skipped for", msg)
  301. def msgCallback2(self, msg):
  302. msg = charPtrToString(msg)
  303. if msg == "midi-clear-all":
  304. # clear all notes
  305. self.clear_drawn_items()
  306. elif msg == "midievent-add":
  307. # adds single midi event
  308. time = int(self.readlineblock())
  309. size = int(self.readlineblock())
  310. data = []
  311. for x in range(size):
  312. data.append(int(self.readlineblock()))
  313. self.handleMidiEvent(time, size, data)
  314. elif msg == "transport":
  315. playing = bool(self.readlineblock() == "true")
  316. frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
  317. bpm, sigNum, sigDenom = [float(i) for i in self.readlineblock().split(":")]
  318. self.fTransportInfo = {
  319. "playing": playing,
  320. "frame": frame,
  321. "bar": bar,
  322. "beat": beat,
  323. "tick": tick,
  324. "bpm": bpm,
  325. "sigNum": sigNum,
  326. "sigDenom": sigDenom
  327. }
  328. elif msg == "show":
  329. self.uiShow()
  330. elif msg == "focus":
  331. self.uiFocus()
  332. elif msg == "hide":
  333. self.uiHide()
  334. elif msg == "quit":
  335. self.fQuitReceived = True
  336. self.uiQuit()
  337. elif msg == "uiTitle":
  338. uiTitle = self.readlineblock()
  339. self.uiTitleChanged(uiTitle)
  340. else:
  341. print("unknown message: \"" + msg + "\"")
  342. # -------------------------------------------------------------------
  343. # Internal stuff
  344. def handleMidiEvent(self, time, size, data):
  345. print("Got MIDI Event on UI", time, size, data)
  346. # NOTE: for now time comes in frames, which might not be desirable
  347. # we'll convert it to a smaller value for now (seconds)
  348. # later on we can have time as PPQ or similar
  349. time /= self.getSampleRate()
  350. status = MIDI_GET_STATUS_FROM_DATA(data)
  351. channel = MIDI_GET_CHANNEL_FROM_DATA(data)
  352. if status == MIDI_STATUS_NOTE_ON:
  353. note = data[1]
  354. velo = data[2]
  355. # append (channel, note, velo, time) for later
  356. self.fPendingNoteOns.append((channel, note, velo, time))
  357. elif status == MIDI_STATUS_NOTE_OFF:
  358. note = data[1]
  359. velo = data[2]
  360. # find previous note-on that matches this note and channel
  361. for noteOnMsg in self.fPendingNoteOns:
  362. channel_, note_, velo_, time_ = noteOnMsg
  363. if channel_ != channel:
  364. continue
  365. if note_ != note:
  366. continue
  367. # found it
  368. self.draw_note(note, time_, time - time_, velo_)
  369. # remove from list
  370. self.fPendingNoteOns.remove(noteOnMsg)
  371. break
  372. def make_ghostnote(self, pos_x, pos_y):
  373. """creates the ghostnote that is placed on the scene before the real one is."""
  374. f_length = self.beat_width / 4
  375. (f_start, f_note,) = snap(pos_x, pos_y)
  376. self.ghost_rect_orig = QRectF(f_start, f_note, f_length, self.note_height)
  377. self.ghost_rect = QRectF(self.ghost_rect_orig)
  378. self.ghost_note = QGraphicsRectItem(self.ghost_rect)
  379. self.ghost_note.setBrush(QColor(230, 221, 45, 100))
  380. self.scene.addItem(self.ghost_note)
  381. def keyPressEvent(self, a_event):
  382. QGraphicsView.keyPressEvent(self, a_event)
  383. if a_event.key() == Qt.Key_B:
  384. if not self.insert_mode:
  385. self.insert_mode = True
  386. if self.show_ghost:
  387. self.make_ghostnote(self.piano_width, self.header_height)
  388. elif self.insert_mode:
  389. self.insert_mode = False
  390. if self.place_ghost:
  391. self.place_ghost = False
  392. self.scene.removeItem(self.ghost_note)
  393. elif self.show_ghost:
  394. self.scene.removeItem(self.ghost_note)
  395. if a_event.key() == Qt.Key_Delete or a_event.key() == Qt.Key_Backspace:
  396. for note in self.selected_notes:
  397. note.deselect()
  398. self.scene.removeItem(note)
  399. def sceneMousePressEvent(self, a_event):
  400. QGraphicsScene.mousePressEvent(self.scene, a_event)
  401. if not (any(key.pressed for key in self.piano_keys) or any(note.pressed for note in self.notes)):
  402. #if not self.scene.mouseGrabberItem():
  403. for note in self.selected_notes:
  404. note.deselect()
  405. self.selected_notes = []
  406. self.f_pos = a_event.scenePos()
  407. if a_event.button() == Qt.LeftButton:
  408. if self.insert_mode:
  409. self.place_ghost = True
  410. else:
  411. self.marquee_select = True
  412. self.marquee_rect = QRectF(self.f_pos.x(), self.f_pos.y(), 1, 1)
  413. self.marquee = QGraphicsRectItem(self.marquee_rect)
  414. self.marquee.setBrush(QColor(255, 255, 255, 100))
  415. self.scene.addItem(self.marquee)
  416. else:
  417. out = False
  418. for note in self.notes:
  419. if note.pressed and note in self.selected_notes:
  420. break
  421. elif note.pressed and note not in self.selected_notes:
  422. s_note = note
  423. out = True
  424. break
  425. if out == True:
  426. for note in self.selected_notes:
  427. note.deselect()
  428. self.selected_notes = [s_note]
  429. elif out == False:
  430. for note in self.selected_notes:
  431. note.mousePressEvent(a_event)
  432. def sceneMouseMoveEvent(self, a_event):
  433. QGraphicsScene.mouseMoveEvent(self.scene, a_event)
  434. #if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))):
  435. if not (any((key.pressed for key in self.piano_keys))):
  436. #if not self.scene.mouseGrabberItem():
  437. m_pos = a_event.scenePos()
  438. if self.insert_mode and self.place_ghost: #placing a note
  439. #bind velocity to vertical mouse movement
  440. self.ghost_vel = self.ghost_vel + (a_event.lastScenePos().y() - m_pos.y())/10
  441. if self.ghost_vel < 0:
  442. self.ghost_vel = 0
  443. elif self.ghost_vel > 127:
  444. self.ghost_vel = 127
  445. #print "velocity:", self.ghost_vel
  446. m_width = self.ghost_rect.x() + self.ghost_rect_orig.width()
  447. if m_pos.x() < m_width:
  448. m_pos.setX(m_width)
  449. m_new_x = snap(m_pos.x())
  450. self.ghost_rect.setRight(m_new_x)
  451. self.ghost_note.setRect(self.ghost_rect)
  452. else:
  453. if m_pos.x() < self.piano_width:
  454. m_pos.setX(self.piano_width)
  455. elif m_pos.x() > self.viewer_width:
  456. m_pos.setX(self.viewer_width)
  457. if m_pos.y() < self.header_height:
  458. m_pos.setY(self.header_height)
  459. if self.insert_mode and self.show_ghost: #ghostnote follows mouse around
  460. (m_new_x, m_new_y,) = snap(m_pos.x(), m_pos.y())
  461. self.ghost_rect.moveTo(m_new_x, m_new_y)
  462. self.ghost_note.setRect(self.ghost_rect)
  463. elif self.marquee_select:
  464. if self.f_pos.x() < m_pos.x() and self.f_pos.y() < m_pos.y():
  465. self.marquee_rect.setBottomRight(m_pos)
  466. elif self.f_pos.x() < m_pos.x() and self.f_pos.y() > m_pos.y():
  467. self.marquee_rect.setTopRight(m_pos)
  468. elif self.f_pos.x() > m_pos.x() and self.f_pos.y() < m_pos.y():
  469. self.marquee_rect.setBottomLeft(m_pos)
  470. elif self.f_pos.x() > m_pos.x() and self.f_pos.y() > m_pos.y():
  471. self.marquee_rect.setTopLeft(m_pos)
  472. self.marquee.setRect(self.marquee_rect)
  473. self.selected_notes = []
  474. for item in self.scene.collidingItems(self.marquee):
  475. if item in self.notes:
  476. self.selected_notes.append(item)
  477. for note in self.notes:
  478. if note in self.selected_notes:
  479. note.select()
  480. else:
  481. note.deselect()
  482. elif not self.marquee_select:
  483. for note in self.selected_notes:
  484. note.mouseMoveEvent(a_event)
  485. def sceneMouseReleaseEvent(self, a_event):
  486. QGraphicsScene.mouseReleaseEvent(self.scene, a_event)
  487. if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))):
  488. #if not self.scene.mouseGrabberItem():
  489. if a_event.button() == Qt.LeftButton:
  490. if self.place_ghost and self.insert_mode:
  491. self.place_ghost = False
  492. f_final_scenePos = a_event.scenePos().x()
  493. f_final_scenePos = snap(f_final_scenePos)
  494. f_delta = f_final_scenePos - self.ghost_rect.x()
  495. if f_delta < self.beat_width / 8:
  496. f_delta = self.beat_width / 4
  497. f_length = f_delta / self.viewer_width * 4
  498. f_start = (self.ghost_rect.x() - self.piano_width - self.padding) / self.beat_width
  499. f_note = self.total_notes - (self.ghost_rect.y() - self.header_height) / self.note_height - 1
  500. self.draw_note(f_note, f_start, f_length, self.ghost_vel)
  501. self.ghost_rect = QRectF(self.ghost_rect_orig)
  502. self.ghost_note.setRect(self.ghost_rect)
  503. elif self.marquee_select:
  504. self.marquee_select = False
  505. self.scene.removeItem(self.marquee)
  506. elif not self.marquee_select:
  507. for n in self.selected_notes:
  508. n.mouseReleaseEvent(a_event)
  509. def draw_header(self):
  510. self.header = QGraphicsRectItem(0, 0, self.viewer_width, self.header_height)
  511. #self.header.setZValue(1.0)
  512. self.header.setPos(self.piano_width + self.padding, 0)
  513. self.scene.addItem(self.header)
  514. self.beat_width = self.viewer_width / self.item_length
  515. self.value_width = self.beat_width / self.grid_div
  516. def draw_piano(self):
  517. f_labels = ['B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C']
  518. f_black_notes = [2,4,6,9,11]
  519. f_piano_label = QFont()
  520. f_piano_label.setPointSize(8)
  521. self.piano = QGraphicsRectItem(0, 0, self.piano_width, self.piano_height)
  522. self.piano.setPos(0, self.header_height)
  523. self.scene.addItem(self.piano)
  524. f_key = piano_key_item(self.piano_width, self.note_height, self.piano)
  525. f_label = QGraphicsSimpleTextItem('C8', f_key)
  526. f_label.setPos(4, 0)
  527. f_label.setFont(f_piano_label)
  528. f_key.setBrush(QColor(255, 255, 255))
  529. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  530. for j in range(self.notes_in_octave, 0, -1):
  531. f_key = piano_key_item(self.piano_width, self.note_height, self.piano)
  532. f_key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  533. if j == 12:
  534. f_label = QGraphicsSimpleTextItem('%s%d' % (f_labels[(j - 1)], self.end_octave - i), f_key)
  535. f_label.setPos(4, 0)
  536. f_label.setFont(f_piano_label)
  537. if j in f_black_notes:
  538. f_key.setBrush(QColor(0, 0, 0))
  539. else:
  540. f_key.setBrush(QColor(255, 255, 255))
  541. self.piano_keys.append(f_key)
  542. def draw_grid(self):
  543. f_black_notes = [2,4,6,9,11]
  544. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  545. for j in range(self.notes_in_octave, 0, -1):
  546. f_scale_bar = QGraphicsRectItem(0, 0, self.viewer_width, self.note_height, self.piano)
  547. f_scale_bar.setPos(self.piano_width + self.padding, self.note_height * j + self.octave_height * (i - 1))
  548. if j not in f_black_notes:
  549. f_scale_bar.setBrush(QColor(230, 230, 230, 100))
  550. f_beat_pen = QPen()
  551. f_beat_pen.setWidth(2)
  552. f_line_pen = QPen()
  553. f_line_pen.setColor(QColor(0, 0, 0, 40))
  554. for i in range(0, int(self.item_length) + 1):
  555. f_beat = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - f_beat_pen.width(), self.header)
  556. f_beat.setPos(self.beat_width * i, 0.5 * f_beat_pen.width())
  557. f_beat.setPen(f_beat_pen)
  558. if i < self.item_length:
  559. f_number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
  560. f_number.setPos(self.beat_width * i + 5, 2)
  561. f_number.setBrush(Qt.white)
  562. for j in range(0, self.grid_div):
  563. f_line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
  564. f_line.setZValue(1.0)
  565. if float(j) == self.grid_div / 2.0:
  566. f_line.setLine(0, 0, 0, self.piano_height)
  567. f_line.setPos(self.beat_width * i + self.value_width * j, self.header_height)
  568. else:
  569. f_line.setPos(self.beat_width * i + self.value_width * j, self.header_height)
  570. f_line.setPen(f_line_pen)
  571. def set_zoom(self, scale_x, scale_y):
  572. self.scale(scale_x, scale_y)
  573. def clear_drawn_items(self):
  574. self.scene.clear()
  575. self.draw_header()
  576. self.draw_piano()
  577. self.draw_grid()
  578. def draw_item(self, a_item):
  579. """ Draw all notes in an instance of the item class"""
  580. for f_notes in a_item.notes:
  581. self.draw_note(f_note)
  582. def draw_note(self, a_note, a_start=None, a_length=None, a_velocity=None):
  583. """a_note is the midi number, a_start is 1-4.99, a_length is in beats, a_velocity is 0-127"""
  584. f_start = self.piano_width + self.padding + self.beat_width * a_start
  585. f_length = self.beat_width * (a_length * 0.25 * self.item_length)
  586. f_note = self.header_height + self.note_height * (self.total_notes - a_note - 1)
  587. f_note_item = note_item(f_length, self.note_height, a_note)
  588. f_note_item.setPos(f_start, f_note)
  589. f_vel_opacity = QGraphicsOpacityEffect()
  590. f_vel_opacity.setOpacity(a_velocity * 0.007874016 * 0.6 + 0.3)
  591. f_note_item.setGraphicsEffect(f_vel_opacity)
  592. self.scene.addItem(f_note_item)
  593. self.notes.append(f_note_item)
  594. #--------------- main ------------------
  595. if __name__ == '__main__':
  596. import resources_rc
  597. pathBinaries, pathResources = getPaths()
  598. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
  599. gCarla.utils.set_process_name("MidiSequencer")
  600. app = CarlaApplication("MidiSequencer")
  601. gui = piano_roll()
  602. app.exit_exec()