--- /dev/null
+
+
+NAME=ikebana
+VERSION=0.0
+PY_FILES=$(wildcard *.py)
+ALL_FILES=$(PY_FILES) GNUmakefile server.ly README
+
+DISTNAME=$(NAME)-$(VERSION)
+
+dist:
+ mkdir $(DISTNAME)
+ ln $(ALL_FILES) $(DISTNAME)/
+ tar cfz $(DISTNAME).tar.gz $(DISTNAME)/
+ rm -rf $(DISTNAME)
+
+
+
+
+
--- /dev/null
+
+This is Ikebana, a simple GUI for entering LilyPond music.
+
+USAGE:
+
+ lilypond -b socket server.ly &
+ python ikebana.py
+
+KEYS
+
+a..g pitch
+, ' octave
+r rest
+space enter note
+left,right move
+. dot
+* / duration longer/shorter
++ - alteration up/down
+backspace delete note
+
+
+
+CAVEAT
+
+This is a proof-of-concept demo. Don't expect features, support, maintenance.
--- /dev/null
+#!/usr/bin/env python
+
+import os
+import gtk
+import gnomecanvas
+import random
+import string
+import notation
+import notationcanvas
+import music
+
+def mainquit(*args):
+ gtk.main_quit()
+
+
+
+class NotationApplication:
+ def __init__ (self):
+ self.music = music.Music_document()
+
+ nc = notation.Notation_controller (self.music)
+ self.notation_controller = nc
+
+ ncc = notationcanvas.Notation_canvas_controller(nc.notation)
+ self.notation_canvas_controller = ncc
+
+ self.window = self.create_window()
+
+ def create_window (self):
+ win = gtk.Window()
+ win.connect('destroy', mainquit)
+ win.set_title('Ikebana - visual music notation')
+
+ canvas = self.notation_canvas_controller.canvas
+ canvas.show()
+ win.add (canvas)
+ win.show()
+
+ return win
+
+ def main (self):
+ self.notation_controller.update_notation ()
+ self.notation_canvas_controller.update_canvas ()
+
+if __name__ == '__main__':
+ c = NotationApplication()
+ c.main ()
+ gtk.main()
--- /dev/null
+import string
+
+
+class Duration:
+ def __init__ (self):
+ self.duration_log = 2
+ self.dots = 0
+ self.factor = (1,1)
+
+ def lisp_expression (self):
+ return '(ly:make-duration %d %d %d %d)' % (self.duration_log,
+ self.dots,
+ self.factor[0],
+ self.factor[1])
+
+ def ly_expression (self):
+ str = '%d%s' % (1 << self.duration_log, '.'*self.dots)
+
+ if self.factor <> (1,1):
+ str += '*%d/%d' % self.factor
+ return str
+
+ def copy (self):
+ d = Duration ()
+ d.dots = self.dots
+ d.duration_log = self.duration_log
+ d.factor = self.factor
+ return d
+
+class Pitch:
+ def __init__ (self):
+ self.alteration = 0
+ self.step = 0
+ self.octave = 0
+
+ def lisp_expression (self):
+ return '(ly:make-pitch %d %d %d)' % (self.octave,
+ self.step,
+ self.alteration)
+
+ def copy (self):
+ p = Pitch ()
+ p.alteration = self.alteration
+ p.step = self.step
+ p.octave = self.octave
+ return p
+
+ def steps (self):
+ return self.step + self.octave * 7
+
+ def ly_expression (self):
+
+ str = 'cdefgab'[self.step]
+ if self.alteration > 0:
+ str += 'is'* self.alteration
+ elif self.alteration < 0:
+ str += 'is'* (-self.alteration)
+
+ if self.octave >= 0:
+ str += "'" * (self.octave + 1)
+ elif self.octave < -1:
+ str += "," * (-self.octave - 1)
+
+ return str
+
+class Music:
+ def __init__ (self):
+ self.tag = None
+ self.parent = None
+ pass
+
+ def set_tag (self, counter, tag_dict):
+ self.tag = counter
+ tag_dict [counter] = self
+ return counter + 1
+
+ def get_properties (self):
+ return ''
+
+ def lisp_expression (self):
+ name = self.name()
+ tag = ''
+ if self.tag:
+ tag = "'input-tag %d" % self.tag
+
+ props = self.get_properties ()
+
+ return "(make-music '%s %s %s)" % (name, tag, props)
+
+ def find_first (self, predicate):
+ if predicate (self):
+ return self
+ return None
+
+class Music_document:
+ def __init__ (self):
+ self.music = test_expr ()
+ self.tag_dict = {}
+
+ def reset_tags (self):
+ self.tag_dict = {}
+ self.music.set_tag (0, self.tag_dict)
+
+class NestedMusic(Music):
+ def __init__ (self):
+ Music.__init__ (self)
+ self.elements = []
+
+ def set_tag (self, counter, dict):
+ counter = Music.set_tag (self, counter, dict)
+ for e in self.elements :
+ counter = e.set_tag (counter, dict)
+ return counter
+
+ def insert_around (self, succ, elt, dir):
+ assert elt.parent == None
+ assert succ == None or succ in self.elements
+
+
+ idx = 0
+ if succ:
+ idx = self.elements.index (succ)
+ if dir > 0:
+ idx += 1
+ else:
+ if dir < 0:
+ idx = 0
+ elif dir > 0:
+ idx = len (self.elements)
+
+ self.elements.insert (idx, elt)
+ elt.parent = self
+
+ def get_properties (self):
+ return ("'elements (list %s)"
+ % string.join (map (lambda x: x.lisp_expression(),
+ self.elements)))
+ def get_neighbor (self, music, dir):
+ assert music.parent == self
+ idx = self.elements.index (music)
+ idx += dir
+ idx = min (idx, len (self.elements) -1)
+ idx = max (idx, 0)
+
+ return self.elements[idx]
+
+ def delete_element (self, element):
+ assert element in self.elements
+
+ self.elements.remove (element)
+ element.parent = None
+
+ def find_first (self, predicate):
+ r = Music.find_first (self, predicate)
+ if r:
+ return r
+
+ for e in self.elements:
+ r = e.find_first (predicate)
+ if r:
+ return r
+ return None
+
+class SequentialMusic (NestedMusic):
+ def name(self):
+ return 'SequentialMusic'
+
+ def ly_expression (self):
+ return '{ %s }' % string.join (map (lambda x:x.ly_expression(),
+ self.elements))
+
+
+class EventChord(NestedMusic):
+ def name(self):
+ return "EventChord"
+ def ly_expression (self):
+ str = string.join (map (lambda x: x.ly_expression(),
+ self.elements))
+ if len (self.elements) > 1:
+ str = '<<%s>>' % str
+
+ return str
+
+class Event(Music):
+ def __init__ (self):
+ Music.__init__ (self)
+
+ def name (self):
+ return "Event"
+
+class RhythmicEvent(Event):
+ def __init__ (self):
+ Event.__init__ (self)
+ self.duration = Duration()
+
+ def get_properties (self):
+ return ("'duration %s"
+ % self.duration.lisp_expression ())
+
+ def name (self):
+ return 'RhythmicEvent'
+
+class RestEvent (RhythmicEvent):
+ def name (self):
+ return 'RestEvent'
+ def ly_expression (self):
+ return '%s' % self.duration.ly_expression ()
+
+class NoteEvent(RhythmicEvent):
+ def __init__ (self):
+ RhythmicEvent.__init__ (self)
+ self.pitch = Pitch()
+
+ def name (self):
+ return 'NoteEvent'
+
+ def get_properties (self):
+ return ("'pitch %s\n 'duration %s"
+ % (self.pitch.lisp_expression (),
+ self.duration.lisp_expression ()))
+
+ def ly_expression (self):
+ return '%s%s' % (self.pitch.ly_expression (),
+ self.duration.ly_expression ())
+
+def test_expr ():
+ m = SequentialMusic()
+
+ evc = EventChord()
+ n = NoteEvent()
+ evc.insert_around (None, n, 0)
+ m.insert_around (None, evc, 0)
+
+ return m
+
+
+if __name__ == '__main__':
+ expr = test_expr()
+ print expr.lisp_expression()
+ print expr.ly_expression()
+
--- /dev/null
+import string
+import re
+import gnomecanvas
+import gtk
+import os
+import socket
+import music
+import pango
+
+def talk_to_lilypond (expression_str):
+ """Send a LISP expression to LilyPond, wait for return value."""
+ sock = socket.socket (socket.AF_INET)
+ address = ("localhost", 2904)
+ sock.connect (address)
+ sock.send (expression_str, socket.MSG_WAITALL)
+
+ cont = 1
+ retval = ''
+ while cont:
+ try:
+ (data, from_addr) = sock.recvfrom (1024)
+ except socket.error:
+ break
+ cont = len (data) > 0
+ retval += data
+
+ return retval
+
+
+class Lilypond_socket_parser:
+ """Maintain state of reading multi-line lilypond output for socket transport."""
+ def __init__ (self, interpret_function):
+ self.clear ()
+ self.interpret_socket_line = interpret_function
+
+ def clear (self):
+ self.cause_tag = None
+ self.bbox = None
+
+ def parse_line (self, line):
+ fields = string.split (line)
+ if not fields:
+ return
+ offset = (0,0)
+ if 0:
+ pass
+ elif fields[0] == 'hello':
+ print 'found server: ', fields[1:]
+ return
+ elif fields[0] == 'at':
+ offset = (string.atof (fields[1]),
+ string.atof (fields[2]))
+ fields = fields[3:]
+ elif fields[0] == 'nocause':
+ self.cause_tag = None
+ self.bbox = None
+ return
+ elif fields[0] == 'cause':
+ self.cause_tag = string.atoi (fields[1])
+ self.name = fields[2]
+ self.bbox = tuple (map (string.atof, fields[3:]))
+ return
+
+ return self.interpret_socket_line (offset, self.cause_tag,
+ self.bbox, fields)
+
+class Notation_controller:
+ """Couple Notation and the music model. Stub for now. """
+ def __init__ (self, music_document):
+ self.document = music_document
+
+ self.notation = Notation (self)
+ self.parser = Lilypond_socket_parser (self.interpret_line)
+
+ def interpret_line (self, offset, cause, bbox, fields):
+ notation_item = self.notation.add_item (offset, cause, bbox, fields)
+
+ def update_notation(self):
+ doc = self.document
+ doc.reset_tags()
+
+ expr = doc.music
+
+ str = expr.lisp_expression()
+ str = talk_to_lilypond (str)
+ self.parse_socket_file (str)
+
+ def parse_socket_file (self, str):
+ self.notation.clear ()
+ lines = string.split (str, '\n')
+ self.parse_lines (lines)
+
+ def parse_lines (self, lines):
+ for l in lines:
+ self.parser.parse_line (l)
+
+class Notation_item:
+ """A single notation element (corresponds to a Grob in LilyPond)"""
+ def __init__ (self):
+ self.origin_tag = None
+ self.bbox = None
+ self.offset = (0,0)
+ self.tag = None
+ self.args = []
+ self.canvas_item = None
+ self.music_expression = None
+
+ def create_round_box_canvas_item (self, canvas):
+ root = canvas.root()
+ type = gnomecanvas.CanvasRect
+ (b, w, d, h, blot) = tuple (self.args)
+ w = root.add (type,
+ fill_color = 'black',
+ x1 = - b,
+ y1 = - d,
+ x2 = w,
+ y2 = h)
+
+ return w
+
+ def create_line_canvas_item (self, canvas):
+ type = gnomecanvas.CanvasLine
+ (thick, x1, y1, x2, y2) = tuple (self.args)
+ w = canvas.root().add (type,
+ fill_color = 'black',
+ width_units = thick,
+ points = [x1, y1, x2, y2]
+ )
+ return w
+
+ def create_glyph_item (self, canvas):
+ type = gnomecanvas.CanvasText
+ (index, font_name, magnification, name) = tuple (self.args)
+ (family, style) = string.split (font_name, '-')
+
+ w = canvas.root().add (type,
+ fill_color = 'black',
+ family = family,
+ family_set = True,
+ anchor = gtk.ANCHOR_WEST,
+ y_offset = 0.15,
+
+ size_points = canvas.pixel_scale * 0.75 * magnification,
+ x = 0, y = 0,
+ text = unichr (index))
+ return w
+
+
+ def create_polygon_item (self, canvas):
+ type = gnomecanvas.CanvasPolygon
+
+ (blot, fill) = tuple (self.args[:2])
+ coords = self.args[2:]
+ w = canvas.root ().add (type,
+ fill_color = 'black',
+ width_units = blot,
+ points = coords)
+
+ return w
+
+ def create_text_item (self, canvas):
+ type = gnomecanvas.CanvasText
+ (descr, str) = tuple (self.args)
+
+ magnification = 0.5
+
+#ugh: how to get pango_descr_from_string() in pygtk?
+
+ (fam,rest) = tuple (string.split (descr, ','))
+ size = string.atof (rest)
+ w = canvas.root().add (type,
+ fill_color = 'black',
+ family_set = True,
+ family = fam,
+ anchor = gtk.ANCHOR_WEST,
+ y_offset = 0.15,
+ size_points = size * canvas.pixel_scale * 0.75 * magnification,
+ text = str)
+ return w
+
+ def create_canvas_item (self, canvas):
+ dispatch_table = {'draw_round_box' : Notation_item.create_round_box_canvas_item,
+ 'drawline': Notation_item.create_line_canvas_item,
+ 'glyphshow': Notation_item.create_glyph_item,
+ 'polygon': Notation_item.create_polygon_item,
+ 'utf-8' : Notation_item.create_text_item,
+ }
+
+ citem = None
+ try:
+ method = dispatch_table[self.tag]
+ citem = method (self, canvas)
+ citem.move (*self.offset)
+ citem.notation_item = self
+
+ canvas.register_notation_canvas_item (citem)
+ except KeyError:
+ print 'no such key', self.tag
+
+ return citem
+
+
+class Notation:
+
+ """A complete line/system/page of LilyPond output. Consists of a
+ number of Notation_items"""
+
+ def __init__ (self, controller):
+ self.items = []
+ self.notation_controller = controller
+
+
+ toplevel = controller.document.music
+ self.music_cursor = toplevel.find_first (lambda x: x.name()== "NoteEvent")
+
+ def get_document (self):
+ return self.notation_controller.document
+
+ def add_item (self, offset, cause, bbox, fields):
+ item = Notation_item()
+ item.tag = fields[0]
+ item.args = map (eval, fields[1:])
+ item.offset = offset
+ item.origin_tag = cause
+
+ if cause and cause >= 0:
+ item.music_expression = self.get_document ().tag_dict[cause]
+
+ item.bbox = bbox
+
+ self.items.append (item)
+
+
+ def clear(self):
+ self.items = []
+
+ def paint_on_canvas (self, canvas):
+ for w in canvas.root().item_list:
+ if w.notation_item:
+ w.destroy()
+
+ for i in self.items:
+ c_item = i.create_canvas_item (canvas)
+
+ canvas.set_cursor_to_music (self.music_cursor)
+
+ def cursor_move (self, dir):
+ mus = self.music_cursor
+ if mus.parent.name() == 'EventChord':
+ mus = mus.parent
+
+ mus = mus.parent.get_neighbor (mus, dir)
+ mus = mus.find_first (lambda x: x.name() in ('NoteEvent', 'RestEvent'))
+ self.music_cursor = mus
+
+ def insert_at_cursor (self, music, dir):
+ mus = self.music_cursor
+ if mus.parent.name() == 'EventChord':
+ mus = mus.parent
+
+ mus.parent.insert_around (mus, music, dir)
+
+ def backspace (self):
+ mus = self.music_cursor
+ if mus.parent.name() == 'EventChord':
+ mus = mus.parent
+
+ neighbor = mus.parent.get_neighbor (mus, -1)
+ mus.parent.delete_element (neighbor)
+
+ def change_octave (self, dir):
+ if self.music_cursor.name() == 'NoteEvent':
+ p = self.music_cursor.pitch
+ p.octave += dir
+
+ def change_step (self, step):
+ if self.music_cursor.name() == 'NoteEvent':
+
+ # relative mode.
+ p = self.music_cursor.pitch
+ p1 = p.copy()
+ p1.step = step
+
+ orig_steps = p.steps ()
+ new_steps = p1.steps ()
+ diff = new_steps - orig_steps
+ if diff >= 4:
+ p1.octave -= 1
+ elif diff <= -4:
+ p1.octave += 1
+
+ self.music_cursor.pitch = p1
+
+ else:
+ print 'not a NoteEvent'
+
+ def change_duration_log (self, dir):
+ if ( self.music_cursor.name() == 'NoteEvent'
+ or self.music_cursor.name() == 'RestEvent'):
+
+ dur = self.music_cursor.duration
+ dl = dur.duration_log
+ dl += dir
+ if dl <= 6 and dl >= -3:
+ dur.duration_log = dl
+
+
+ def ensure_note (self):
+ if self.music_cursor.name() == 'RestEvent':
+ m = self.music_cursor
+ note = music.NoteEvent()
+ m.parent.insert_around (None, note, 1)
+ m.parent.delete_element (m)
+ self.music_cursor = note
+
+ def ensure_rest (self):
+ if self.music_cursor.name() == 'NoteEvent':
+ m = self.music_cursor
+ rest = music.RestEvent()
+ m.parent.insert_around (None, rest, 1)
+ m.parent.delete_element (m)
+ self.music_cursor = rest
+
+ def change_dots (self):
+ if self.music_cursor.name() == 'NoteEvent':
+ p = self.music_cursor.duration
+ if p.dots == 1:
+ p.dots = 0
+ elif p.dots == 0:
+ p.dots = 1
+
+
+ def change_alteration (self, dir):
+ if self.music_cursor.name() == 'NoteEvent':
+ p = self.music_cursor.pitch
+
+ new_alt = p.alteration + dir
+ if abs (new_alt) <= 4:
+ p.alteration = new_alt
+
+
--- /dev/null
+import gtk
+import gnomecanvas
+import music
+
+class Notation_canvas (gnomecanvas.Canvas):
+ """The canvas for drawing notation symbols."""
+
+ def __init__ (self):
+ gnomecanvas.Canvas.__init__ (self,
+ #aa=True
+ )
+ w,h = 800,400
+ self.set_size_request (w, h)
+ self.set_scroll_region(0, 0, w, h)
+ root = self.root()
+ root.affine_relative ((1,0,0,-1,0,0))
+ self.pixel_scale = 10
+ self.set_pixels_per_unit (self.pixel_scale)
+ self.create_cursor ()
+
+ def create_cursor (self):
+ type = gnomecanvas.CanvasRect
+ w = self.root().add (type,
+ fill_color = 'None',
+ outline_color = 'blue')
+ w.notation_item = None
+
+ self.cursor_widget = w
+
+ def set_cursor (self, notation_item):
+ if not notation_item.bbox:
+ print 'no bbox'
+ return
+
+ (x1, y1, x2, y2) = notation_item.bbox
+ self.cursor_widget.set (x1 = x1,
+ x2 = x2,
+ y1 = y1,
+ y2 = y2)
+
+
+ def item_set_active_state (self, item, active):
+ color = 'black'
+ if active:
+ color = 'red'
+
+ item.set (fill_color = color)
+
+ def item_event (self, widget, event=None):
+ if 0:
+ pass
+ elif event.type == gtk.gdk.ENTER_NOTIFY:
+ self.item_set_active_state(widget, True)
+ return True
+ elif event.type == gtk.gdk.LEAVE_NOTIFY:
+ self.item_set_active_state(widget, False)
+ return True
+ return False
+
+ def register_notation_canvas_item (self, citem):
+ if citem.notation_item and citem.notation_item.music_expression:
+ citem.connect ("event", self.item_event)
+
+ def set_cursor_to_music (self, music_expression):
+ c_items = [it for it in self.root().item_list if
+ (it.notation_item
+ and it.notation_item.music_expression
+ == music_expression)]
+
+ if c_items:
+ self.set_cursor (c_items[0].notation_item)
+
+class Notation_canvas_controller:
+ """The connection between canvas and the abstract notation graphics."""
+
+ def __init__ (self, notation):
+ self.canvas = Notation_canvas ()
+ self.notation = notation
+ self.canvas.connect ("key-press-event", self.keypress)
+
+ def update_cursor (self):
+ self.canvas.set_cursor_to_music (self.notation.music_cursor)
+
+ def update_canvas (self):
+ self.notation.paint_on_canvas (self.canvas)
+
+ def add_note (self):
+ note = music.NoteEvent ()
+ if self.notation.music_cursor.name () == 'NoteEvent':
+ note.pitch = self.notation.music_cursor.pitch.copy()
+ note.pitch.alteration = 0
+ note.duration = self.notation.music_cursor.duration.copy()
+
+ ch = music.EventChord ()
+ ch.insert_around (None, note, 0)
+
+ self.notation.insert_at_cursor (ch, 1)
+ self.notation.cursor_move (1)
+
+ def keypress (self, widget, event):
+ key = event.keyval
+ name = gtk.gdk.keyval_name (key)
+
+ if 0:
+ pass
+ elif name == 'Left':
+ self.notation.cursor_move (-1)
+ self.update_cursor ()
+
+ elif name == 'Right':
+ self.notation.cursor_move (1)
+ self.update_cursor ()
+ elif name == 'space':
+ self.add_note ()
+ elif name == 'apostrophe':
+ self.notation.change_octave (1)
+ elif name == 'comma':
+ self.notation.change_octave (-1)
+ elif name == 'BackSpace':
+ self.notation.backspace ()
+ elif name == 'plus':
+ self.notation.change_alteration (2)
+ elif name == 'minus':
+ self.notation.change_alteration (-2)
+ elif name == 'period':
+ self.notation.change_dots ()
+ elif name == 'slash':
+ self.notation.change_duration_log (1)
+ elif name == 'asterisk':
+ self.notation.change_duration_log (-1)
+
+ elif name == 'r':
+ self.notation.ensure_rest ()
+ elif len (name) == 1 and name in 'abcdefg':
+ step = (ord (name) - ord ('a') + 5) % 7
+ self.notation.ensure_note ()
+ self.notation.change_step (step)
+ else:
+ print 'Unknown key %s' % name
+ return False
+
+ self.notation.notation_controller.update_notation ()
+ self.notation.paint_on_canvas (self.canvas)
+ return True
+
--- /dev/null
+
+\paper
+{
+ raggedright = ##t
+ indent = 0.0
+}
+
+\layout {
+ \context {
+ \Score
+ \override BarNumber #'break-visibility = #all-visible
+ }
+}
+
+#(define (render-socket-music music socket)
+ (let*
+ ((score (ly:make-score music))
+ )
+
+ (ly:score-process score #f $defaultpaper $defaultlayout socket)
+ ))
+
+
+
+#(if #t
+ (let ((s (socket PF_INET SOCK_STREAM 0)))
+ (setsockopt s SOL_SOCKET SO_REUSEADDR 1)
+ ;; Specific address?
+ ;; (bind s AF_INET (inet-aton "127.0.0.1") 2904)
+ (bind s AF_INET INADDR_ANY 2904)
+ (listen s 5)
+
+ (simple-format #t "Listening for clients in pid: ~S" (getpid))
+ (newline)
+
+ (while #t
+ (let* ((client-connection (accept s))
+ (start-time (get-internal-real-time))
+ (client-details (cdr client-connection))
+ (client (car client-connection)))
+ (simple-format #t "Got new client connection: ~S"
+ client-details)
+ (newline)
+ (simple-format #t "Client address: ~S"
+ (gethostbyaddr
+ (sockaddr:addr client-details)))
+ (newline)
+ ;; Send back the greeting to the client port
+ (display "hello LilyPond 2.7.1\n" client)
+
+ (let* ((question (read client))
+ (music (eval question (current-module))))
+
+ (render-socket-music music client)
+ (close client)
+ (display (format "Finished. Time elapsed: ~a\n"
+ (/ (- (get-internal-real-time) start-time) (* 1.0 internal-time-units-per-second))
+ ))
+ )))))
+
+
+#(define test-exp (make-music
+ 'SequentialMusic
+ 'elements
+ (list (make-music
+ 'EventChord
+ 'elements
+ (list (make-music
+ 'NoteEvent
+ 'input-tag 42
+ 'duration
+ (ly:make-duration 2 0 1 1)
+ 'pitch
+ (ly:make-pitch -1 0 0))))))
+)
+
+#(render-socket-music test-exp "test")
+