#!/usr/pkg/bin/python Copyright = """ pyano.py - funky keyboard to help write lilypond source Copyright (C) 2004 John Comeau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. """ import sys, time, thread, os, types, re, pwd from Tkinter import * from ScrolledText import * from types import * try: sys.path.append(os.sep.join([pwd.getpwuid(os.geteuid())[5], 'lib', 'python'])) from com.jcomeau import gpl, jclicense except: pass # let it fail later if it's going to try: from com.jcomeau.midi import pymidi midiout = pymidi.open("/dev/midi", "wb") except: midiout = None print "pymidi module is preferred, fetch from http://jcomeau.com/." try: from com.jcomeau import beep except: raise Exception, 'Must have either pymidi (preferred) or beep installed' # global variables and constants debug = False # global methods def dbgprint(string): if debug: print string # classes class keymap: """Create and pack buttons according to selected keyboard scheme.""" qwerty = { 0: ('1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ['-', 'minus'], '='), 1: ('Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']'), 2: ('A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', "'", [u'\u25c4', 'Return']), 3: ('Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', [u'\u2191', 'Up'], [u'\u25b2', 'Shift_R']), } alphabet = ('1', '2', '3', '4', '5', '6', '7', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ',', '.', ) def __init__(self, master, mapping = 'qwerty'): self.parent = master if mapping == 'qwerty': for row in range(0, 4): for key in range(0, 12): text = self.qwerty[row][key] value = (row * 12) + key master.button[value] = pyanokey(master, value, text) master.button[value].pack({'side': 'left'}) class nolock: """to avoid errors when not using Pythreads""" def __init__(self): pass def acquire(self): pass def release(self): pass class pyanokey(Button): """Create a button capable of playing a note. Also binds the corresponding key to the same note in the root window, which is assumed to be the parent widget's parent. NOTE that clicking in this window will cause all remaining keyboard hits to echo the key's character in addition to the lilypond note representation.""" isactive = False # maybe need a threadlock on this, so far works def __init__(self, master, value, key = None): lilynotes = ('c', 'df', 'd', 'ef', 'e', 'f', 'gf', 'g', 'af', 'a', 'bf', 'b') lilymarkup = [",", "", "'", "''"] Button.__init__(self, master) self.parent = master # keypress events only seem to work in application root widget: root = master.parent # note: make sure this is true! octave, note = int(value / 12), value % 12 self.lilynote = lilynotes[note] + lilymarkup[octave] self.count = 0 if key != None: if isinstance(key, list): text, X11key = key dbgprint("list: " + text + ", " + X11key) else: text, X11key = key, key self['text'] = text # something screwy about shift key, it "locks on"; skip it if X11key != 'Shift_R': root.bind('', self.play) root.bind('', self.stopplay) if len(X11key) == 1 and X11key != X11key.lower(): # lowercase root.bind('', self.play) root.bind('', self.stopplay) else: self['text'] = self.lilynote if type(midiout) is NoneType: self.tone = beep.notes[(octave, note)] else: self.tone = (12 * 3) + (octave * 12) + note # MIDI note self.bind('', self.stopplay) self.bind('<1>', self.play) def play(self, event): "Queue a note to start playing." if type(midiout) is not NoneType: pymidi.write(midiout, [0x90, self.tone, 64]) if self.isactive: dbgprint("already playing: " + repr(self)) else: self.isactive = True dbgprint("play: " + repr(self)) root = self.parent.parent # note: make sure this is true! root.playlock.acquire() root.notelist.append(self) root.playlock.release() def stopplay(self, event): "Remove this button's note from the queue." dbgprint("stopplay: " + repr(self)) if type(midiout) is not NoneType: pymidi.write(midiout, [0x80, self.tone, 64]) self.isactive = False root = self.parent.parent # note: make sure this is true! root.playlock.acquire() root.notelist.remove(self) root.playlock.release() class pyanokeyboard(Frame): "A frame to hold the graphical representation of the pyano keys" def __init__(self, master): Frame.__init__(self, master) self.parent = master self.button = dict() keymap(self, 'qwerty') class pyano(Tk): "Root window of the application" playlock = nolock() notelist = [] activenote = None # only one note active at a time duration = 0 # how long activenote has been active if debug: interval = 0.1 # time between updates threshold = 3 # intervals before we decide that player likes the note else: interval = 0.05 threshold = 5 stopped = False # allows helper threads to be cleanly stopped def __init__(self, master = None): "Put everything together" Tk.__init__(self, master) self.textbox = ScrolledText(self, {'undo': True}) self.keyboard = pyanokeyboard(self) self.keyboard.pack() self.textbox.pack() def main(self): """Get everything running. Binding 'Destroy' to the 'pleasestop' method solved the problem of the application being difficult to shut down; clicking the big red button just left it churning away, and WinXT had to force it to shut down.""" app = self app.title("Pyano") self.bind('', self.pleasestop) thread.start_new_thread(self.pyanoplayer, ()) app.mainloop() def pleasestop(self, reason): "Just set a boolean variable that child threads need to monitor." self.stopped = True # tell other threads to stop sys.exit(0) # and exit ourselves def pyanoplayer(self): """Watch the note queue, play music and update textbox. This is the main workhorse of the program, and it is really crappy. It would be much better to find a way to access the standard MIDI function of the sound card, and send MIDI 'on' and 'off' messages to it. For the Windows 'Beep' service to work you have to know in advance the duration of the beep, which procludes a real-time response except for a kludge like this one.""" try: self.playlock = Pythread.allocate_lock() while not self.stopped: # put any debugging code before or after the lock dbgprint("notelist: " + repr(self.notelist)) dbgprint("activenote: " + repr(self.activenote)) dbgprint("duration: " + str(self.duration)) self.playlock.acquire() # lock the note list if len(self.notelist) > 0: if self.activenote != self.notelist[0]: # previous ended if self.activenote != None: if self.duration < self.threshold: dbgprint("deleting before new note") self.textbox.edit_undo() # don't save it else: # fix so it won't be removed later self.textbox.edit_reset() self.activenote = self.notelist[0] self.duration = 0 self.textbox.insert(self.textbox.index(('end')), self.notelist[0].lilynote + " ", ()) else: # continue previous note self.duration = self.duration + 1 else: # empty notelist, so must stop any activenote playing if self.activenote != None: self.activenote = None if self.duration < self.threshold: dbgprint("deleting, no new note") self.textbox.edit_undo() else: self.textbox.edit_reset() self.duration = 0 self.playlock.release() # release lock before playing note! if type(midiout) is NoneType and self.activenote != None: dbgprint("playing note: " + self.activenote.lilynote) beep.beep(self.activenote.tone, self.interval) dbgprint("done playing note") else: # otherwise sleep for same amount of time dbgprint("nothing to play, sleeping instead") time.sleep(self.interval) system.exit(0) except: dbgprint("exception received" + repr(sys.exc_info())) sys.exit(0) if __name__ == "__main__": """This expression is something I've seen in many Python programs, but as far as I know, never seen explained. What it does, in effect, is check if you are running this program or importing the package from another; if the latter, the following method does not get invoked.""" pyano.main(pyano())