]> git.donarmstrong.com Git - lilypond.git/blob - notation.py
* notation.py (Notation.change_duration_log): duration logs of all
[lilypond.git] / notation.py
1 import string
2 import re
3 import gnomecanvas
4 import gtk
5 import os
6 import socket
7 import music
8 import pango
9 import math
10 from rational import Rational
11
12 display_dpi = 75
13
14 def get_display_dpi():
15     str = os.popen ('xdpyinfo | grep "dots per inch"').read ()
16     m = re.match ('([0-9]+)x([0-9]+) dots')
17     if m:
18         display_dpi = (string.atoi (m.group (2)) + string.atoi (m.group (2)))/2
19
20
21 scale_alterations = [0, 0, -2, 0, 0,-2,-2]  
22
23 copy_lilypond_input = 1
24 time_sig = (4, 4)
25 measure_length = Rational (time_sig[0], time_sig[1])
26 measure_count = 4
27
28
29 try:
30     server = os.environ['IKEBANASERVER']
31 except KeyError:
32     server = 'localhost'
33
34 lilypond_input_log_file = open ("input.log", 'w')
35
36 def talk_to_lilypond (expression_str):
37     """Send a LISP expression to LilyPond, wait for return value."""
38     if copy_lilypond_input:
39         lilypond_input_log_file.write (expression_str)
40         lilypond_input_log_file.flush ()
41     
42     sock = socket.socket (socket.AF_INET)
43     address = (server, 2904)
44     sock.connect (address)
45     sock.send (expression_str, socket.MSG_WAITALL)
46
47     cont = 1
48     retval = ''
49     while  cont:
50         try:
51             (data, from_addr) = sock.recvfrom (1024)
52         except socket.error:
53             break
54         cont = len (data) > 0
55         retval += data
56  
57     return retval
58
59 def set_measure_number (str, num):
60     return """(make-music 'SequentialMusic 'elements (list
61       (context-spec-music
62        (make-property-set 'timeSignatureFraction (cons %d %d)) 'Score)
63       (context-spec-music
64        (make-property-set 'measureLength (ly:make-moment %d %d)) 'Score)
65       (context-spec-music
66        (make-property-set 'beatLength (ly:make-moment 1 %d)) 'Score)
67       (context-spec-music
68        (make-property-set 'currentBarNumber %d) 'Score)
69     
70     %s))""" % (time_sig[0], time_sig[1], time_sig[0],
71                time_sig[1], time_sig[1], num, str)
72
73 def render_score (filename, ly):
74     print ly
75     str =  '''
76 myNotes = %s
77 \\score  { \myNotes }
78 ''' % ly
79     open (filename, 'w').write (str)
80     base = os.path.splitext (filename)[0] + '.ps'
81     os.system ('(lilypond %s && gv %s)&  ' % (filename, base))
82
83 def add_start_skip (str, start_skip):
84     return """(make-music 'SequentialMusic 'elements
85                (list
86                 (make-music 'SkipMusic
87                             'duration (ly:make-duration 0 0 %d %d))
88                 %s))
89 """ % (start_skip.num, start_skip.den, str)
90     
91     
92
93 class Lilypond_socket_parser:
94     """Maintain state of reading multi-line lilypond output for socket transport."""
95     def __init__ (self, interpret_function):
96         self.clear ()
97         self.interpret_socket_line = interpret_function
98
99     def clear (self):
100         self.cause_tag = None
101         self.bbox = None
102         
103     def parse_line (self, line):
104         fields = string.split (line)
105         if not fields:
106             return
107         offset = (0,0)
108         if 0:
109             pass
110         elif fields[0] == 'hello':
111             print 'found server: ', fields[1:]
112             return
113         elif fields[0] == 'at':
114                 offset = (string.atof (fields[1]),
115                           string.atof (fields[2]))
116                 fields = fields[3:]
117         elif fields[0] == 'nocause':
118             self.cause_tag = None
119             self.bbox = None
120             return
121         elif fields[0] == 'cause':
122             self.cause_tag = string.atoi (fields[1])
123             self.name = fields[2][1:-1]
124             self.bbox = tuple (map (string.atof, fields[3:]))
125             return
126
127         return self.interpret_socket_line (offset, self.cause_tag,
128                                            self.bbox, self.name,
129                                            fields)
130
131 class Notation_controller:
132     """Couple Notation and the music model. Stub for now. """
133     def __init__ (self, music_document):
134         self.document = music_document
135         
136         self.notation = Notation (self)
137         self.parser = Lilypond_socket_parser (self.interpret_line)
138         self.start_moment = Rational (0)
139         self.stop_moment = Rational (3)
140
141     def interpret_line (self, offset, cause, bbox, name, fields):
142         notation_item = self.notation.add_item (offset, cause, bbox, fields)
143         notation_item.name = name
144         
145     def touch_document (self):
146         self.document.touched = True
147         
148     def update_notation(self):
149         doc = self.document
150         doc.recompute()
151         
152         expr = doc.music
153
154         def sub(x):
155                 ok = (x.start >= self.start_moment and
156                       x.start + x.length() <= self.stop_moment)
157                 return ok
158
159         def sub2 (x):
160             return x.name() in ('RestEvent','NoteEvent') and  sub(x)
161
162         start_note = expr.find_first (sub2)
163
164         start_skip = start_note.start - start_note.start.floor()
165         
166         str = expr.lisp_sub_expression (sub)
167         str = add_start_skip (str, start_skip)
168         
169         bar_count = (self.start_moment / measure_length).floor()
170         str = set_measure_number (str, bar_count.num)
171         str = talk_to_lilypond (str)
172         self.parse_socket_file (str)
173
174     def ensure_visible (self, when):
175         new_start =  max ((when - measure_length).floor(), Rational(0))
176         new_stop = new_start + Rational (measure_count) * measure_length
177
178         if new_start <> self.start_moment or new_stop <> self.stop_moment:
179 #            print "render interval", new_start, new_stop
180             self.touch_document()
181             
182         self.start_moment = new_start
183         self.stop_moment = new_stop
184
185     def parse_socket_file (self, str):
186         self.notation.clear ()
187         lines = string.split (str, '\n')
188         self.parse_lines (lines)
189         self.notation.touched = True
190         self.document.touched = False
191         
192     def parse_lines (self, lines):
193         for l in lines:
194             self.parser.parse_line (l)
195
196 class Notation_item:
197     """A single notation element (corresponds to a Grob in LilyPond)"""
198     def __init__ (self):
199         self.origin_tag = None
200         self.bbox = None
201         self.offset = (0,0)
202         self.tag = None
203         self.name = ''
204         self.args = []
205         self.canvas_item = None
206         self.music_expression = None
207         
208 class Notation:
209     """A complete line/system/page of LilyPond output. Consists of a
210     number of Notation_items"""
211     
212     def __init__ (self, controller):
213         self.items = []
214         self.notation_controller = controller
215         self.touched = True
216         self.cursor_touched = True
217         self.cursor_callback = None
218         
219         toplevel = controller.document.music
220         self.music_cursor = toplevel.find_first (lambda x: x.name()== "NoteEvent") 
221         
222     def get_document (self):
223         return self.notation_controller.document
224         
225     def add_item (self, offset, cause, bbox, fields):
226             item = Notation_item()
227             item.tag = fields[0]
228             item.args = map (eval, fields[1:])
229             item.offset = offset
230             item.origin_tag = cause
231             
232             if cause and cause >= 0:
233                 item.music_expression = self.get_document ().tag_dict[cause]
234                 
235             item.bbox = bbox
236             
237             self.items.append (item)
238             return item
239         
240     def clear(self):
241         self.items = [] 
242             
243     def paint_on_canvas (self,  canvas):
244         for w in  canvas.root().item_list:
245             if w.notation_item:
246                 w.destroy()
247                 
248         for i in self.items:
249             c_item = canvas.create_canvas_item (i)
250
251         canvas.set_cursor_to_music (self.music_cursor)
252         self.touched = False
253
254     def set_cursor (self, music_expr):
255         if music_expr <> self.music_cursor:
256             self.music_cursor = music_expr
257             self.cursor_touched = True
258             self.ensure_cursor_visible ()
259             proc = self.set_cursor_callback
260             if proc:
261                 proc (self)
262         
263     def cursor_move (self, dir):
264         mus = self.music_cursor
265         if mus.parent.name() == 'EventChord':
266             mus = mus.parent
267         
268         mus = mus.parent.get_neighbor (mus, dir)
269         mus = mus.find_first (lambda x: x.name() in ('NoteEvent', 'RestEvent'))
270         self.set_cursor (mus)
271
272     def cursor_move_chord (self, dir):
273         mus = self.music_cursor
274         if mus.name ()=='NoteEvent':
275             current_steps  = mus.pitch.steps()
276             other_steps = [(note, note.pitch.steps()) for
277                            note in mus.parent.elements if note.name()=='NoteEvent']
278
279             def cmp(a,b):
280                 if a[1] > b[1]:
281                     return 1
282                 if a[1] < b[1]:
283                     return -1
284                 return 0
285
286             bound_set = [(note, dir * step) for (note, step) in other_steps
287                          if dir * (step - current_steps) > 0]
288             bound_set.sort (cmp)
289             if bound_set:
290                 self.set_cursor (bound_set[0][0])
291               
292     def insert_at_cursor (self, music, dir):
293         mus = self.music_cursor
294         if mus.parent.name() == 'EventChord':
295             mus = mus.parent
296
297         mus.parent.insert_around (mus, music, dir)
298         self.touch_document()
299
300     def touch_document (self):
301         self.get_document ().touched = True
302
303     def check_update (self):
304         if self.get_document().touched:
305             self.notation_controller.update_notation ()
306         
307     def backspace (self):
308         mus = self.music_cursor
309         if (mus.parent.name() == 'EventChord'
310             and len (mus.parent.elements) <= 1):
311             
312             mus = mus.parent
313
314         neighbor = mus.parent.get_neighbor (mus, -1)
315         mus.parent.delete_element (neighbor)
316         self.touch_document ()
317         
318     def change_octave (self, dir):
319         if self.music_cursor.name() == 'NoteEvent':
320             p = self.music_cursor.pitch
321             p.octave += dir 
322         self.touch_document ()
323
324     def set_step (self, step):
325         self.ensure_note ()
326         if self.music_cursor.name() == 'NoteEvent':
327
328             # relative mode.
329             p = self.music_cursor.pitch
330             p1 = p.copy()
331             p1.step = step
332             p1.alteration = scale_alterations [step]
333             
334             orig_steps = p.steps ()
335             new_steps = p1.steps ()
336             diff = new_steps - orig_steps
337             if diff >= 4:
338                     p1.octave -= 1
339             elif diff <= -4:
340                     p1.octave += 1
341                     
342             self.music_cursor.pitch = p1
343             self.touch_document ()
344
345         else:
346             print 'not a NoteEvent'
347
348     def add_step (self, step):
349         self.ensure_note ()
350         if self.music_cursor.name() == 'NoteEvent':
351
352             # relative mode.
353             p = self.music_cursor.pitch
354             p1 = p.copy()
355             p1.step = step
356             
357             orig_steps = p.steps ()
358             new_steps = p1.steps ()
359             diff = new_steps - orig_steps
360             if diff >= 4:
361                     p1.octave -= 1
362             elif diff <= -4:
363                     p1.octave += 1
364
365             new_ev = music.NoteEvent()
366             new_ev.pitch = p1
367             new_ev.duration = self.music_cursor.duration.copy()
368
369             self.music_cursor.parent.insert_around (self.music_cursor,
370                                                     new_ev, 1)
371             self.music_cursor = new_ev
372             self.touch_document ()
373         else:
374             print 'not a NoteEvent'
375
376     def change_step (self, dstep):
377         self.ensure_note ()
378         if self.music_cursor.name() == 'NoteEvent':
379
380             # relative mode.
381             p = self.music_cursor.pitch
382             p1 = p.copy()
383             p1.step += dstep
384
385             if p1.step > 6:
386                 p1.step -= 7
387                 p1.octave += 1
388             elif p1.step < 0:
389                 p1.step += 7
390                 p1.octave -= 1
391
392             p1.alteration = scale_alterations [p1.step]
393                 
394             self.music_cursor.pitch = p1
395             self.touch_document ()
396
397         else:
398             print 'not a NoteEvent'
399             
400     def change_duration_log (self, dir):
401         if ( self.music_cursor.name() == 'NoteEvent'
402              or self.music_cursor.name() == 'RestEvent'):
403
404             m = self.music_cursor
405             dur = self.music_cursor.duration
406             dl = dur.duration_log
407             dl += dir
408             if dl > 6 and dl < -2:
409                 return None
410
411             evs = [x for x in m.parent.elements if x.name() in ('NoteEvent', 'RestEvent')]
412             for e in evs:
413                 e.duration.duration_log = dl
414                 
415             self.touch_document ()
416
417     def ensure_note (self):
418         if self.music_cursor.name() == 'RestEvent':
419             m = self.music_cursor
420             note = music.NoteEvent()
421             m.parent.insert_around (None, note, 1)
422             m.parent.delete_element (m)
423             self.music_cursor = note
424             self.touch_document ()
425             
426     def ensure_rest (self):
427         if self.music_cursor.name() == 'NoteEvent':
428             m = self.music_cursor
429             rest = music.RestEvent()
430             m.parent.insert_around (None, rest, 1)
431             m.parent.delete_element (m)
432             self.music_cursor = rest  
433             self.touch_document ()
434             
435     def change_dots (self):
436         if self.music_cursor.name() == 'NoteEvent':
437             p = self.music_cursor.duration
438             if p.dots == 1:
439                 p.dots = 0
440             elif p.dots == 0:
441                 p.dots = 1
442             self.touch_document ()
443             
444     def ensure_cursor_visible(self):
445         self.notation_controller.document.recompute()
446         self.notation_controller.ensure_visible (self.music_cursor.start)
447
448     def change_alteration (self, dir):
449         if self.music_cursor.name() == 'NoteEvent':
450             p = self.music_cursor.pitch
451
452             new_alt = p.alteration + dir
453             if abs (new_alt) <= 4: 
454                 p.alteration = new_alt
455             self.touch_document ()
456
457     def set_alteration (self, alter):
458         if self.music_cursor.name() == 'NoteEvent':
459             p = self.music_cursor.pitch
460             p.alteration = alter
461             self.touch_document ()
462
463     def toggle_arpeggio (self):
464         par = self.music_cursor.parent
465         if par.name()== "EventChord":
466             arps = [e for e in par.elements if e.name()=='ArpeggioEvent']
467             if arps:
468                 par.delete_element (arps[0])
469             else:
470                 arp = music.ArpeggioEvent()
471                 par.insert_around (self.music_cursor, arp, -1)
472             self.touch_document()
473             
474     def print_score(self):
475         doc = self.notation_controller.document
476         ly = doc.music.ly_expression()
477         render_score('score.ly', ly) 
478
479     def add_note (self):
480         if self.music_cursor.name () == 'NoteEvent':
481             note = music.NoteEvent ()
482             note.pitch = self.music_cursor.pitch.copy()
483             note.duration = self.music_cursor.duration.copy()
484             
485             ch = music.EventChord ()
486             ch.insert_around (None, note, 0)
487             
488             self.insert_at_cursor (ch, 1)
489             self.cursor_move (1)
490             self.touch_document ()
491
492         elif self.music_cursor.name () == 'RestEvent':
493             rest = music.RestEvent ()
494             rest.duration = self.music_cursor.duration.copy()
495             
496             ch = music.EventChord ()
497             ch.insert_around (None, rest, 0)
498             
499             self.insert_at_cursor (ch, 1)
500             self.cursor_move (1)
501             self.touch_document ()
502             
503