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 47KB

9 years ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193
  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, t):
  313. total_duration = 1920 * self.time_sig[0] * self.num_measures
  314. pos = t['bar']*1920*self.time_sig[0] + t['beat']*1920 + t['tick']
  315. frac = (pos % total_duration) / total_duration
  316. self.play_head.setPos(QPointF(frac * self.grid_width, 0))
  317. def setTimeSig(self, time_sig):
  318. try:
  319. new_time_sig = list(map(float, time_sig.split('/')))
  320. if len(new_time_sig)==2:
  321. self.time_sig = new_time_sig
  322. self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1]
  323. self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
  324. self.grid_width = self.measure_width * self.num_measures
  325. self.setGridDiv()
  326. except ValueError:
  327. pass
  328. def setMeasures(self, measures):
  329. try:
  330. self.num_measures = float(measures)
  331. self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
  332. self.grid_width = self.measure_width * self.num_measures
  333. self.refreshScene()
  334. except:
  335. pass
  336. def setDefaultLength(self, length):
  337. try:
  338. v = list(map(float, length.split('/')))
  339. if len(v) < 3:
  340. self.default_length = \
  341. v[0] if len(v)==1 else \
  342. v[0] / v[1]
  343. pos = self.enforce_bounds(self.mousePos)
  344. if self.insert_mode: self.makeGhostNote(pos.x(), pos.y())
  345. except ValueError:
  346. pass
  347. def setGridDiv(self, div=None):
  348. if not div: div = self.quantize_val
  349. try:
  350. val = list(map(int, div.split('/')))
  351. if len(val) < 3:
  352. self.quantize_val = div
  353. self.grid_div = val[0] if len(val)==1 else val[1]
  354. self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None
  355. self.setQuantize(div)
  356. self.refreshScene()
  357. except ValueError:
  358. pass
  359. def setQuantize(self, value):
  360. try:
  361. val = list(map(float, value.split('/')))
  362. if len(val) == 1:
  363. self.quantize(val[0])
  364. self.quantize_val = value
  365. elif len(val) == 2:
  366. self.quantize(val[0] / val[1])
  367. self.quantize_val = value
  368. except ValueError:
  369. pass
  370. # -------------------------------------------------------------------------
  371. # Event Callbacks
  372. def keyPressEvent(self, event):
  373. QGraphicsScene.keyPressEvent(self, event)
  374. if event.key() == Qt.Key_F:
  375. if not self.insert_mode:
  376. self.velocity_mode = False
  377. self.insert_mode = True
  378. self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
  379. self.modeupdate.emit('insert_mode')
  380. elif self.insert_mode:
  381. self.insert_mode = False
  382. if self.place_ghost: self.place_ghost = False
  383. self.removeItem(self.ghost_note)
  384. self.ghost_note = None
  385. self.modeupdate.emit('')
  386. elif event.key() == Qt.Key_D:
  387. if self.velocity_mode:
  388. self.velocity_mode = False
  389. self.modeupdate.emit('')
  390. else:
  391. if self.insert_mode:
  392. self.removeItem(self.ghost_note)
  393. self.ghost_note = None
  394. self.insert_mode = False
  395. self.place_ghost = False
  396. self.velocity_mode = True
  397. self.modeupdate.emit('velocity_mode')
  398. elif event.key() == Qt.Key_A:
  399. if all((note.isSelected() for note in self.notes)):
  400. for note in self.notes:
  401. note.setSelected(False)
  402. self.selected_notes = []
  403. else:
  404. for note in self.notes:
  405. note.setSelected(True)
  406. self.selected_notes = self.notes[:]
  407. elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
  408. self.notes = [note for note in self.notes if note not in self.selected_notes]
  409. for note in self.selected_notes:
  410. self.removeItem(note)
  411. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  412. del note
  413. self.selected_notes = []
  414. def mousePressEvent(self, event):
  415. QGraphicsScene.mousePressEvent(self, event)
  416. if not (any(key.pressed for key in self.piano_keys)
  417. or any(note.pressed for note in self.notes)):
  418. for note in self.selected_notes:
  419. note.setSelected(False)
  420. self.selected_notes = []
  421. if event.button() == Qt.LeftButton:
  422. if self.insert_mode:
  423. self.place_ghost = True
  424. else:
  425. self.marquee_select = True
  426. self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1)
  427. self.marquee = QGraphicsRectItem(self.marquee_rect)
  428. self.marquee.setBrush(QColor(255, 255, 255, 100))
  429. self.addItem(self.marquee)
  430. else:
  431. for s_note in self.notes:
  432. if s_note.pressed and s_note in self.selected_notes:
  433. break
  434. elif s_note.pressed and s_note not in self.selected_notes:
  435. for note in self.selected_notes:
  436. note.setSelected(False)
  437. self.selected_notes = [s_note]
  438. break
  439. for note in self.selected_notes:
  440. if not self.velocity_mode:
  441. note.mousePressEvent(event)
  442. def mouseMoveEvent(self, event):
  443. QGraphicsScene.mouseMoveEvent(self, event)
  444. self.mousePos = event.scenePos()
  445. if not (any((key.pressed for key in self.piano_keys))):
  446. m_pos = event.scenePos()
  447. if self.insert_mode and self.place_ghost: #placing a note
  448. m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
  449. if m_pos.x() > m_width:
  450. m_new_x = self.snap(m_pos.x())
  451. self.ghost_rect.setRight(m_new_x)
  452. self.ghost_note.setRect(self.ghost_rect)
  453. #self.adjust_note_vel(event)
  454. else:
  455. m_pos = self.enforce_bounds(m_pos)
  456. if self.insert_mode: #ghostnote follows mouse around
  457. (m_new_x, m_new_y) = self.snap(m_pos.x(), m_pos.y())
  458. self.ghost_rect.moveTo(m_new_x, m_new_y)
  459. try:
  460. self.ghost_note.setRect(self.ghost_rect)
  461. except RuntimeError:
  462. self.ghost_note = None
  463. self.makeGhostNote(m_new_x, m_new_y)
  464. elif self.marquee_select:
  465. marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton)
  466. if marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() < m_pos.y():
  467. self.marquee_rect.setBottomRight(m_pos)
  468. elif marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() > m_pos.y():
  469. self.marquee_rect.setTopRight(m_pos)
  470. elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() < m_pos.y():
  471. self.marquee_rect.setBottomLeft(m_pos)
  472. elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() > m_pos.y():
  473. self.marquee_rect.setTopLeft(m_pos)
  474. self.marquee.setRect(self.marquee_rect)
  475. self.selected_notes = []
  476. for item in self.collidingItems(self.marquee):
  477. if item in self.notes:
  478. self.selected_notes.append(item)
  479. for note in self.notes:
  480. if note in self.selected_notes: note.setSelected(True)
  481. else: note.setSelected(False)
  482. elif self.velocity_mode:
  483. if Qt.LeftButton == event.buttons():
  484. for note in self.selected_notes:
  485. note.updateVelocity(event)
  486. elif not self.marquee_select: #move selected
  487. if Qt.LeftButton == event.buttons():
  488. x = y = False
  489. if any(note.back.stretch for note in self.selected_notes):
  490. x = True
  491. elif any(note.front.stretch for note in self.selected_notes):
  492. y = True
  493. for note in self.selected_notes:
  494. note.back.stretch = x
  495. note.front.stretch = y
  496. note.moveEvent(event)
  497. def mouseReleaseEvent(self, event):
  498. if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))):
  499. if event.button() == Qt.LeftButton:
  500. if self.place_ghost and self.insert_mode:
  501. self.place_ghost = False
  502. note_start = self.get_note_start_from_x(self.ghost_rect.x())
  503. note_num = self.get_note_num_from_y(self.ghost_rect.y())
  504. note_length = self.get_note_length_from_x(self.ghost_rect.width())
  505. self.drawNote(note_num, note_start, note_length, self.ghost_vel)
  506. self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel])
  507. self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
  508. elif self.marquee_select:
  509. self.marquee_select = False
  510. self.removeItem(self.marquee)
  511. elif not self.marquee_select:
  512. for note in self.selected_notes:
  513. old_info = note.note[:]
  514. note.mouseReleaseEvent(event)
  515. if self.velocity_mode:
  516. note.setSelected(True)
  517. if not old_info == note.note:
  518. self.midievent.emit(["midievent-remove", old_info[0], old_info[1], old_info[2], old_info[3]])
  519. self.midievent.emit(["midievent-add", note.note[0], note.note[1], note.note[2], note.note[3]])
  520. # -------------------------------------------------------------------------
  521. # Internal Functions
  522. def drawHeader(self):
  523. self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height)
  524. #self.header.setZValue(1.0)
  525. self.header.setPos(self.piano_width, 0)
  526. self.addItem(self.header)
  527. def drawPiano(self):
  528. piano_keys_width = self.piano_width - self.padding
  529. labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C')
  530. black_notes = (2,4,6,9,11)
  531. piano_label = QFont()
  532. piano_label.setPointSize(6)
  533. self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height)
  534. self.piano.setPos(0, self.header_height)
  535. self.addItem(self.piano)
  536. key = PianoKeyItem(piano_keys_width, self.note_height, self.piano)
  537. label = QGraphicsSimpleTextItem('C8', key)
  538. label.setPos(18, 1)
  539. label.setFont(piano_label)
  540. key.setBrush(QColor(255, 255, 255))
  541. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  542. for j in range(self.notes_in_octave, 0, -1):
  543. if j in black_notes:
  544. key = PianoKeyItem(piano_keys_width/1.4, self.note_height, self.piano)
  545. key.setBrush(QColor(0, 0, 0))
  546. key.setZValue(1.0)
  547. key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  548. elif (j - 1) and (j + 1) in black_notes:
  549. key = PianoKeyItem(piano_keys_width, self.note_height * 2, self.piano)
  550. key.setBrush(QColor(255, 255, 255))
  551. key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
  552. elif (j - 1) in black_notes:
  553. key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano)
  554. key.setBrush(QColor(255, 255, 255))
  555. key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
  556. elif (j + 1) in black_notes:
  557. key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano)
  558. key.setBrush(QColor(255, 255, 255))
  559. key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  560. if j == 12:
  561. label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i), key )
  562. label.setPos(18, 6)
  563. label.setFont(piano_label)
  564. self.piano_keys.append(key)
  565. def drawGrid(self):
  566. black_notes = [2,4,6,9,11]
  567. scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
  568. scale_bar.setPos(self.piano_width, 0)
  569. scale_bar.setBrush(QColor(100,100,100))
  570. clearpen = QPen(QColor(0,0,0,0))
  571. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  572. for j in range(self.notes_in_octave, 0, -1):
  573. scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
  574. scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1))
  575. scale_bar.setPen(clearpen)
  576. if j not in black_notes:
  577. scale_bar.setBrush(QColor(120,120,120))
  578. else:
  579. scale_bar.setBrush(QColor(100,100,100))
  580. measure_pen = QPen(QColor(0, 0, 0, 120), 3)
  581. half_measure_pen = QPen(QColor(0, 0, 0, 40), 2)
  582. line_pen = QPen(QColor(0, 0, 0, 40))
  583. for i in range(0, int(self.num_measures) + 1):
  584. measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header)
  585. measure.setPos(self.measure_width * i, 0.5 * measure_pen.width())
  586. measure.setPen(measure_pen)
  587. if i < self.num_measures:
  588. number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
  589. number.setPos(self.measure_width * i + 5, 2)
  590. number.setBrush(Qt.white)
  591. for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.):
  592. line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
  593. line.setZValue(1.0)
  594. line.setPos(self.measure_width * i + self.value_width * j, self.header_height)
  595. if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0:
  596. line.setPen(half_measure_pen)
  597. else:
  598. line.setPen(line_pen)
  599. def drawPlayHead(self):
  600. self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height)
  601. self.play_head.setPen(QPen(QColor(255,255,255,50), 2))
  602. self.play_head.setZValue(1.)
  603. self.addItem(self.play_head)
  604. def refreshScene(self):
  605. list(map(self.removeItem, self.notes))
  606. self.selected_notes = []
  607. self.piano_keys = []
  608. self.clear()
  609. self.drawPiano()
  610. self.drawHeader()
  611. self.drawGrid()
  612. self.drawPlayHead()
  613. for note in self.notes[:]:
  614. if note.note[1] >= (self.num_measures * self.time_sig[0]):
  615. self.notes.remove(note)
  616. elif note.note[2] > self.max_note_length:
  617. new_note = note.note
  618. self.notes.remove(note)
  619. self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False)
  620. list(map(self.addItem, self.notes))
  621. if self.views():
  622. self.views()[0].setSceneRect(self.itemsBoundingRect())
  623. def clearNotes(self):
  624. self.clear()
  625. self.notes = []
  626. self.selected_notes = []
  627. self.drawPiano()
  628. self.drawHeader()
  629. self.drawGrid()
  630. def makeGhostNote(self, pos_x, pos_y):
  631. """creates the ghostnote that is placed on the scene before the real one is."""
  632. if self.ghost_note:
  633. self.removeItem(self.ghost_note)
  634. length = self.full_note_width * self.default_length
  635. (start, note) = self.snap(pos_x, pos_y)
  636. self.ghost_vel = self.default_ghost_vel
  637. self.ghost_rect = QRectF(start, note, length, self.note_height)
  638. self.ghost_rect_orig_width = self.ghost_rect.width()
  639. self.ghost_note = QGraphicsRectItem(self.ghost_rect)
  640. self.ghost_note.setBrush(QColor(230, 221, 45, 100))
  641. self.addItem(self.ghost_note)
  642. def drawNote(self, note_num, note_start=None, note_length=None, note_velocity=None, add=True):
  643. """
  644. note_num: midi number, 0 - 127
  645. note_start: 0 - (num_measures * time_sig[0]) so this is in beats
  646. note_length: 0 - (num_measures * time_sig[0]/time_sig[1]) this is in measures
  647. note_velocity: 0 - 127
  648. """
  649. info = [note_num, note_start, note_length, note_velocity]
  650. if not note_start % (self.num_measures * self.time_sig[0]) == note_start:
  651. #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity])
  652. while not note_start % (self.num_measures * self.time_sig[0]) == note_start:
  653. self.setMeasures(self.num_measures+1)
  654. self.measureupdate.emit(self.num_measures)
  655. self.refreshScene()
  656. x_start = self.get_note_x_start(note_start)
  657. if note_length > self.max_note_length:
  658. note_length = self.max_note_length + 0.25
  659. x_length = self.get_note_x_length(note_length)
  660. y_pos = self.get_note_y_pos(note_num)
  661. note = NoteItem(self.note_height, x_length, info)
  662. note.setPos(x_start, y_pos)
  663. self.notes.append(note)
  664. if add:
  665. self.addItem(note)
  666. # -------------------------------------------------------------------------
  667. # Helper Functions
  668. def frange(self, x, y, t):
  669. while x < y:
  670. yield x
  671. x += t
  672. def quantize(self, value):
  673. self.snap_value = float(self.full_note_width) * value if value else None
  674. def snap(self, pos_x, pos_y = None):
  675. if self.snap_value:
  676. pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) \
  677. * self.snap_value + self.piano_width
  678. if pos_y:
  679. pos_y = int((pos_y - self.header_height) / self.note_height) \
  680. * self.note_height + self.header_height
  681. return (pos_x, pos_y) if pos_y else pos_x
  682. def adjust_note_vel(self, event):
  683. m_pos = event.scenePos()
  684. #bind velocity to vertical mouse movement
  685. self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10
  686. if self.ghost_vel < 0:
  687. self.ghost_vel = 0
  688. elif self.ghost_vel > 127:
  689. self.ghost_vel = 127
  690. m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
  691. if m_pos.x() < m_width:
  692. m_pos.setX(m_width)
  693. m_new_x = self.snap(m_pos.x())
  694. self.ghost_rect.setRight(m_new_x)
  695. self.ghost_note.setRect(self.ghost_rect)
  696. def enforce_bounds(self, pos):
  697. if pos.x() < self.piano_width:
  698. pos.setX(self.piano_width)
  699. elif pos.x() > self.grid_width + self.piano_width:
  700. pos.setX(self.grid_width + self.piano_width)
  701. if pos.y() < self.header_height + self.padding:
  702. pos.setY(self.header_height + self.padding)
  703. return pos
  704. def get_note_start_from_x(self, note_x):
  705. return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0])
  706. def get_note_x_start(self, note_start):
  707. return self.piano_width + \
  708. (self.grid_width / self.num_measures / self.time_sig[0]) * note_start
  709. def get_note_x_length(self, note_length):
  710. return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures
  711. def get_note_length_from_x(self, note_x):
  712. return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width \
  713. * note_x
  714. def get_note_y_pos(self, note_num):
  715. return self.header_height + self.note_height * (self.total_notes - note_num - 1)
  716. def get_note_num_from_y(self, note_y_pos):
  717. return -(((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)
  718. class PianoRollView(QGraphicsView):
  719. def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
  720. QGraphicsView.__init__(self)
  721. self.piano = PianoRoll(time_sig, num_measures, quantize_val)
  722. self.setScene(self.piano)
  723. self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  724. x = 0 * self.sceneRect().width() + self.sceneRect().left()
  725. y = 0.4 * self.sceneRect().height() + self.sceneRect().top()
  726. self.centerOn(x, y)
  727. self.setAlignment(Qt.AlignLeft)
  728. self.o_transform = self.transform()
  729. self.zoom_x = 1
  730. self.zoom_y = 1
  731. def setZoomX(self, scale_x):
  732. self.setTransform(self.o_transform)
  733. self.zoom_x = 1 + scale_x / float(99) * 2
  734. self.scale(self.zoom_x, self.zoom_y)
  735. def setZoomY(self, scale_y):
  736. self.setTransform(self.o_transform)
  737. self.zoom_y = 1 + scale_y / float(99)
  738. self.scale(self.zoom_x, self.zoom_y)
  739. # ------------------------------------------------------------------------------------------------------------
  740. # External UI
  741. class ModeIndicator(QWidget):
  742. def __init__(self):
  743. QWidget.__init__(self)
  744. #self.setGeometry(0, 0, 30, 20)
  745. self.setFixedSize(30,20)
  746. self.mode = None
  747. def paintEvent(self, event):
  748. painter = QPainter()
  749. painter.begin(self)
  750. painter.setPen(QPen(QColor(0, 0, 0, 0)))
  751. if self.mode == 'velocity_mode':
  752. painter.setBrush(QColor(127, 0, 0))
  753. elif self.mode == 'insert_mode':
  754. painter.setBrush(QColor(0, 100, 127))
  755. else:
  756. painter.setBrush(QColor(0, 0, 0, 0))
  757. painter.drawRect(0, 0, 30, 20)
  758. painter.end()
  759. def changeMode(self, new_mode):
  760. self.mode = new_mode
  761. self.update()
  762. class MainWindow(ExternalUI, QWidget):
  763. def __init__(self):
  764. ExternalUI.__init__(self)
  765. QWidget.__init__(self)
  766. # to be filled with note-on events, while waiting for their matching note-off
  767. self.fPendingNoteOns = [] # (channel, note, velocity, time)
  768. self.fTransportInfo = {
  769. "playing": False,
  770. "frame": 0,
  771. "bar": 0,
  772. "beat": 0,
  773. "tick": 0,
  774. "bpm": 120.0,
  775. "sigNum": 4.0,
  776. "sigDenom": 4.0
  777. }
  778. self.initUI()
  779. self.piano.midievent.connect(self.sendMsg)
  780. self.piano.measureupdate.connect(self.updateMeasureBox)
  781. self.piano.modeupdate.connect(self.modeIndicator.changeMode)
  782. self.fIdleTimer = self.startTimer(30)
  783. self.setWindowTitle(self.fUiName)
  784. self.ready()
  785. def initUI(self):
  786. self.view = PianoRollView(
  787. time_sig = "{}/{}".format(
  788. int(self.fTransportInfo["sigNum"]),
  789. int(self.fTransportInfo["sigNum"])),
  790. num_measures = 4,
  791. quantize_val = '1/8')
  792. self.piano = self.view.piano
  793. self.timeSigLabel = QLabel('time signature')
  794. self.timeSigLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter)
  795. self.timeSigLabel.setMaximumWidth(100)
  796. self.timeSigBox = QComboBox()
  797. self.timeSigBox.setEditable(True)
  798. self.timeSigBox.setMaximumWidth(100)
  799. self.timeSigBox.addItems(
  800. ('1/4', '2/4', '3/4', '4/4', '5/4', '6/4', '12/8'))
  801. self.timeSigBox.setCurrentIndex(3)
  802. self.measureLabel = QLabel('measures')
  803. self.measureLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter)
  804. self.measureLabel.setMaximumWidth(100)
  805. self.measureBox = QComboBox()
  806. self.measureBox.setMaximumWidth(100)
  807. self.measureBox.addItems(list(map(str, range(1,17))))
  808. self.measureBox.setCurrentIndex(3)
  809. self.defaultLengthLabel = QLabel('default length')
  810. self.defaultLengthLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter)
  811. self.defaultLengthLabel.setMaximumWidth(100)
  812. self.defaultLengthBox = QComboBox()
  813. self.defaultLengthBox.setEditable(True)
  814. self.defaultLengthBox.setMaximumWidth(100)
  815. self.defaultLengthBox.addItems(('1/16', '1/15', '1/12', '1/9', '1/8', '1/6', '1/4', '1/3', '1/2', '1'))
  816. self.defaultLengthBox.setCurrentIndex(4)
  817. self.quantizeLabel = QLabel('quantize')
  818. self.quantizeLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter)
  819. self.quantizeLabel.setMaximumWidth(100)
  820. self.quantizeBox = QComboBox()
  821. self.quantizeBox.setEditable(True)
  822. self.quantizeBox.setMaximumWidth(100)
  823. self.quantizeBox.addItems(('0', '1/16', '1/15', '1/12', '1/9', '1/8', '1/6', '1/4', '1/3', '1/2', '1'))
  824. self.quantizeBox.setCurrentIndex(5)
  825. self.hSlider = QSlider(Qt.Horizontal)
  826. self.hSlider.setTracking(True)
  827. #hSlider.setMaximum(1920*6*3*4)
  828. self.vSlider = QSlider(Qt.Vertical)
  829. self.vSlider.setTracking(True)
  830. self.vSlider.setInvertedAppearance(True)
  831. self.vSlider.setMaximumHeight(500)
  832. self.modeIndicator = ModeIndicator()
  833. self.timeSigBox.currentIndexChanged[str].connect(self.piano.setTimeSig)
  834. self.measureBox.currentIndexChanged[str].connect(self.piano.setMeasures)
  835. self.defaultLengthBox.currentIndexChanged[str].connect(self.piano.setDefaultLength)
  836. self.quantizeBox.currentIndexChanged[str].connect(self.piano.setGridDiv)
  837. self.hSlider.valueChanged.connect(self.view.setZoomX)
  838. self.vSlider.valueChanged.connect(self.view.setZoomY)
  839. self.hBox = QHBoxLayout()
  840. self.hBox.addWidget(self.modeIndicator)
  841. self.hBox.addWidget(self.timeSigLabel)
  842. self.hBox.addWidget(self.timeSigBox)
  843. self.hBox.addWidget(self.measureLabel)
  844. self.hBox.addWidget(self.measureBox)
  845. self.hBox.addWidget(self.defaultLengthLabel)
  846. self.hBox.addWidget(self.defaultLengthBox)
  847. self.hBox.addWidget(self.quantizeLabel)
  848. self.hBox.addWidget(self.quantizeBox)
  849. self.hBox.addWidget(self.hSlider)
  850. self.viewBox = QHBoxLayout()
  851. self.viewBox.addWidget(self.vSlider)
  852. self.viewBox.addWidget(self.view)
  853. self.layout = QVBoxLayout()
  854. self.layout.addLayout(self.hBox)
  855. self.layout.addLayout(self.viewBox)
  856. self.setLayout(self.layout)
  857. self.view.setFocus()
  858. # -------------------------------------------------------------------
  859. # DSP Callbacks
  860. def dspParameterChanged(self, index, value):
  861. pass
  862. def dspStateChanged(self, key, value):
  863. pass
  864. # -------------------------------------------------------------------
  865. # ExternalUI Callbacks
  866. def uiShow(self):
  867. self.show()
  868. def uiFocus(self):
  869. self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
  870. self.show()
  871. self.raise_()
  872. self.activateWindow()
  873. def uiHide(self):
  874. self.hide()
  875. def uiQuit(self):
  876. self.closeExternalUI()
  877. self.close()
  878. app.quit()
  879. def uiTitleChanged(self, uiTitle):
  880. self.setWindowTitle(uiTitle)
  881. # -------------------------------------------------------------------
  882. # Qt events
  883. def timerEvent(self, event):
  884. if event.timerId() == self.fIdleTimer:
  885. self.idleExternalUI()
  886. QGraphicsView.timerEvent(self, event)
  887. def closeEvent(self, event):
  888. self.closeExternalUI()
  889. QGraphicsView.closeEvent(self, event)
  890. # -------------------------------------------------------------------
  891. # Custom callback
  892. def updateMeasureBox(self, index):
  893. self.measureBox.setCurrentIndex(index-1)
  894. def sendMsg(self, data):
  895. msg = data[0]
  896. if msg == "midievent-remove":
  897. note, start, length, vel = data[1:5]
  898. note_start = start * 60. / self.fTransportInfo["bpm"] * 4. / self.fTransportInfo["sigDenom"] * self.getSampleRate()
  899. note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.getSampleRate()
  900. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  901. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  902. elif msg == "midievent-add":
  903. note, start, length, vel = data[1:5]
  904. note_start = start * 60. / self.fTransportInfo["bpm"] * self.getSampleRate()
  905. note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.getSampleRate()
  906. self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
  907. self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])
  908. def msgCallback(self, msg):
  909. #try:
  910. self.msgCallback2(msg)
  911. #except:
  912. #print("Custom msgCallback error, skipped for", msg)
  913. def msgCallback2(self, msg):
  914. msg = charPtrToString(msg)
  915. if msg == "midi-clear-all":
  916. # clear all notes
  917. self.piano.clearNotes()
  918. elif msg == "midievent-add":
  919. # adds single midi event
  920. time = int(self.readlineblock())
  921. size = int(self.readlineblock())
  922. data = []
  923. for x in range(size):
  924. data.append(int(self.readlineblock()))
  925. self.handleMidiEvent(time, size, data)
  926. elif msg == "transport":
  927. playing = bool(self.readlineblock() == "true")
  928. frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
  929. bpm, sigNum, sigDenom = [float(i) for i in self.readlineblock().split(":")]
  930. if beat != self.fTransportInfo["beat"]:
  931. print(beat)
  932. self.fTransportInfo = {
  933. "playing": playing,
  934. "frame": frame,
  935. "bar": bar,
  936. "beat": beat,
  937. "tick": tick,
  938. "bpm": bpm,
  939. "sigNum": sigNum,
  940. "sigDenom": sigDenom
  941. }
  942. #self.piano.movePlayHead(self.fTransportInfo)
  943. elif msg == "show":
  944. self.uiShow()
  945. elif msg == "focus":
  946. self.uiFocus()
  947. elif msg == "hide":
  948. self.uiHide()
  949. elif msg == "quit":
  950. self.fQuitReceived = True
  951. self.uiQuit()
  952. elif msg == "uiTitle":
  953. uiTitle = self.readlineblock()
  954. self.uiTitleChanged(uiTitle)
  955. else:
  956. print("unknown message: \"" + msg + "\"")
  957. # -------------------------------------------------------------------
  958. # Internal stuff
  959. def handleMidiEvent(self, time, size, data):
  960. #print("Got MIDI Event on UI", time, size, data)
  961. # NOTE: for now time comes in frames, which might not be desirable
  962. # we'll convert it to a smaller value for now (seconds)
  963. # later on we can have time as PPQ or similar
  964. time /= self.getSampleRate()
  965. status = MIDI_GET_STATUS_FROM_DATA(data)
  966. channel = MIDI_GET_CHANNEL_FROM_DATA(data)
  967. if status == MIDI_STATUS_NOTE_ON:
  968. note = data[1]
  969. velo = data[2]
  970. # append (channel, note, velo, time) for later
  971. self.fPendingNoteOns.append((channel, note, velo, time))
  972. elif status == MIDI_STATUS_NOTE_OFF:
  973. note = data[1]
  974. velo = data[2]
  975. # find previous note-on that matches this note and channel
  976. for noteOnMsg in self.fPendingNoteOns:
  977. channel_, note_, velo_, time_ = noteOnMsg
  978. if channel_ != channel:
  979. continue
  980. if note_ != note:
  981. continue
  982. # found it
  983. #print("{} {} {} {}\n".format(note, time_, time-time_, velo_))
  984. start = time_ / 60. * self.fTransportInfo["bpm"] / 4. * self.fTransportInfo["sigDenom"]
  985. length = (time - time_) / 60. * self.fTransportInfo["bpm"] / 4. / self.fTransportInfo["sigNum"] * self.fTransportInfo["sigDenom"]
  986. self.piano.drawNote(note, start, length, velo_)
  987. # remove from list
  988. self.fPendingNoteOns.remove(noteOnMsg)
  989. break
  990. #--------------- main ------------------
  991. if __name__ == '__main__':
  992. import resources_rc
  993. pathBinaries, pathResources = getPaths()
  994. gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
  995. gCarla.utils.set_process_name("MidiSequencer")
  996. app = CarlaApplication("MidiSequencer")
  997. gui = MainWindow()
  998. app.exit_exec()