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.

midiseq-ui 48KB

9 years ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204
  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 Perry Nguyen
  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. config_UseQt5 = False
  24. if config_UseQt5:
  25. from PyQt5.QtCore import Qt, QRectF, QPointF, pyqtSignal
  26. from PyQt5.QtGui import QColor, QFont, QPen, QPainter
  27. from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
  28. from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
  29. from PyQt4.QtWidgets import QWidget, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QStyle
  30. else:
  31. from PyQt4.QtCore import Qt, QRectF, QPointF, pyqtSignal
  32. from PyQt4.QtGui import QColor, QFont, QPen, QPainter
  33. from PyQt4.QtGui import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
  34. from PyQt4.QtGui import QGraphicsScene, QGraphicsView
  35. from PyQt4.QtGui import QWidget, QLabel, QComboBox, QSlider, QHBoxLayout, QVBoxLayout, QStyle
  36. # ------------------------------------------------------------------------------------------------------------
  37. # Imports (Custom)
  38. from carla_shared import *
  39. from carla_utils import *
  40. # ------------------------------------------------------------------------------------------------------------
  41. # Imports (ExternalUI)
  42. from carla_app import CarlaApplication
  43. from externalui import ExternalUI
  44. # ------------------------------------------------------------------------------------------------------------
  45. # MIDI definitions, copied from CarlaMIDI.h
  46. MAX_MIDI_CHANNELS = 16
  47. MAX_MIDI_NOTE = 128
  48. MAX_MIDI_VALUE = 128
  49. MAX_MIDI_CONTROL = 120 # 0x77
  50. MIDI_STATUS_BIT = 0xF0
  51. MIDI_CHANNEL_BIT = 0x0F
  52. # MIDI Messages List
  53. MIDI_STATUS_NOTE_OFF = 0x80 # note (0-127), velocity (0-127)
  54. MIDI_STATUS_NOTE_ON = 0x90 # note (0-127), velocity (0-127)
  55. MIDI_STATUS_POLYPHONIC_AFTERTOUCH = 0xA0 # note (0-127), pressure (0-127)
  56. MIDI_STATUS_CONTROL_CHANGE = 0xB0 # see 'Control Change Messages List'
  57. MIDI_STATUS_PROGRAM_CHANGE = 0xC0 # program (0-127), none
  58. MIDI_STATUS_CHANNEL_PRESSURE = 0xD0 # pressure (0-127), none
  59. MIDI_STATUS_PITCH_WHEEL_CONTROL = 0xE0 # LSB (0-127), MSB (0-127)
  60. # MIDI Message type
  61. def MIDI_IS_CHANNEL_MESSAGE(status): return status >= MIDI_STATUS_NOTE_OFF and status < MIDI_STATUS_BIT
  62. def MIDI_IS_SYSTEM_MESSAGE(status): return status >= MIDI_STATUS_BIT and status <= 0xFF
  63. def MIDI_IS_OSC_MESSAGE(status): return status == '/' or status == '#'
  64. # MIDI Channel message type
  65. def MIDI_IS_STATUS_NOTE_OFF(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_OFF
  66. def MIDI_IS_STATUS_NOTE_ON(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_ON
  67. def MIDI_IS_STATUS_POLYPHONIC_AFTERTOUCH(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_POLYPHONIC_AFTERTOUCH
  68. def MIDI_IS_STATUS_CONTROL_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CONTROL_CHANGE
  69. def MIDI_IS_STATUS_PROGRAM_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PROGRAM_CHANGE
  70. def MIDI_IS_STATUS_CHANNEL_PRESSURE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CHANNEL_PRESSURE
  71. def MIDI_IS_STATUS_PITCH_WHEEL_CONTROL(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PITCH_WHEEL_CONTROL
  72. # MIDI Utils
  73. def MIDI_GET_STATUS_FROM_DATA(data): return data[0] & MIDI_STATUS_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else data[0]
  74. def MIDI_GET_CHANNEL_FROM_DATA(data): return data[0] & MIDI_CHANNEL_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else 0
  75. # ------------------------------------------------------------------------------------------------------------
  76. # Graphics Items
  77. class NoteExpander(QGraphicsRectItem):
  78. def __init__(self, length, height, parent):
  79. QGraphicsRectItem.__init__(self, 0, 0, length, height, parent)
  80. self.parent = parent
  81. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  82. self.setAcceptHoverEvents(True)
  83. clearpen = QPen(QColor(0,0,0,0))
  84. self.setPen(clearpen)
  85. self.orig_brush = QColor(0, 0, 0, 0)
  86. self.hover_brush = QColor(200, 200, 200)
  87. self.stretch = False
  88. def mousePressEvent(self, event):
  89. QGraphicsRectItem.mousePressEvent(self, event)
  90. self.stretch = True
  91. def hoverEnterEvent(self, event):
  92. QGraphicsRectItem.hoverEnterEvent(self, event)
  93. if self.parent.isSelected():
  94. self.parent.setBrush(self.parent.select_brush)
  95. else:
  96. self.parent.setBrush(self.parent.orig_brush)
  97. self.setBrush(self.hover_brush)
  98. def hoverLeaveEvent(self, event):
  99. QGraphicsRectItem.hoverLeaveEvent(self, event)
  100. if self.parent.isSelected():
  101. self.parent.setBrush(self.parent.select_brush)
  102. elif self.parent.hovering:
  103. self.parent.setBrush(self.parent.hover_brush)
  104. else:
  105. self.parent.setBrush(self.parent.orig_brush)
  106. self.setBrush(self.orig_brush)
  107. class NoteItem(QGraphicsRectItem):
  108. '''a note on the pianoroll sequencer'''
  109. def __init__(self, height, length, note_info):
  110. QGraphicsRectItem.__init__(self, 0, 0, length, height)
  111. self.setFlag(QGraphicsItem.ItemIsMovable)
  112. self.setFlag(QGraphicsItem.ItemIsSelectable)
  113. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  114. self.setAcceptHoverEvents(True)
  115. clearpen = QPen(QColor(0,0,0,0))
  116. self.setPen(clearpen)
  117. self.orig_brush = QColor(note_info[3], 0, 0)
  118. self.hover_brush = QColor(note_info[3] + 100, 200, 100)
  119. self.select_brush = QColor(note_info[3] + 100, 100, 100)
  120. self.setBrush(self.orig_brush)
  121. self.note = note_info
  122. self.length = length
  123. self.piano = self.scene
  124. self.pressed = False
  125. self.hovering = False
  126. self.moving_diff = (0,0)
  127. self.expand_diff = 0
  128. l = 5
  129. self.front = NoteExpander(l, height, self)
  130. self.back = NoteExpander(l, height, self)
  131. self.back.setPos(length - l, 0)
  132. def paint(self, painter, option, widget=None):
  133. paint_option = option
  134. paint_option.state &= ~QStyle.State_Selected
  135. QGraphicsRectItem.paint(self, painter, paint_option, widget)
  136. def setSelected(self, boolean):
  137. QGraphicsRectItem.setSelected(self, boolean)
  138. if boolean: self.setBrush(self.select_brush)
  139. else: self.setBrush(self.orig_brush)
  140. def hoverEnterEvent(self, event):
  141. self.hovering = True
  142. QGraphicsRectItem.hoverEnterEvent(self, event)
  143. if not self.isSelected():
  144. self.setBrush(self.hover_brush)
  145. def hoverLeaveEvent(self, event):
  146. self.hovering = False
  147. QGraphicsRectItem.hoverLeaveEvent(self, event)
  148. if not self.isSelected():
  149. self.setBrush(self.orig_brush)
  150. elif self.isSelected():
  151. self.setBrush(self.select_brush)
  152. def mousePressEvent(self, event):
  153. QGraphicsRectItem.mousePressEvent(self, event)
  154. self.setSelected(True)
  155. self.pressed = True
  156. def mouseMoveEvent(self, event):
  157. pass
  158. def moveEvent(self, event):
  159. offset = event.scenePos() - event.lastScenePos()
  160. if self.back.stretch:
  161. self.expand(self.back, offset)
  162. else:
  163. self.move_pos = self.scenePos() + offset \
  164. + QPointF(self.moving_diff[0],self.moving_diff[1])
  165. pos = self.piano().enforce_bounds(self.move_pos)
  166. pos_x, pos_y = pos.x(), pos.y()
  167. pos_sx, pos_sy = self.piano().snap(pos_x, pos_y)
  168. self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy)
  169. if self.front.stretch:
  170. right = self.rect().right() - offset.x() + self.expand_diff
  171. if (self.scenePos().x() == self.piano().piano_width and offset.x() < 0) \
  172. or right < 10:
  173. self.expand_diff = 0
  174. return
  175. self.expand(self.front, offset)
  176. self.setPos(pos_sx, self.scenePos().y())
  177. else:
  178. self.setPos(pos_sx, pos_sy)
  179. def expand(self, rectItem, offset):
  180. rect = self.rect()
  181. right = rect.right() + self.expand_diff
  182. if rectItem == self.back:
  183. right += offset.x()
  184. if right > self.piano().grid_width:
  185. right = self.piano().grid_width
  186. elif right < 10:
  187. right = 10
  188. new_x = self.piano().snap(right)
  189. else:
  190. right -= offset.x()
  191. new_x = self.piano().snap(right+2.75)
  192. if self.piano().snap_value: new_x -= 2.75 # where does this number come from?!
  193. self.expand_diff = right - new_x
  194. self.back.setPos(new_x - 5, 0)
  195. rect.setRight(new_x)
  196. self.setRect(rect)
  197. def updateNoteInfo(self, pos_x, pos_y):
  198. self.note[0] = self.piano().get_note_num_from_y(pos_y)
  199. self.note[1] = self.piano().get_note_start_from_x(pos_x)
  200. self.note[2] = self.piano().get_note_length_from_x(
  201. self.rect().right() - self.rect().left())
  202. #print("note: {}".format(self.note))
  203. def mouseReleaseEvent(self, event):
  204. QGraphicsRectItem.mouseReleaseEvent(self, event)
  205. self.pressed = False
  206. if event.button() == Qt.LeftButton:
  207. self.moving_diff = (0,0)
  208. self.expand_diff = 0
  209. self.back.stretch = False
  210. self.front.stretch = False
  211. (pos_x, pos_y,) = self.piano().snap(self.pos().x(), self.pos().y())
  212. self.setPos(pos_x, pos_y)
  213. self.updateNoteInfo(pos_x, pos_y)
  214. def updateVelocity(self, event):
  215. offset = event.scenePos().x() - event.lastScenePos().x()
  216. self.note[3] += int(offset/5)
  217. if self.note[3] > 127:
  218. self.note[3] = 127
  219. elif self.note[3] < 0:
  220. self.note[3] = 0
  221. print("new vel: {}".format(self.note[3]))
  222. self.orig_brush = QColor(self.note[3], 0, 0)
  223. self.select_brush = QColor(self.note[3] + 100, 100, 100)
  224. self.setBrush(self.orig_brush)
  225. class PianoKeyItem(QGraphicsRectItem):
  226. def __init__(self, width, height, parent):
  227. QGraphicsRectItem.__init__(self, 0, 0, width, height, parent)
  228. self.setPen(QPen(QColor(0,0,0,80)))
  229. self.width = width
  230. self.height = height
  231. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  232. self.setAcceptHoverEvents(True)
  233. self.hover_brush = QColor(200, 0, 0)
  234. self.click_brush = QColor(255, 100, 100)
  235. self.pressed = False
  236. def hoverEnterEvent(self, event):
  237. QGraphicsRectItem.hoverEnterEvent(self, event)
  238. self.orig_brush = self.brush()
  239. self.setBrush(self.hover_brush)
  240. def hoverLeaveEvent(self, event):
  241. if self.pressed:
  242. self.pressed = False
  243. self.setBrush(self.hover_brush)
  244. QGraphicsRectItem.hoverLeaveEvent(self, event)
  245. self.setBrush(self.orig_brush)
  246. #def mousePressEvent(self, event):
  247. # self.pressed = True
  248. # self.setBrush(self.click_brush)
  249. def mouseMoveEvent(self, event):
  250. """this may eventually do something"""
  251. pass
  252. def mouseReleaseEvent(self, event):
  253. self.pressed = False
  254. QGraphicsRectItem.mouseReleaseEvent(self, event)
  255. self.setBrush(self.hover_brush)
  256. class PianoRoll(QGraphicsScene):
  257. '''the piano roll'''
  258. midievent = pyqtSignal(list)
  259. measureupdate = pyqtSignal(int)
  260. modeupdate = pyqtSignal(str)
  261. def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
  262. QGraphicsScene.__init__(self)
  263. self.setBackgroundBrush(QColor(50, 50, 50))
  264. self.mousePos = QPointF()
  265. self.notes = []
  266. self.selected_notes = []
  267. self.piano_keys = []
  268. self.marquee_select = False
  269. self.insert_mode = False
  270. self.velocity_mode = False
  271. self.place_ghost = False
  272. self.ghost_note = None
  273. self.default_ghost_vel = 100
  274. self.ghost_vel = self.default_ghost_vel
  275. ## dimensions
  276. self.padding = 2
  277. ## piano dimensions
  278. self.note_height = 10
  279. self.start_octave = -2
  280. self.end_octave = 8
  281. self.notes_in_octave = 12
  282. self.total_notes = (self.end_octave - self.start_octave) \
  283. * self.notes_in_octave + 1
  284. self.piano_height = self.note_height * self.total_notes
  285. self.octave_height = self.notes_in_octave * self.note_height
  286. self.piano_width = 34
  287. ## height
  288. self.header_height = 20
  289. self.total_height = self.piano_height - self.note_height + self.header_height
  290. #not sure why note_height is subtracted
  291. ## width
  292. self.full_note_width = 250 # i.e. a 4/4 note
  293. self.snap_value = None
  294. self.quantize_val = quantize_val
  295. ### dummy vars that will be changed
  296. self.time_sig = 0
  297. self.measure_width = 0
  298. self.num_measures = 0
  299. self.max_note_length = 0
  300. self.grid_width = 0
  301. self.value_width = 0
  302. self.grid_div = 0
  303. self.piano = None
  304. self.header = None
  305. self.play_head = None
  306. self.setTimeSig(time_sig)
  307. self.setMeasures(num_measures)
  308. self.setGridDiv()
  309. self.default_length = 1. / self.grid_div
  310. # -------------------------------------------------------------------------
  311. # Callbacks
  312. def movePlayHead(self, transport_info):
  313. # TODO: need conversion between frames and PPQ
  314. x = 105. # works for 120bpm
  315. total_duration = self.time_sig[0] * self.num_measures * x
  316. pos = transport_info['frame'] / x
  317. frac = (pos % total_duration) / total_duration
  318. self.play_head.setPos(QPointF(frac * self.grid_width, 0))
  319. def setTimeSig(self, time_sig):
  320. try:
  321. new_time_sig = list(map(float, time_sig.split('/')))
  322. if len(new_time_sig)==2:
  323. self.time_sig = new_time_sig
  324. self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1]
  325. self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
  326. self.grid_width = self.measure_width * self.num_measures
  327. self.setGridDiv()
  328. except ValueError:
  329. pass
  330. def setMeasures(self, measures):
  331. try:
  332. self.num_measures = float(measures)
  333. self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
  334. self.grid_width = self.measure_width * self.num_measures
  335. self.refreshScene()
  336. except:
  337. pass
  338. def setDefaultLength(self, length):
  339. try:
  340. v = list(map(float, length.split('/')))
  341. if len(v) < 3:
  342. self.default_length = \
  343. v[0] if len(v)==1 else \
  344. v[0] / v[1]
  345. pos = self.enforce_bounds(self.mousePos)
  346. if self.insert_mode: self.makeGhostNote(pos.x(), pos.y())
  347. except ValueError:
  348. pass
  349. def setGridDiv(self, div=None):
  350. if not div: div = self.quantize_val
  351. try:
  352. val = list(map(int, div.split('/')))
  353. if len(val) < 3:
  354. self.quantize_val = div
  355. self.grid_div = val[0] if len(val)==1 else val[1]
  356. self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None
  357. self.setQuantize(div)
  358. self.refreshScene()
  359. except ValueError:
  360. pass
  361. def setQuantize(self, value):
  362. try:
  363. val = list(map(float, value.split('/')))
  364. if len(val) == 1:
  365. self.quantize(val[0])
  366. self.quantize_val = value
  367. elif len(val) == 2:
  368. self.quantize(val[0] / val[1])
  369. self.quantize_val = value
  370. except ValueError:
  371. pass
  372. # -------------------------------------------------------------------------
  373. # Event Callbacks
  374. def keyPressEvent(self, event):
  375. QGraphicsScene.keyPressEvent(self, event)
  376. if event.key() == Qt.Key_F:
  377. if not self.insert_mode:
  378. self.velocity_mode = False
  379. self.insert_mode = True
  380. self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
  381. self.modeupdate.emit('insert_mode')
  382. elif self.insert_mode:
  383. self.insert_mode = False
  384. if self.place_ghost: self.place_ghost = False
  385. self.removeItem(self.ghost_note)
  386. self.ghost_note = None
  387. self.modeupdate.emit('')
  388. elif event.key() == Qt.Key_D:
  389. if self.velocity_mode:
  390. self.velocity_mode = False
  391. self.modeupdate.emit('')
  392. else:
  393. if self.insert_mode:
  394. self.removeItem(self.ghost_note)
  395. self.ghost_note = None
  396. self.insert_mode = False
  397. self.place_ghost = False
  398. self.velocity_mode = True
  399. self.modeupdate.emit('velocity_mode')
  400. elif event.key() == Qt.Key_A:
  401. if all((note.isSelected() for note in self.notes)):
  402. for note in self.notes:
  403. note.setSelected(False)
  404. self.selected_notes = []
  405. else:
  406. for note in self.notes:
  407. note.setSelected(True)
  408. self.selected_notes = self.notes[:]
  409. elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
  410. self.notes = [note for note in self.notes if note not in self.selected_notes]
  411. for note in self.selected_notes:
  412. self.removeItem(note)
  413. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  414. del note
  415. self.selected_notes = []
  416. def mousePressEvent(self, event):
  417. QGraphicsScene.mousePressEvent(self, event)
  418. if not (any(key.pressed for key in self.piano_keys)
  419. or any(note.pressed for note in self.notes)):
  420. for note in self.selected_notes:
  421. note.setSelected(False)
  422. self.selected_notes = []
  423. if event.button() == Qt.LeftButton:
  424. if self.insert_mode:
  425. self.place_ghost = True
  426. else:
  427. self.marquee_select = True
  428. self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1)
  429. self.marquee = QGraphicsRectItem(self.marquee_rect)
  430. self.marquee.setBrush(QColor(255, 255, 255, 100))
  431. self.addItem(self.marquee)
  432. else:
  433. for s_note in self.notes:
  434. if s_note.pressed and s_note in self.selected_notes:
  435. break
  436. elif s_note.pressed and s_note not in self.selected_notes:
  437. for note in self.selected_notes:
  438. note.setSelected(False)
  439. self.selected_notes = [s_note]
  440. break
  441. for note in self.selected_notes:
  442. if not self.velocity_mode:
  443. note.mousePressEvent(event)
  444. def mouseMoveEvent(self, event):
  445. QGraphicsScene.mouseMoveEvent(self, event)
  446. self.mousePos = event.scenePos()
  447. if not (any((key.pressed for key in self.piano_keys))):
  448. m_pos = event.scenePos()
  449. if self.insert_mode and self.place_ghost: #placing a note
  450. m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
  451. if m_pos.x() > m_width:
  452. m_new_x = self.snap(m_pos.x())
  453. self.ghost_rect.setRight(m_new_x)
  454. self.ghost_note.setRect(self.ghost_rect)
  455. #self.adjust_note_vel(event)
  456. else:
  457. m_pos = self.enforce_bounds(m_pos)
  458. if self.insert_mode: #ghostnote follows mouse around
  459. (m_new_x, m_new_y) = self.snap(m_pos.x(), m_pos.y())
  460. self.ghost_rect.moveTo(m_new_x, m_new_y)
  461. try:
  462. self.ghost_note.setRect(self.ghost_rect)
  463. except RuntimeError:
  464. self.ghost_note = None
  465. self.makeGhostNote(m_new_x, m_new_y)
  466. elif self.marquee_select:
  467. marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton)
  468. if marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() < m_pos.y():
  469. self.marquee_rect.setBottomRight(m_pos)
  470. elif marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() > m_pos.y():
  471. self.marquee_rect.setTopRight(m_pos)
  472. elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() < m_pos.y():
  473. self.marquee_rect.setBottomLeft(m_pos)
  474. elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() > m_pos.y():
  475. self.marquee_rect.setTopLeft(m_pos)
  476. self.marquee.setRect(self.marquee_rect)
  477. self.selected_notes = []
  478. for item in self.collidingItems(self.marquee):
  479. if item in self.notes:
  480. self.selected_notes.append(item)
  481. for note in self.notes:
  482. if note in self.selected_notes: note.setSelected(True)
  483. else: note.setSelected(False)
  484. elif self.velocity_mode:
  485. if Qt.LeftButton == event.buttons():
  486. for note in self.selected_notes:
  487. note.updateVelocity(event)
  488. elif not self.marquee_select: #move selected
  489. if Qt.LeftButton == event.buttons():
  490. x = y = False
  491. if any(note.back.stretch for note in self.selected_notes):
  492. x = True
  493. elif any(note.front.stretch for note in self.selected_notes):
  494. y = True
  495. for note in self.selected_notes:
  496. note.back.stretch = x
  497. note.front.stretch = y
  498. note.moveEvent(event)
  499. def mouseReleaseEvent(self, event):
  500. if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))):
  501. if event.button() == Qt.LeftButton:
  502. if self.place_ghost and self.insert_mode:
  503. self.place_ghost = False
  504. note_start = self.get_note_start_from_x(self.ghost_rect.x())
  505. note_num = self.get_note_num_from_y(self.ghost_rect.y())
  506. note_length = self.get_note_length_from_x(self.ghost_rect.width())
  507. self.drawNote(note_num, note_start, note_length, self.ghost_vel)
  508. self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel])
  509. self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
  510. elif self.marquee_select:
  511. self.marquee_select = False
  512. self.removeItem(self.marquee)
  513. elif not self.marquee_select:
  514. for note in self.selected_notes:
  515. old_info = note.note[:]
  516. note.mouseReleaseEvent(event)
  517. if self.velocity_mode:
  518. note.setSelected(True)
  519. if not old_info == note.note:
  520. self.midievent.emit(["midievent-remove", old_info[0], old_info[1], old_info[2], old_info[3]])
  521. self.midievent.emit(["midievent-add", note.note[0], note.note[1], note.note[2], note.note[3]])
  522. # -------------------------------------------------------------------------
  523. # Internal Functions
  524. def drawHeader(self):
  525. self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height)
  526. #self.header.setZValue(1.0)
  527. self.header.setPos(self.piano_width, 0)
  528. self.addItem(self.header)
  529. def drawPiano(self):
  530. piano_keys_width = self.piano_width - self.padding
  531. labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C')
  532. black_notes = (2,4,6,9,11)
  533. piano_label = QFont()
  534. piano_label.setPointSize(6)
  535. self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height)
  536. self.piano.setPos(0, self.header_height)
  537. self.addItem(self.piano)
  538. key = PianoKeyItem(piano_keys_width, self.note_height, self.piano)
  539. label = QGraphicsSimpleTextItem('C8', key)
  540. label.setPos(18, 1)
  541. label.setFont(piano_label)
  542. key.setBrush(QColor(255, 255, 255))
  543. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  544. for j in range(self.notes_in_octave, 0, -1):
  545. if j in black_notes:
  546. key = PianoKeyItem(piano_keys_width/1.4, self.note_height, self.piano)
  547. key.setBrush(QColor(0, 0, 0))
  548. key.setZValue(1.0)
  549. key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  550. elif (j - 1) and (j + 1) in black_notes:
  551. key = PianoKeyItem(piano_keys_width, self.note_height * 2, self.piano)
  552. key.setBrush(QColor(255, 255, 255))
  553. key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
  554. elif (j - 1) in black_notes:
  555. key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano)
  556. key.setBrush(QColor(255, 255, 255))
  557. key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
  558. elif (j + 1) in black_notes:
  559. key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano)
  560. key.setBrush(QColor(255, 255, 255))
  561. key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  562. if j == 12:
  563. label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i), key )
  564. label.setPos(18, 6)
  565. label.setFont(piano_label)
  566. self.piano_keys.append(key)
  567. def drawGrid(self):
  568. black_notes = [2,4,6,9,11]
  569. scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
  570. scale_bar.setPos(self.piano_width, 0)
  571. scale_bar.setBrush(QColor(100,100,100))
  572. clearpen = QPen(QColor(0,0,0,0))
  573. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  574. for j in range(self.notes_in_octave, 0, -1):
  575. scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
  576. scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1))
  577. scale_bar.setPen(clearpen)
  578. if j not in black_notes:
  579. scale_bar.setBrush(QColor(120,120,120))
  580. else:
  581. scale_bar.setBrush(QColor(100,100,100))
  582. measure_pen = QPen(QColor(0, 0, 0, 120), 3)
  583. half_measure_pen = QPen(QColor(0, 0, 0, 40), 2)
  584. line_pen = QPen(QColor(0, 0, 0, 40))
  585. for i in range(0, int(self.num_measures) + 1):
  586. measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header)
  587. measure.setPos(self.measure_width * i, 0.5 * measure_pen.width())
  588. measure.setPen(measure_pen)
  589. if i < self.num_measures:
  590. number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
  591. number.setPos(self.measure_width * i + 5, 2)
  592. number.setBrush(Qt.white)
  593. for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.):
  594. line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
  595. line.setZValue(1.0)
  596. line.setPos(self.measure_width * i + self.value_width * j, self.header_height)
  597. if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0:
  598. line.setPen(half_measure_pen)
  599. else:
  600. line.setPen(line_pen)
  601. def drawPlayHead(self):
  602. self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height)
  603. self.play_head.setPen(QPen(QColor(255,255,255,50), 2))
  604. self.play_head.setZValue(1.)
  605. self.addItem(self.play_head)
  606. def refreshScene(self):
  607. list(map(self.removeItem, self.notes))
  608. self.selected_notes = []
  609. self.piano_keys = []
  610. self.clear()
  611. self.drawPiano()
  612. self.drawHeader()
  613. self.drawGrid()
  614. self.drawPlayHead()
  615. for note in self.notes[:]:
  616. if note.note[1] >= (self.num_measures * self.time_sig[0]):
  617. self.notes.remove(note)
  618. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  619. elif note.note[2] > self.max_note_length:
  620. new_note = note.note[:]
  621. new_note[2] = self.max_note_length
  622. self.notes.remove(note)
  623. self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False)
  624. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  625. self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
  626. list(map(self.addItem, self.notes))
  627. if self.views():
  628. self.views()[0].setSceneRect(self.itemsBoundingRect())
  629. def clearNotes(self):
  630. self.clear()
  631. self.notes = []
  632. self.selected_notes = []
  633. self.drawPiano()
  634. self.drawHeader()
  635. self.drawGrid()
  636. def makeGhostNote(self, pos_x, pos_y):
  637. """creates the ghostnote that is placed on the scene before the real one is."""
  638. if self.ghost_note:
  639. self.removeItem(self.ghost_note)
  640. length = self.full_note_width * self.default_length
  641. (start, note) = self.snap(pos_x, pos_y)
  642. self.ghost_vel = self.default_ghost_vel
  643. self.ghost_rect = QRectF(start, note, length, self.note_height)
  644. self.ghost_rect_orig_width = self.ghost_rect.width()
  645. self.ghost_note = QGraphicsRectItem(self.ghost_rect)
  646. self.ghost_note.setBrush(QColor(230, 221, 45, 100))
  647. self.addItem(self.ghost_note)
  648. def drawNote(self, note_num, note_start=None, note_length=None, note_velocity=None, add=True):
  649. """
  650. note_num: midi number, 0 - 127
  651. note_start: 0 - (num_measures * time_sig[0]) so this is in beats
  652. note_length: 0 - (num_measures * time_sig[0]/time_sig[1]) this is in measures
  653. note_velocity: 0 - 127
  654. """
  655. info = [note_num, note_start, note_length, note_velocity]
  656. if not note_start % (self.num_measures * self.time_sig[0]) == note_start:
  657. #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity])
  658. while not note_start % (self.num_measures * self.time_sig[0]) == note_start:
  659. self.setMeasures(self.num_measures+1)
  660. self.measureupdate.emit(self.num_measures)
  661. self.refreshScene()
  662. x_start = self.get_note_x_start(note_start)
  663. if note_length > self.max_note_length:
  664. note_length = self.max_note_length + 0.25
  665. x_length = self.get_note_x_length(note_length)
  666. y_pos = self.get_note_y_pos(note_num)
  667. note = NoteItem(self.note_height, x_length, info)
  668. note.setPos(x_start, y_pos)
  669. self.notes.append(note)
  670. if add:
  671. self.addItem(note)
  672. # -------------------------------------------------------------------------
  673. # Helper Functions
  674. def frange(self, x, y, t):
  675. while x < y:
  676. yield x
  677. x += t
  678. def quantize(self, value):
  679. self.snap_value = float(self.full_note_width) * value if value else None
  680. def snap(self, pos_x, pos_y = None):
  681. if self.snap_value:
  682. pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) \
  683. * self.snap_value + self.piano_width
  684. if pos_y:
  685. pos_y = int((pos_y - self.header_height) / self.note_height) \
  686. * self.note_height + self.header_height
  687. return (pos_x, pos_y) if pos_y else pos_x
  688. def adjust_note_vel(self, event):
  689. m_pos = event.scenePos()
  690. #bind velocity to vertical mouse movement
  691. self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10
  692. if self.ghost_vel < 0:
  693. self.ghost_vel = 0
  694. elif self.ghost_vel > 127:
  695. self.ghost_vel = 127
  696. m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
  697. if m_pos.x() < m_width:
  698. m_pos.setX(m_width)
  699. m_new_x = self.snap(m_pos.x())
  700. self.ghost_rect.setRight(m_new_x)
  701. self.ghost_note.setRect(self.ghost_rect)
  702. def enforce_bounds(self, pos):
  703. if pos.x() < self.piano_width:
  704. pos.setX(self.piano_width)
  705. elif pos.x() > self.grid_width + self.piano_width:
  706. pos.setX(self.grid_width + self.piano_width)
  707. if pos.y() < self.header_height + self.padding:
  708. pos.setY(self.header_height + self.padding)
  709. return pos
  710. def get_note_start_from_x(self, note_x):
  711. return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0])
  712. def get_note_x_start(self, note_start):
  713. return self.piano_width + \
  714. (self.grid_width / self.num_measures / self.time_sig[0]) * note_start
  715. def get_note_x_length(self, note_length):
  716. return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures
  717. def get_note_length_from_x(self, note_x):
  718. return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width \
  719. * note_x
  720. def get_note_y_pos(self, note_num):
  721. return self.header_height + self.note_height * (self.total_notes - note_num - 1)
  722. def get_note_num_from_y(self, note_y_pos):
  723. return -(((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)
  724. class PianoRollView(QGraphicsView):
  725. def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
  726. QGraphicsView.__init__(self)
  727. self.piano = PianoRoll(time_sig, num_measures, quantize_val)
  728. self.setScene(self.piano)
  729. self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  730. x = 0 * self.sceneRect().width() + self.sceneRect().left()
  731. y = 0.4 * self.sceneRect().height() + self.sceneRect().top()
  732. self.centerOn(x, y)
  733. self.setAlignment(Qt.AlignLeft)
  734. self.o_transform = self.transform()
  735. self.zoom_x = 1
  736. self.zoom_y = 1
  737. def setZoomX(self, scale_x):
  738. self.setTransform(self.o_transform)
  739. self.zoom_x = 1 + scale_x / float(99) * 2
  740. self.scale(self.zoom_x, self.zoom_y)
  741. def setZoomY(self, scale_y):
  742. self.setTransform(self.o_transform)
  743. self.zoom_y = 1 + scale_y / float(99)
  744. self.scale(self.zoom_x, self.zoom_y)
  745. # ------------------------------------------------------------------------------------------------------------
  746. # External UI
  747. class ModeIndicator(QWidget):
  748. def __init__(self):
  749. QWidget.__init__(self)
  750. #self.setGeometry(0, 0, 30, 20)
  751. self.setFixedSize(30,20)
  752. self.mode = None
  753. def paintEvent(self, event):
  754. painter = QPainter()
  755. painter.begin(self)
  756. painter.setPen(QPen(QColor(0, 0, 0, 0)))
  757. if self.mode == 'velocity_mode':
  758. painter.setBrush(QColor(127, 0, 0))
  759. elif self.mode == 'insert_mode':
  760. painter.setBrush(QColor(0, 100, 127))
  761. else:
  762. painter.setBrush(QColor(0, 0, 0, 0))
  763. painter.drawRect(0, 0, 30, 20)
  764. painter.end()
  765. def changeMode(self, new_mode):
  766. self.mode = new_mode
  767. self.update()
  768. class MainWindow(ExternalUI, QWidget):
  769. def __init__(self):
  770. ExternalUI.__init__(self)
  771. QWidget.__init__(self)
  772. # to be filled with note-on events, while waiting for their matching note-off
  773. self.fPendingNoteOns = [] # (channel, note, velocity, time)
  774. self.fTransportInfo = {
  775. "playing": False,
  776. "frame": 0,
  777. "bar": 0,
  778. "beat": 0,
  779. "tick": 0,
  780. "bpm": 120.0,
  781. "sigNum": 4.0,
  782. "sigDenom": 4.0
  783. }
  784. self.PPQ = 48.
  785. self.initUI()
  786. self.piano.midievent.connect(self.sendMsg)
  787. self.piano.measureupdate.connect(self.updateMeasureBox)
  788. self.piano.modeupdate.connect(self.modeIndicator.changeMode)
  789. self.fIdleTimer = self.startTimer(30)
  790. self.setWindowTitle(self.fUiName)
  791. self.ready()
  792. def initUI(self):
  793. self.view = PianoRollView(
  794. time_sig = "{}/{}".format(
  795. int(self.fTransportInfo["sigNum"]),
  796. int(self.fTransportInfo["sigNum"])),
  797. num_measures = 4,
  798. quantize_val = '1/8')
  799. self.piano = self.view.piano
  800. self.timeSigLabel = QLabel('time signature')
  801. self.timeSigLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter)
  802. self.timeSigLabel.setMaximumWidth(100)
  803. self.timeSigBox = QComboBox()
  804. self.timeSigBox.setEditable(True)
  805. self.timeSigBox.setMaximumWidth(100)
  806. self.timeSigBox.addItems(
  807. ('1/4', '2/4', '3/4', '4/4', '5/4', '6/4', '12/8'))
  808. self.timeSigBox.setCurrentIndex(3)
  809. self.measureLabel = QLabel('measures')
  810. self.measureLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter)
  811. self.measureLabel.setMaximumWidth(100)
  812. self.measureBox = QComboBox()
  813. self.measureBox.setMaximumWidth(100)
  814. self.measureBox.addItems(list(map(str, range(1,17))))
  815. self.measureBox.setCurrentIndex(3)
  816. self.defaultLengthLabel = QLabel('default length')
  817. self.defaultLengthLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter)
  818. self.defaultLengthLabel.setMaximumWidth(100)
  819. self.defaultLengthBox = QComboBox()
  820. self.defaultLengthBox.setEditable(True)
  821. self.defaultLengthBox.setMaximumWidth(100)
  822. self.defaultLengthBox.addItems(('1/16', '1/15', '1/12', '1/9', '1/8', '1/6', '1/4', '1/3', '1/2', '1'))
  823. self.defaultLengthBox.setCurrentIndex(4)
  824. self.quantizeLabel = QLabel('quantize')
  825. self.quantizeLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter)
  826. self.quantizeLabel.setMaximumWidth(100)
  827. self.quantizeBox = QComboBox()
  828. self.quantizeBox.setEditable(True)
  829. self.quantizeBox.setMaximumWidth(100)
  830. self.quantizeBox.addItems(('0', '1/16', '1/15', '1/12', '1/9', '1/8', '1/6', '1/4', '1/3', '1/2', '1'))
  831. self.quantizeBox.setCurrentIndex(5)
  832. self.hSlider = QSlider(Qt.Horizontal)
  833. self.hSlider.setTracking(True)
  834. #hSlider.setMaximum(1920*6*3*4)
  835. self.vSlider = QSlider(Qt.Vertical)
  836. self.vSlider.setTracking(True)
  837. self.vSlider.setInvertedAppearance(True)
  838. self.vSlider.setMaximumHeight(500)
  839. self.modeIndicator = ModeIndicator()
  840. self.timeSigBox.currentIndexChanged[str].connect(self.piano.setTimeSig)
  841. self.measureBox.currentIndexChanged[str].connect(self.piano.setMeasures)
  842. self.defaultLengthBox.currentIndexChanged[str].connect(self.piano.setDefaultLength)
  843. self.quantizeBox.currentIndexChanged[str].connect(self.piano.setGridDiv)
  844. self.hSlider.valueChanged.connect(self.view.setZoomX)
  845. self.vSlider.valueChanged.connect(self.view.setZoomY)
  846. self.hBox = QHBoxLayout()
  847. self.hBox.addWidget(self.modeIndicator)
  848. self.hBox.addWidget(self.timeSigLabel)
  849. self.hBox.addWidget(self.timeSigBox)
  850. self.hBox.addWidget(self.measureLabel)
  851. self.hBox.addWidget(self.measureBox)
  852. self.hBox.addWidget(self.defaultLengthLabel)
  853. self.hBox.addWidget(self.defaultLengthBox)
  854. self.hBox.addWidget(self.quantizeLabel)
  855. self.hBox.addWidget(self.quantizeBox)
  856. self.hBox.addWidget(self.hSlider)
  857. self.viewBox = QHBoxLayout()
  858. self.viewBox.addWidget(self.vSlider)
  859. self.viewBox.addWidget(self.view)
  860. self.layout = QVBoxLayout()
  861. self.layout.addLayout(self.hBox)
  862. self.layout.addLayout(self.viewBox)
  863. self.setLayout(self.layout)
  864. self.view.setFocus()
  865. # -------------------------------------------------------------------
  866. # DSP Callbacks
  867. def dspParameterChanged(self, index, value):
  868. pass
  869. def dspStateChanged(self, key, value):
  870. pass
  871. # -------------------------------------------------------------------
  872. # ExternalUI Callbacks
  873. def uiShow(self):
  874. self.show()
  875. def uiFocus(self):
  876. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  877. self.show()
  878. self.raise_()
  879. self.activateWindow()
  880. def uiHide(self):
  881. self.hide()
  882. def uiQuit(self):
  883. self.closeExternalUI()
  884. self.close()
  885. app.quit()
  886. def uiTitleChanged(self, uiTitle):
  887. self.setWindowTitle(uiTitle)
  888. # -------------------------------------------------------------------
  889. # Qt events
  890. def timerEvent(self, event):
  891. if event.timerId() == self.fIdleTimer:
  892. self.idleExternalUI()
  893. QGraphicsView.timerEvent(self, event)
  894. def closeEvent(self, event):
  895. self.closeExternalUI()
  896. QGraphicsView.closeEvent(self, event)
  897. # -------------------------------------------------------------------
  898. # Custom callback
  899. def updateMeasureBox(self, index):
  900. self.measureBox.setCurrentIndex(index-1)
  901. def sendMsg(self, data):
  902. msg = data[0]
  903. if msg == "midievent-remove":
  904. note, start, length, vel = data[1:5]
  905. note_start = start * 60. / self.fTransportInfo["bpm"] * 4. / self.fTransportInfo["sigDenom"] * self.PPQ
  906. note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ
  907. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  908. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  909. elif msg == "midievent-add":
  910. note, start, length, vel = data[1:5]
  911. note_start = start * 60. / self.fTransportInfo["bpm"] * self.PPQ
  912. note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ
  913. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  914. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  915. def msgCallback(self, msg):
  916. #try:
  917. self.msgCallback2(msg)
  918. #except:
  919. #print("Custom msgCallback error, skipped for", msg)
  920. def msgCallback2(self, msg):
  921. msg = charPtrToString(msg)
  922. if msg == "midi-clear-all":
  923. # clear all notes
  924. self.piano.clearNotes()
  925. elif msg == "midievent-add":
  926. # adds single midi event
  927. time = int(self.readlineblock())
  928. size = int(self.readlineblock())
  929. data = []
  930. for x in range(size):
  931. data.append(int(self.readlineblock()))
  932. self.handleMidiEvent(time, size, data)
  933. elif msg == "transport":
  934. playing = bool(self.readlineblock() == "true")
  935. frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
  936. bpm, sigNum, sigDenom = [float(i) for i in self.readlineblock().split(":")]
  937. if beat != self.fTransportInfo["beat"]:
  938. print(beat)
  939. old_frame = self.fTransportInfo['frame']
  940. self.fTransportInfo = {
  941. "playing": playing,
  942. "frame": frame,
  943. "bar": bar,
  944. "beat": beat,
  945. "tick": tick,
  946. "bpm": bpm,
  947. "sigNum": sigNum,
  948. "sigDenom": sigDenom
  949. }
  950. if old_frame != frame:
  951. self.piano.movePlayHead(self.fTransportInfo)
  952. elif msg == "show":
  953. self.uiShow()
  954. elif msg == "focus":
  955. self.uiFocus()
  956. elif msg == "hide":
  957. self.uiHide()
  958. elif msg == "quit":
  959. self.fQuitReceived = True
  960. self.uiQuit()
  961. elif msg == "uiTitle":
  962. uiTitle = self.readlineblock()
  963. self.uiTitleChanged(uiTitle)
  964. else:
  965. print("unknown message: \"" + msg + "\"")
  966. # -------------------------------------------------------------------
  967. # Internal stuff
  968. def handleMidiEvent(self, time, size, data):
  969. #print("Got MIDI Event on UI", time, size, data)
  970. # NOTE: for now time comes in frames, which might not be desirable
  971. # we'll convert it to a smaller value for now (seconds)
  972. # later on we can have time as PPQ or similar
  973. time /= self.PPQ
  974. status = MIDI_GET_STATUS_FROM_DATA(data)
  975. channel = MIDI_GET_CHANNEL_FROM_DATA(data)
  976. if status == MIDI_STATUS_NOTE_ON:
  977. note = data[1]
  978. velo = data[2]
  979. # append (channel, note, velo, time) for later
  980. self.fPendingNoteOns.append((channel, note, velo, time))
  981. elif status == MIDI_STATUS_NOTE_OFF:
  982. note = data[1]
  983. velo = data[2]
  984. # find previous note-on that matches this note and channel
  985. for noteOnMsg in self.fPendingNoteOns:
  986. channel_, note_, velo_, time_ = noteOnMsg
  987. if channel_ != channel:
  988. continue
  989. if note_ != note:
  990. continue
  991. # found it
  992. #print("{} {} {} {}\n".format(note, time_, time-time_, velo_))
  993. start = time_ / 60. * self.fTransportInfo["bpm"] / 4. * self.fTransportInfo["sigDenom"]
  994. length = (time - time_) / 60. * self.fTransportInfo["bpm"] / 4. / self.fTransportInfo["sigNum"] * self.fTransportInfo["sigDenom"]
  995. self.piano.drawNote(note, start, length, velo_)
  996. # remove from list
  997. self.fPendingNoteOns.remove(noteOnMsg)
  998. break
  999. #--------------- main ------------------
  1000. if __name__ == '__main__':
  1001. import resources_rc
  1002. pathBinaries, pathResources = getPaths()
  1003. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
  1004. gCarla.utils.set_process_name("MidiSequencer")
  1005. app = CarlaApplication("MidiSequencer")
  1006. gui = MainWindow()
  1007. app.exit_exec()