Package jazzparser :: Package harmonical :: Module tsgui
[hide private]
[frames] | no frames]

Source Code for Module jazzparser.harmonical.tsgui

  1  from __future__ import absolute_import 
  2  """Tonal space GUI interface. 
  3   
  4  Super-cool awesome. 
  5   
  6  """ 
  7  """ 
  8  ============================== License ======================================== 
  9   Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding 
 10    
 11   This file is part of The Jazz Parser. 
 12    
 13   The Jazz Parser is free software: you can redistribute it and/or modify 
 14   it under the terms of the GNU General Public License as published by 
 15   the Free Software Foundation, either version 3 of the License, or 
 16   (at your option) any later version. 
 17    
 18   The Jazz Parser is distributed in the hope that it will be useful, 
 19   but WITHOUT ANY WARRANTY; without even the implied warranty of 
 20   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 21   GNU General Public License for more details. 
 22    
 23   You should have received a copy of the GNU General Public License 
 24   along with The Jazz Parser.  If not, see <http://www.gnu.org/licenses/>. 
 25   
 26  ============================ End license ====================================== 
 27   
 28  """ 
 29  __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"  
 30   
 31  # Check we've got the right version of PyGtk 
 32  # This check is made when this module first gets loaded 
 33  import pygtk 
 34  pygtk.require('2.0') 
 35   
 36  import gtk, sys, gobject, math, copy 
 37  from jazzparser.utils.gtk import get_text_from_dialog 
 38  from midi.sequencer_pygame import RealtimeSequencer 
 39  from midi import ProgramChangeEvent, single_note_tuning_event 
 40   
 41  from jazzparser.utils.tonalspace import coordinate_to_alpha_name_c 
 42  from jazzparser.harmonical.midi import tonal_space_note_events 
43 44 -class TonalSpaceWindow(gtk.Window):
45 """ 46 A window that displays an interactive tonal space. 47 48 """ 49 HELP = """\ 50 The following keyboard commands are available from the GUI: 51 F11: toggle fullscreen 52 q: quit 53 x: all notes off 54 a: clear selection 55 c: select a chord to play 56 r: toggle chord replace mode (chords replace all playing notes instead of 57 just adding to them) 58 e: toggle equal temperament. All notes played after this will be tuned 59 to equal temperament. Notes already playing will not be retuned 60 until they're replayed. 61 Ctrl+e: select all equal-temperament equivalents of the current selection. 62 """ 63
64 - def __init__(self, left, bottom, right, top, sequencer, *args, **kwargs):
65 """ 66 @type left: int 67 @param left: leftmost column to include 68 @type right: int 69 @param right: rightmost column 70 @type bottom: int 71 @param bottom: lowest row 72 @type top: int 73 @param top: highest row 74 @type sequencer: L{midi.sequencer_pygame.RealtimeSequencer} 75 @param sequencer: sequencer to use for playing notes 76 @type font_size: int or float 77 @kwarg font_size: size in points of the font used to draw the cell labels 78 @type chord_types: dict of coordinate lists (3-tuples) 79 @kwarg chord_types: chord types to allow the user to add 80 @type chord_type_order: list of strings 81 @kwarg chord_type_order: ordered list giving the order of the chord types 82 (optional) 83 @type vfill: bool 84 @kwarg vfill: expand the tonal space to fill the vertical size of the 85 window. Default: True. 86 @type hfill: bool 87 @kwarg hfill: expand the tonal space to fill the horizontal size of 88 the window. Default: True. 89 90 """ 91 font_size = kwargs.pop('font_size', 14) 92 self.font_size = font_size 93 self.chord_types = kwargs.pop('chord_types', {}) 94 self.chord_type_order = kwargs.pop('chord_type_order', None) 95 vfill = kwargs.pop('vfill', True) 96 hfill = kwargs.pop('hfill', True) 97 super(TonalSpaceWindow, self).__init__(gtk.WINDOW_TOPLEVEL, *args, **kwargs) 98 99 self.sequencer = sequencer 100 101 self.left = left 102 self.right = right 103 self.bottom = bottom 104 self.top = top 105 106 # Connect the basic event handlers 107 self.connect("destroy", self.destroy) 108 109 ####### Window furniture 110 # Set up the appearance of the window 111 DEFAULT_CELL_SIZE = 60 112 self.set_default_size(DEFAULT_CELL_SIZE*(right-left), DEFAULT_CELL_SIZE*(top-bottom)) 113 self.set_title("Tonal Space") 114 self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("black")) 115 # A box to put all the widgets in 116 hbox = gtk.HBox() 117 vbox = gtk.VBox() 118 hbox.pack_end(vbox, fill=hfill) 119 self.add(hbox) 120 # Bind keypress events to the window 121 self.connect('key_press_event', self.on_key_press) 122 123 self._fullscreen = False 124 self.playing_cells = set() 125 self.selected_cells = set() 126 self.playing_midi_notes = {} 127 128 self._cell_select_callback = None 129 self._point_select_callback = None 130 131 self.chords_replace = False 132 self.equal_temperament = False 133 134 # Create a tonal space grid 135 grid = gtk.Table(top-bottom+1, right-left+1, homogeneous=True) 136 self.grid = grid 137 vbox.pack_end(grid, fill=vfill) 138 139 self.cells = {} 140 # Add the cells to the layout grid 141 self.add_all_cells() 142 143 # Create the chords menu 144 self.chord_menu = gtk.Menu() 145 if self.chord_type_order is None: 146 self.chord_type_order = list(sorted(self.chord_types.keys())) 147 if len(self.chord_type_order) > 0: 148 for chord in self.chord_type_order: 149 crd_item = gtk.MenuItem(label=chord) 150 crd_item.show() 151 # Define a closure to respond to the menu item selection 152 def _get_chord_clicked(crd): 153 def _chord_clicked(item): 154 self.play_chord(crd)
155 return _chord_clicked
156 crd_item.connect('activate', _get_chord_clicked(chord)) 157 self.chord_menu.append(crd_item) 158 else: 159 # No items to add 160 dummy_item = gtk.MenuItem(label="No chord vocabularly loaded") 161 dummy_item.set_sensitive(False) 162 dummy_item.show() 163 self.chord_menu.append(dummy_item) 164 165 self.show_all() 166 167 # Set the current cursor to the centre 168 self.cursor = (0,0) 169 self.set_cursor((0,0)) 170 171 # Create some extra cells 172 self.create_buffer_cells() 173
174 - def get_cell_label(self, col, row):
175 name = coordinate_to_alpha_name_c((col,row), 176 sharp = u"\u266F", 177 flat = u"\u266D", 178 plus = "<sup>+</sup>", 179 minus = "<sup>-</sup>") 180 return name
181
182 - def toggle_fullscreen(self):
183 if self._fullscreen: 184 self.unfullscreen() 185 self._fullscreen = False 186 else: 187 self.fullscreen() 188 self._fullscreen = True
189
190 - def get_cell(self, x, y):
191 """ 192 Returns the cell for the given 2D tonal space coordinate. Returns 193 None if this cell isn't in the visible region. 194 195 """ 196 if x <= self.right and x >= self.left and \ 197 y <= self.top and y >= self.bottom: 198 return self.cells[(x,y)] 199 else: 200 return None
201
202 - def get_point(self, x, y):
203 """ 204 Returns the cell corresponding to the 2D TS coordinate, whether or 205 not it's in the visible region, creating it if necessary. 206 207 """ 208 if (x,y) not in self.cells: 209 self.cells[(x,y)] = self.create_cell(x, y) 210 return self.cells[(x,y)]
211
212 - def center_grid(self, x, y):
213 """ 214 Put the coordinate (x,y) in the centre of the visible grid by shifting 215 the portion visible. 216 217 """ 218 width = self.right - self.left + 1 219 height = self.top - self.bottom + 1 220 # Work out the point we want in the bottom left to put (x,y) central 221 left = x - (width / 2) 222 bottom = y - (height / 2) 223 # Work out how much to shift the space by 224 shift_x = self.left - left 225 shift_y = self.bottom - bottom 226 self.shift_grid(shift_x, shift_y)
227
228 - def shift_grid(self, x, y):
229 """ 230 Shift the visible portion of the tonal space covered by the grid. 231 232 """ 233 self.remove_all_cells() 234 self.left -= x 235 self.right -= x 236 self.top -= y 237 self.bottom -= y 238 self.add_all_cells() 239 # Create a border of extra prepared cells 240 self.create_buffer_cells()
241
242 - def remove_all_cells(self):
243 # Remove all cells in the layout grid 244 for cell in self.grid: 245 self.grid.remove(cell)
246
247 - def add_all_cells(self):
248 # Add all the cells in the cell matrix 249 rows = self.top - self.bottom + 1 250 for row,y in enumerate(range(self.bottom, self.top+1)): 251 for col,x in enumerate(range(self.left, self.right+1)): 252 self.grid.attach(self.get_point(x,y), 253 col, col+1, rows-row-1, rows-row, 254 xoptions=(gtk.EXPAND|gtk.SHRINK|gtk.FILL), 255 yoptions=(gtk.EXPAND|gtk.SHRINK|gtk.FILL) )
256
257 - def create_cell(self, col, row):
258 return TonalSpaceCell(self.get_cell_label(col, row), 259 self.sequencer, 260 self, 261 coord=(col,row), 262 font_size=self.font_size)
263
264 - def create_buffer_cells(self):
265 """ 266 Create some extra cells around the edge while we're not doing 267 anything else, so it doesn't take time when we need them. 268 269 """ 270 for x in range(self.left-2, self.right+3): 271 for y in range(self.bottom-2, self.top+3): 272 self.get_point(x, y)
273
274 - def on_key_press(self, widget, event):
275 keyname = gtk.gdk.keyval_name(event.keyval) 276 control = event.state & gtk.gdk.CONTROL_MASK 277 if keyname == "F11": 278 # Toggle fullscreen when F11 is pressed 279 self.toggle_fullscreen() 280 elif keyname == "x": 281 # Cancel all playing notes 282 self.all_notes_off() 283 elif keyname == "q": 284 # Exit 285 print "Exiting" 286 gtk.main_quit() 287 elif keyname == "a": 288 self.clear_selection() 289 elif keyname == "c": 290 # Pop up the chord menu 291 self.show_chord_menu(event) 292 elif keyname == "r": 293 self.chords_replace = not self.chords_replace 294 print "Chord replace mode %s" % ("on" if self.chords_replace else "off") 295 elif control and keyname == "e": 296 self.add_et_equivs() 297 elif keyname == "e": 298 self.equal_temperament = not self.equal_temperament 299 if self.equal_temperament: 300 print "Equal temperament" 301 else: 302 print "True tonal space intonation" 303 elif keyname == "Right": 304 self.set_cursor((1,0)) 305 elif keyname == "Left": 306 self.set_cursor((-1,0)) 307 elif keyname == "Up": 308 self.set_cursor((0,1)) 309 elif keyname == "Down": 310 self.set_cursor((0,-1)) 311 elif keyname == "space": 312 # This isn't right, but I don't really care 313 pos = self.get_cell(*self.cursor).window.get_position() 314 def _get_positioner(x,y): 315 def _positioner(menu): 316 return x,y,False
317 return _positioner 318 self.get_cell(*self.cursor).menu.popup(None, None, _get_positioner(*pos), 0, event.time) 319 elif keyname == "F1": 320 self.center_grid(*self.cursor) 321 elif keyname == "Home": 322 self.center_grid(0,0) 323 self.set_cursor((0,0), absolute=True) 324
325 - def destroy(self, widget, data=None):
326 """Handler for window destruction.""" 327 gtk.main_quit()
328
329 - def all_notes_off(self):
330 playing_cells = copy.copy(self.playing_cells) 331 for cell in playing_cells: 332 cell.all_off()
333
334 - def clear_selection(self):
335 selection = copy.copy(self.selected_cells) 336 for cell in selection: 337 cell.deselect()
338
339 - def select_et_equivs(self, coord):
340 """ 341 Select all equal-temperament equivalents of the given coord (not 342 including the coord itself). 343 344 """ 345 start_col = self.left + (coord[0]-self.left) % 4 346 # Each fourth column 347 for col in range(start_col, self.right+1, 4): 348 spacex = int(math.floor(float(col-coord[0])/4)) 349 start_row = self.bottom + (coord[1]-self.bottom-spacex) % 3 350 # Each third row, shifted down one for each column right 351 for row in range(start_row, self.top+1, 3): 352 # Skip the coordinate itself 353 if (col,row) != (coord[0],coord[1]): 354 # Select this point 355 self.get_cell(col, row).select()
356
357 - def add_et_equivs(self):
358 """ 359 Add to the current selection all equal-temperament equivalents of the 360 notes currently selected. 361 362 """ 363 selection = copy.copy(self.selected_cells) 364 for cell in selection: 365 self.select_et_equivs(cell.coord)
366
367 - def show_chord_menu(self, event):
368 # Pop up the chord menu next to the mouse 369 time = event.time 370 self.chord_menu.popup(None, None, None, 0, time) 371 return True
372
373 - def cell_clicked(self, cell, event):
374 """ 375 Called by each cell when it gets clicked. If we return True here, 376 it means the cell shouldn't do its usual click actions, because we're 377 doing something that replaces them. Otherwise, the cell can go ahead 378 and do its usual thing. 379 380 """ 381 callback = getattr(self, '_cell_select_callback') 382 if callback is not None: 383 # Call the callback on this cell and event 384 return callback(cell, event) 385 return False
386
387 - def point_selected(self, cell, octave, item):
388 """ 389 Works like cell_clicked, but called when a 3D position (i.e., cell 390 plus octave) is selected. 391 392 """ 393 callback = getattr(self, '_point_select_callback') 394 if callback is not None: 395 return callback(cell, octave, item) 396 return False
397
398 - def play_chord(self, chord_name):
399 """ 400 Set the notes of a particular chord going. 401 402 The first thing this does is to wait for the user to select a chord 403 root. Then it positions the note cluster around the root and plays 404 those notes. 405 406 """ 407 # Set up a callback to happen when the user next clicks a cell 408 def _callback(cell, octave, item): 409 # If chords_replace is toggled, clear all playing notes before 410 # adding the new ones 411 if self.chords_replace: 412 self.all_notes_off() 413 root = (cell.coord[0], cell.coord[1], octave) 414 # Get the notes of this chord type 415 notes = self.chord_types[chord_name] 416 # Shift all the notes so the chord is rooted at the selected point 417 def _sum3d(c1, c2): 418 return (c1[0]+c2[0], c1[1]+c2[1], c1[2]+c2[2])
419 notes = [_sum3d(root, n) for n in notes] 420 # Set all the notes playing 421 for note in notes: 422 self.play_note(note) 423 # Don't do this next time a point is selected 424 self._point_select_callback = None 425 return True 426 self._point_select_callback = _callback 427
428 - def play_note(self, coord):
429 """ 430 Set the note playing corresponding to a 3D coordinate, if that 431 note is in the current portion of the tonal space. 432 433 """ 434 x,y,z = coord 435 cell = self.get_cell(x, y) 436 if cell is not None: 437 # Only start it playing if it's not already playing 438 if z not in cell.playing: 439 cell.toggle_note(z)
440
441 - def set_cursor(self, coord, absolute=False):
442 """ 443 Set the 2D coord to be under the current cursor. By default, 444 relative to the previous. 445 446 """ 447 # Calculate the relative cursor position 448 if not absolute: 449 coord = [self.cursor[0]+coord[0], self.cursor[1]+coord[1]] 450 # Check whether the coordinate is off the showing grid 451 if coord[0] < self.left: 452 coord[0] = self.left 453 elif coord[0] > self.right: 454 coord[0] = self.right 455 if coord[1] < self.bottom: 456 coord[1] = self.bottom 457 elif coord[1] > self.top: 458 coord[1] = self.top 459 # Undraw the old cursor 460 old_cell = self.cursor_cell.unset_cursor() 461 # Draw the new cursor marking 462 self.cursor = tuple(coord) 463 self.cursor_cell.set_cursor()
464 465 @property
466 - def cursor_cell(self):
467 return self.get_point(*self.cursor)
468
469 -class TonalSpaceCell(gtk.EventBox):
470 """ 471 Widget to draw a single point in the tonal space. 472 473 """
474 - def __init__(self, label, sequencer, parent, font_size=14.0, coord=(0,0)):
475 gtk.EventBox.__init__(self) 476 477 self.sequencer = sequencer 478 self.coord = coord 479 self.parent_window = parent 480 self._selected = False 481 482 self.unselected_color = gtk.gdk.color_parse("white") 483 self.unselected_text_color = gtk.gdk.color_parse("black") 484 self.selected_color = gtk.gdk.color_parse("#C45656") 485 self.selected_text_color = gtk.gdk.color_parse("white") 486 self.playing_color = gtk.gdk.color_parse("#C2D0E0") 487 488 # Set the background colour to black to get the border 489 self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("black")) 490 491 # Add another event box for the white area in the middle 492 self.white_box = gtk.EventBox() 493 self.white_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("white")) 494 self.white_box.set_border_width(1) 495 self.add(self.white_box) 496 497 self.vbox = gtk.VBox() 498 self.white_box.add(self.vbox) 499 500 # Add the text in a label 501 self.label = gtk.Label() 502 # Set the text size 503 font_size = int(font_size * 1000) 504 markup = u'<span size="%s">%s</span>' % (font_size, label) 505 self.label.set_markup(markup) 506 self.vbox.pack_start(self.label, expand=True) 507 508 # Display playing notes at the bottom 509 self.playing_box = gtk.HBox() 510 self.vbox.pack_end(self.playing_box, expand=False) 511 512 # Keep track of which notes (octaves of this coord) are playing 513 self.playing = [] 514 self.playing_labels = {} 515 516 # Prepare a menu to appear when we click on the cell 517 # Calculate what octave we're in relative to (0,0) 518 octave = int(math.floor(math.log(1.5**coord[0] * 1.25**coord[1], 2))) 519 # Make the choice of octaves be centred on the octave (0,0) is in 520 center = -1*octave 521 522 self.menu = gtk.Menu() 523 for i in range(center-2, center+3): 524 if i > 0: 525 label = "+%d" % i 526 else: 527 label = str(i) 528 menuitem = gtk.MenuItem(label=label) 529 menuitem.show() 530 # Respond to selecting this by toggling the note playing 531 def _get_note_toggler(octave): 532 def _note_toggler(widget): 533 finished = self.parent_window.point_selected(self, octave, widget) 534 if not finished: 535 self.toggle_note(octave) 536 return True
537 return _note_toggler
538 menuitem.connect("activate", _get_note_toggler(i)) 539 self.menu.append(menuitem) 540 541 # Respond to a click event 542 self.connect("button_press_event", self.button_press_event) 543 self.set_events(self.get_events() | gtk.gdk.BUTTON_PRESS_MASK) 544 self.show_all() 545
546 - def button_press_event(self, widget, event):
547 # Let the main window do what it likes 548 finished = self.parent_window.cell_clicked(self, event) 549 if not finished: 550 if event.button == 3: 551 # Right click 552 self.toggle_selected() 553 else: 554 # Pop up the menu next to the mouse 555 time = event.time 556 self.menu.popup(None, None, None, event.button, time) 557 return True
558
559 - def toggle_note(self, octave):
560 """Start or stop a note playing""" 561 # Get the events (note on and off) for this coordinate 562 tuning, note_on, note_off = tonal_space_note_events( 563 (self.coord[0], self.coord[1], octave), 564 0, 0) 565 if octave in self.playing: 566 self.playing.remove(octave) 567 del self.parent_window.playing_midi_notes[note_on.pitch] 568 self.sequencer.send_event(note_off) 569 # If no more notes playing, remove ourselves from the global playing record 570 if len(self.playing) == 0: 571 self.parent_window.playing_cells.remove(self) 572 else: 573 # If another instance of this ET note is playing, stop it, because 574 # it will get retuned by this tuning event :-( 575 if note_on.pitch in self.parent_window.playing_midi_notes: 576 oldcell,oldoctave = self.parent_window.playing_midi_notes[note_on.pitch] 577 oldcell.toggle_note(oldoctave) 578 # Start the note playing 579 self.playing.append(octave) 580 if self.parent_window.equal_temperament: 581 # Return the note to its default tuning (ET) 582 tuning = single_note_tuning_event( 583 [(note_on.pitch, note_on.pitch, 0)]) 584 self.sequencer.send_event(tuning) 585 self.sequencer.send_event(note_on) 586 # Note globally that we're playing 587 self.parent_window.playing_cells.add(self) 588 self.parent_window.playing_midi_notes[note_on.pitch] = (self, octave) 589 590 self.refresh_playing()
591
592 - def retune_note(self, octave):
593 """Retune the given note without playing it.""" 594 # Get the tuning event for this coordinate 595 tuning, note_on, note_off = tonal_space_note_events( 596 (self.coord[0], self.coord[1], octave), 597 0, 0) 598 self.sequencer.send_event(tuning)
599
600 - def select(self):
601 """Set this cell to be part of the current selection.""" 602 self._selected = True 603 self.parent_window.selected_cells.add(self) 604 self.white_box.modify_bg(gtk.STATE_NORMAL, self.selected_color) 605 self.label.modify_fg(gtk.STATE_NORMAL, self.selected_text_color)
606
607 - def deselect(self):
608 """Removes this cell from the current selection.""" 609 self._selected = False 610 self.parent_window.selected_cells.remove(self) 611 # Use the playing colour if a note is playing 612 if len(self.playing) > 0: 613 self.white_box.modify_bg(gtk.STATE_NORMAL, self.playing_color) 614 else: 615 self.white_box.modify_bg(gtk.STATE_NORMAL, self.unselected_color) 616 self.label.modify_fg(gtk.STATE_NORMAL, self.unselected_text_color)
617
618 - def toggle_selected(self):
619 if self._selected: 620 self.deselect() 621 else: 622 self.select()
623
624 - def set_cursor(self):
625 """Display the cursor on this cell""" 626 self.white_box.set_border_width(3)
627
628 - def unset_cursor(self):
629 self.white_box.set_border_width(1)
630
631 - def all_off(self):
632 """Cancel all playing notes.""" 633 for octave in copy.copy(self.playing): 634 self.toggle_note(octave)
635
636 - def refresh_playing(self):
637 """ 638 Refreshes the display of which notes are playing. 639 640 """ 641 # Remove notes no longer playing 642 not_playing = set(self.playing_labels.keys()) - set(self.playing) 643 for octave in not_playing: 644 self.playing_labels[octave].destroy() 645 del self.playing_labels[octave] 646 # Add notes playing but not displayed 647 new_playing = set(self.playing) - set(self.playing_labels.keys()) 648 for octave in new_playing: 649 # Put the label in an event box, so we can click on it 650 eb = gtk.EventBox() 651 eb.set_visible_window(False) 652 label = gtk.Label(str(octave)) 653 eb.add(label) 654 eb.add_events(gtk.gdk.BUTTON_PRESS_MASK) 655 # Add the label to the list 656 self.playing_box.add(eb) 657 # Define an action to trigger the note off when the label's clicked on 658 def _get_note_toggler(i): 659 def _note_toggler(widget, event): 660 self.toggle_note(i) 661 return True
662 return _note_toggler 663 eb.connect("button_press_event", _get_note_toggler(octave)) 664 eb.show() 665 label.show() 666 # Keep this so we can remove it later 667 self.playing_labels[octave] = eb 668 # Set the colour depending on whether we're playing 669 if not self._selected: 670 if len(self.playing) == 0: 671 # No note playing: set the normal unselected colour 672 self.white_box.modify_bg(gtk.STATE_NORMAL, self.unselected_color) 673 else: 674 # Something's playing: use the playing colour 675 self.white_box.modify_bg(gtk.STATE_NORMAL, self.playing_color) 676
677 -def create_window(left, bottom, right, top, device_id, tuner=False, **kwargs):
678 sequencer = RealtimeSequencer(device_id) 679 instr = kwargs.pop('instrument', None) 680 if instr is not None: 681 pc = ProgramChangeEvent() 682 pc.value = instr 683 sequencer.send_event(pc) 684 if tuner: 685 from .tstuner import TonalSpaceRetunerWindow 686 cls = TonalSpaceRetunerWindow 687 else: 688 cls = TonalSpaceWindow 689 return cls(left, bottom, right, top, sequencer, **kwargs)
690