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
32
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
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
107 self.connect("destroy", self.destroy)
108
109
110
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
116 hbox = gtk.HBox()
117 vbox = gtk.VBox()
118 hbox.pack_end(vbox, fill=hfill)
119 self.add(hbox)
120
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
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
141 self.add_all_cells()
142
143
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
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
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
168 self.cursor = (0,0)
169 self.set_cursor((0,0))
170
171
172 self.create_buffer_cells()
173
181
183 if self._fullscreen:
184 self.unfullscreen()
185 self._fullscreen = False
186 else:
187 self.fullscreen()
188 self._fullscreen = True
189
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
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
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
221 left = x - (width / 2)
222 bottom = y - (height / 2)
223
224 shift_x = self.left - left
225 shift_y = self.bottom - bottom
226 self.shift_grid(shift_x, shift_y)
227
241
243
244 for cell in self.grid:
245 self.grid.remove(cell)
246
248
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
263
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
275 keyname = gtk.gdk.keyval_name(event.keyval)
276 control = event.state & gtk.gdk.CONTROL_MASK
277 if keyname == "F11":
278
279 self.toggle_fullscreen()
280 elif keyname == "x":
281
282 self.all_notes_off()
283 elif keyname == "q":
284
285 print "Exiting"
286 gtk.main_quit()
287 elif keyname == "a":
288 self.clear_selection()
289 elif keyname == "c":
290
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
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
330 playing_cells = copy.copy(self.playing_cells)
331 for cell in playing_cells:
332 cell.all_off()
333
335 selection = copy.copy(self.selected_cells)
336 for cell in selection:
337 cell.deselect()
338
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
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
351 for row in range(start_row, self.top+1, 3):
352
353 if (col,row) != (coord[0],coord[1]):
354
355 self.get_cell(col, row).select()
356
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
368
369 time = event.time
370 self.chord_menu.popup(None, None, None, 0, time)
371 return True
372
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
384 return callback(cell, event)
385 return False
386
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
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
408 def _callback(cell, octave, item):
409
410
411 if self.chords_replace:
412 self.all_notes_off()
413 root = (cell.coord[0], cell.coord[1], octave)
414
415 notes = self.chord_types[chord_name]
416
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
421 for note in notes:
422 self.play_note(note)
423
424 self._point_select_callback = None
425 return True
426 self._point_select_callback = _callback
427
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
438 if z not in cell.playing:
439 cell.toggle_note(z)
440
442 """
443 Set the 2D coord to be under the current cursor. By default,
444 relative to the previous.
445
446 """
447
448 if not absolute:
449 coord = [self.cursor[0]+coord[0], self.cursor[1]+coord[1]]
450
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
460 old_cell = self.cursor_cell.unset_cursor()
461
462 self.cursor = tuple(coord)
463 self.cursor_cell.set_cursor()
464
465 @property
468
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
489 self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("black"))
490
491
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
501 self.label = gtk.Label()
502
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
509 self.playing_box = gtk.HBox()
510 self.vbox.pack_end(self.playing_box, expand=False)
511
512
513 self.playing = []
514 self.playing_labels = {}
515
516
517
518 octave = int(math.floor(math.log(1.5**coord[0] * 1.25**coord[1], 2)))
519
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
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
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
558
560 """Start or stop a note playing"""
561
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
570 if len(self.playing) == 0:
571 self.parent_window.playing_cells.remove(self)
572 else:
573
574
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
579 self.playing.append(octave)
580 if self.parent_window.equal_temperament:
581
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
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
599
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
608 """Removes this cell from the current selection."""
609 self._selected = False
610 self.parent_window.selected_cells.remove(self)
611
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
619 if self._selected:
620 self.deselect()
621 else:
622 self.select()
623
625 """Display the cursor on this cell"""
626 self.white_box.set_border_width(3)
627
629 self.white_box.set_border_width(1)
630
635
637 """
638 Refreshes the display of which notes are playing.
639
640 """
641
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
647 new_playing = set(self.playing) - set(self.playing_labels.keys())
648 for octave in new_playing:
649
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
656 self.playing_box.add(eb)
657
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
667 self.playing_labels[octave] = eb
668
669 if not self._selected:
670 if len(self.playing) == 0:
671
672 self.white_box.modify_bg(gtk.STATE_NORMAL, self.unselected_color)
673 else:
674
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):
690