From 2e936c92e5b8ef4397c802e3d24dbf890ee17d2b Mon Sep 17 00:00:00 2001 From: hanwen Date: Wed, 13 Jul 2005 19:10:53 +0000 Subject: [PATCH] start --- GNUmakefile | 19 +++ README | 25 ++++ ikebana.py | 48 +++++++ music.py | 241 ++++++++++++++++++++++++++++++++ notation.py | 341 ++++++++++++++++++++++++++++++++++++++++++++++ notationcanvas.py | 145 ++++++++++++++++++++ server.ly | 78 +++++++++++ 7 files changed, 897 insertions(+) create mode 100644 GNUmakefile create mode 100644 README create mode 100644 ikebana.py create mode 100644 music.py create mode 100644 notation.py create mode 100644 notationcanvas.py create mode 100644 server.ly diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000000..cc0040e273 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,19 @@ + + +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) + + + + + diff --git a/README b/README new file mode 100644 index 0000000000..43d1379e0a --- /dev/null +++ b/README @@ -0,0 +1,25 @@ + +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. diff --git a/ikebana.py b/ikebana.py new file mode 100644 index 0000000000..166dbe60c0 --- /dev/null +++ b/ikebana.py @@ -0,0 +1,48 @@ +#!/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() diff --git a/music.py b/music.py new file mode 100644 index 0000000000..bfbcc81556 --- /dev/null +++ b/music.py @@ -0,0 +1,241 @@ +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() + diff --git a/notation.py b/notation.py new file mode 100644 index 0000000000..7b59fbb782 --- /dev/null +++ b/notation.py @@ -0,0 +1,341 @@ +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 + + diff --git a/notationcanvas.py b/notationcanvas.py new file mode 100644 index 0000000000..468e2dd766 --- /dev/null +++ b/notationcanvas.py @@ -0,0 +1,145 @@ +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 + diff --git a/server.ly b/server.ly new file mode 100644 index 0000000000..2b586f11dd --- /dev/null +++ b/server.ly @@ -0,0 +1,78 @@ + +\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") + -- 2.39.5