# justkeys.py - Just Intonation Simple keyboard in Jython. # Takes GUI button clicks, produces MIDI w. pitch bends. # D. Parson, for Electro-Music 2010-2011 Solstice from java.lang import System from javax.swing import * from javax.swing.event import * from javax.sound.midi import * from java.awt import * from java.awt.event import * import sys import re import copy import math import types # The following control-adapting classes all us to read and write # values in a swing control. They are the basis for a simple # command language used with live coding. class PJButton(JButton): """ Add a v property to a JButton. Reading this property returns the isSelected state of the button. Setting it to a value of 0 or less presses the button via doClick(). Setting it to a value > 0.0 (float or int) presses it for (timeMaster.getMSPerTick() * value) milliseconds using doClick(ms). It also invokes mousePressed and mouseReleased methods for any mouse handlers attached to the JButton. If the timeMaster parameter is None, the value parameter to setValue is used directly as milliseconds. The leading arg0, arg1 and arg2 are the optional arguments to the JButton constructor. """ def __init__(self, arg0=None, arg1=None, arg2=None, timeMaster=None): if (arg0 == None): super(PJButton, self).__init__() elif (arg1 == None): super(PJButton, self).__init__(arg0) elif (arg2 == None): super(PJButton, self).__init__(arg0, arg1) else: super(PJButton, self).__init__(arg0, arg1, arg2, arg3) class l(ActionListener): def __init__(self, master): self.master = master def actionPerformed(self, event): self.master.timer.stop() for mh in self.master.getMouseListeners(): mh.mouseReleased(self.master.event) self.timeMaster = timeMaster self.timer = Timer(0, l(self)) self.timer.stop() self.event = MouseEvent(self,0,0,0,0,0,1,False) def getValue(self): return self.isSelected() def setValue(self, value): ty = type(value) if ty != int and ty != float and ty != long: return if (value <= 0): for mh in self.getMouseListeners(): mh.mousePressed(self.event) self.doClick() for mh in self.getMouseListeners(): mh.mouseReleased(self.event) else: ms = 0 try: if (self.timeMaster): ms = int(round(self.timeMaster.getMSPerTick() * value)) else: ms = int(round(value)) except Exception, estr: print "WARNING on PJButton value:", str(estr) return for mh in self.getMouseListeners(): mh.mousePressed(self.event) self.timer.setInitialDelay(ms) self.timer.setDelay(ms) self.timer.start() self.doClick(ms) v = property(getValue, setValue, None) class PJCheckBox(JCheckBox): """ Add a v property to a JCheckBox. Reading this property returns the isSelected state of the button. Setting it to a value of 0 or False sets it to False via doClick() if it is not already False. Setting it to a value of 1 or True sets it to True via doClick() if it is not already True. """ def getValue(self): return self.isSelected() def setValue(self, value): issel = self.isSelected() if value and not issel: self.doClick() elif issel and not value: self.doClick() v = property(getValue, setValue, None) class PJComboBox(JComboBox): """ Add a v property to a JComboBox. Reading this property returns the getSelectedItem() value. Setting it to a String value sets it to that option, catching any exception for an invalid option. """ def getValue(self): return self.getSelectedItem() def setValue(self, value): try: self.setSelectedItem(str(value)) except Exception, estr: print "WARNING on PJComboBox value:", str(estr) v = property(getValue, setValue, None) class PJSpinner(JSpinner): """ Add a v property to a JSpinner. Reading this property returns the getValue() value. Setting it to a value sets it to that setValue() value, catching any exception for an invalid option. """ def getValue(self): return self.getModel().getValue() def setValue(self, value): try: self.getModel().setValue(value) except Exception, estr: print "WARNING on PJSpinner value:", str(estr) v = property(getValue, setValue, None) # eqt holds frequencies in equal temperament, A440 = MIDI key 69 # See http://algoart.com/help/artwonk4/MicroTone/microtone.htm eqt = [None for note in range(0,128)] tro2 = 2.0 ** (1.0/12.0) # twelfth root of 2 freq = 440.0 for note in range(69,-1,-12): eqt[note] = freq freq = freq / 2.0 freq = 440.0 for note in range(69,128,12): eqt[note] = freq freq = freq * 2.0 freq = (eqt[9] / 2.0) * tro2 * tro2 * tro2 # lowest C at MIDI note 0. for note in range(0,128): if (eqt[note] == None): eqt[note] = freq else: freq = eqt[note] # Re-focus at the A440 multiple notes. freq = freq * tro2 # justfreq, justupbend (bend up from closest eqt note) and justdownbend # (bend down from closest eqt note) depend on what frequency we select as # the tonic. When we "change keys" in Just Intonation, the frequencies of # the various notes change. For example, if we take the perfect fourth in # one key and make that the tonic of the key that we shift into, the # the frequencies of the notes around that new tonic change. # For now this code assumes that the receiving synth has a bend range of # + or - 1 semitone. With 0x2000 signifying centered pitch bend, 0x0000 # is -8192 (1 semitone down), and 0x3fff is is 8191 (1 semitone up). # Both cents and pitch bend are linear. jintervals = [1.0, 16.0/15.0, 9.0/8.0, 6.0/5.0, 5.0/4.0, 4.0/3.0, 45.0/32.0, 3.0/2.0, 8.0/5.0, 5.0/3.0, 9.0/5.0, 15.0/8.0, 2.0/1.0] def getJustScale(frequency): """ Return False if not possible, else 2-tuple of justroot location 0..127 into just table, then just table with 128 entries, each being a 5-tuple: (frequency, midiNoteBelow, bendUpFromBelow, midiNoteAbove, bendDownFromAbove) Where midiNoteBelow and midiNoteAbove are indices in eqt. In cases where the just note == the eqt note (happens only with A440), the same midi note with a bend of 0 is used for both directions. In cases where the limits of the MIDI 0..127 range are exceeded, a midi note and bend of None are used in the tuple. """ if (frequency <= eqt[0] or frequency >= eqt[127]): print "BUG, Just Reference frequency", frequency, \ "is outside range." return False just = [None for i in range(0,128)] justroot = None # Update with MIDI note of the Just tonic. # 1. Find the subscript within just[] table where the reference tonic goes. midiNoteBelow, bendUpFromBelow = findMidiNoteBelow(frequency) midiNoteAbove, bendDownFromAbove = findMidiNoteAbove(frequency) if (midiNoteBelow != None): if (midiNoteAbove != None): if abs(bendUpFromBelow) <= abs(bendDownFromAbove): justroot = midiNoteBelow else: justroot = midiNoteAbove else: justroot = midiNoteBelow else: if (midiNoteAbove == None): print "BUG, Just Reference frequency", frequency, \ "is outside range." return False justroot = midiNoteAbove # 2. Populate the just[] table down from this tonic reference frequency. freq = frequency for root in range(justroot,-12,-12): for step in range(0,12): index = root + step if (index > -1 and index < 128): stepfreq = freq * jintervals[step] midiNoteBelow, bendUpFromBelow = findMidiNoteBelow(stepfreq) midiNoteAbove, bendDownFromAbove = findMidiNoteAbove(stepfreq) just[index] = (stepfreq, midiNoteBelow, bendUpFromBelow, midiNoteAbove, bendDownFromAbove) freq = freq / 2.0 # 3. Populate the just[] table up from this tonic reference frequency. freq = frequency for root in range(justroot,140,12): for step in range(0,12): index = root + step if (index > -1 and index < 128): stepfreq = freq * jintervals[step] midiNoteBelow, bendUpFromBelow = findMidiNoteBelow(stepfreq) midiNoteAbove, bendDownFromAbove = findMidiNoteAbove(stepfreq) just[index] = (stepfreq, midiNoteBelow, bendUpFromBelow, midiNoteAbove, bendDownFromAbove) freq = freq * 2.0 return (justroot, just) def findMidiNoteBelow(justfreq): if (justfreq < eqt[0] or (justfreq >= (eqt[127] * 2.0))): return (None, None) # Use serial search for now. index = 0 while (index < 128 and eqt[index] <= justfreq): index += 1 index -= 1 if (index < 0): return (None, None) if eqt[index] == justfreq: return (index, 0) above = eqt[index] * tro2 ratio = (justfreq - eqt[index]) / (above - eqt[index]) bend = int(round(8192 * ratio)) if (bend > 8191): # assymetry in 2's complement numbers bend = 8191 return (index, bend) def findMidiNoteAbove(justfreq): if (justfreq <= (eqt[0] / 2.0) or (justfreq > eqt[127])): return (None, None) # Use serial search for now. index = 127 while (index > -1 and eqt[index] >= justfreq): index -= 1 index += 1 if (index > 127): return (None, None) if eqt[index] == justfreq: return (index, 0) below = eqt[index] / tro2 ratio = (eqt[index] - justfreq) / (eqt[index] - below) bend = - int(round(8192 * ratio)) if (bend < -8192): # assymetry in 2's complement numbers bend = -8192 return (index, bend) __intervals__ = \ ['1', 'b2', '2', 'b3', '3', '4', 'T', '5', 'b6', '6', 'b7', '7', 'o'] __midiNoteNames__ = \ ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b'] __CHANNELS__ = 16 __ALL_NOTES_OFF__ = 123 # midi control message __VOLUME__ = 7 # midi control message __INIT_OCTAVE__ = 0 # initial frequency is usually in middle of 0..127 __CHKXBARS__ = 4 # number of crossbar columns for key coupling # jroot, just = getJustScale(440.0) # Use A 440 reference for error checking. class keyboard(JFrame): """ Create a set of 16 keyboards, 1 per midi channel, with a subset of notes in a 12-step just intonation scale, and the ability to re-set the tonic to one of the notes in the just scale being played. """ class environ(object): """ WARNING: Class environ mutates every time addprop is called. All instances of environ get any new property added by addprop. keyboad.environ is used to add a lexical environment for live coding. Properties added to each channel's environ object are available as Lexpressions and Rexpressions in live coding. After addprop is invoked, an instance object of environ must have its initprop method called to initialize that property with a value. """ def __init__(self, scopearray, scheduler, channel, button=None): """ Store scopearray in the sc field, where scopearray is the list of all environ objects, one per channel. The genstr generator string (source code for a generator) and genfunc functional object (a invokable method or generator within the scope of this object) are initialized to None. The scheduler is the class that schedules genfunc to run inside this scope after successful compilation. """ self.sc = scopearray self.scheduler = scheduler self.channel = channel self.genstr = None self.genfunc = None self.button = button if (self.button): self.button.setForeground(Color.BLACK) class l(ActionListener): def __init__(self, master): self.master = master def actionPerformed(self, event): try: delay = self.master.execute() if (delay): # A non-0 delay delay = int(round( delay * self.master.scheduler.getMSPerTick())) self.master.timer.setDelay(delay) else: self.master.timer.stop() self.master.genfunc = None except Exception, estr: print "CODE ERROR on channel ", \ str(self.master.channel+1), str(estr) self.master.timer.stop() self.master.genfunc = None self.timer = Timer(0, l(self)) def setButton(self, button): self.button = button def saveAndSilence(self, codestring): codestring = codestring.strip() self.timer.stop() self.genstr = codestring self.genfunc = None def compile(self, codestring): """ Compile string in genstr into genfunc, if compile fails, raise exception after setting genfunc to None. Store and leave codestring in self.genstr. """ codestring = codestring.strip() self.genstr = codestring self.genfunc = None self.timer.stop() exec(codestring, globals(), self.__dict__) t = type(self.genfunc) if (t != types.FunctionType): self.genfunc = None if (self.button): self.button.setForeground(Color.BLACK) raise TypeError, ("Invalid code type: " + str(t)) delay = self.execute() # Run the code the first time. if (delay): # A non-0 delay delay = int(round(delay * self.scheduler.getMSPerTick())) self.timer.setInitialDelay(delay) self.timer.setDelay(delay) self.timer.start() else: self.genfunc = None def execute(self): """ Execute genfunc compiled by compile. """ delay = None if (self.genfunc): t = type(self.genfunc) if (t != types.FunctionType and t != types.GeneratorType): self.genfunc = None if (self.button): self.button.setForeground(Color.BLACK) raise TypeError, ("Invalid code type: " + str(t)) elif t == types.FunctionType: try: delay = self.genfunc(self, self.sc, self.channel) except Exception, estr: self.genfunc = None if (self.button): self.button.setForeground(Color.BLACK) raise TypeError, ("Error in code for chan " + str(self.channel+1)) if type(delay) == types.GeneratorType: # The 1st function call has constructed a generator. self.genfunc = delay t = types.GeneratorType delay = None if (t == types.GeneratorType): try: delay = self.genfunc.next() except StopIteration: delay = None self.genfunc = None except Exception, estr: self.genfunc = None if (self.button): self.button.setForeground(Color.BLACK) raise TypeError, ("Error in code for chan " + str(self.channel+1)) if (not delay): self.genfunc = None if (self.button): self.button.setForeground(Color.BLACK) return None t = type(delay) if ((t != int and t != float) or delay <= 0): self.genfunc = None if (self.button): self.button.setForeground(Color.BLACK) raise TypeError, ("Error in code return value for chan " + str(self.channel+1) + ": " + str(t) + " " + str(delay)) return delay __propnames = [] # Class static variable to track addprop. @staticmethod def getPropNames(): return copy.copy(keyboard.environ.__propnames) @staticmethod def addprop(propname): # neither CPython nor Jython allow direct modification of a # __dict__ attribute of a class object via exec, so this code # execs method definitions into a dummy 'template' object and # then copies this code into the environ class via setattr. # The 'template' object is then left to the garbage collector. # After addprop is invoked, an instance object of environ must have # its initprop method called to initialize that property's value. class foo(object) : pass template = foo() getter = 'get' + propname setter = 'set' + propname variable = '__' + propname getstr = 'def ' + getter + '(self): return self.' + variable \ + '.getValue()' setstr = 'def ' + setter + '(self,value): self.' + variable \ + '.setValue(value)' exec(getstr, template.__dict__) exec(setstr, template.__dict__) setattr(keyboard.environ, getter, getattr(template, getter)) setattr(keyboard.environ, setter, getattr(template, setter)) setattr(keyboard.environ, propname, property( getattr(keyboard.environ, getter), getattr(keyboard.environ, setter))) if not propname in keyboard.environ.__propnames: keyboard.environ.__propnames.append(propname) def initprop(self, pname, pvalue): # After addprop is invoked, an instance object of environ must have # its initprop method called to initialize that property's value. setattr(self, '__' + pname, pvalue) def __init__(self, keymap, msPerTick, tonic, envInitFunc=None): """ Setup up 16 keyboards, one per midi channel and one per row. Parameter 'keymap' has up to 13 entries (top for octave), each 1 enables that key in the octave being played. For example, [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1] enables tonic, 5th, b7th, oct. Each row has a bend either|up|down selector, a retuned to map step 0..12 to the new tonic for that row. parameter msPerTick is the int number of milliseconds in a tick. The tonic parameter is the frequency of the initial tonic. If envInitFunc is not None, it is a function that takes a reference to a list of scope objects 0..__CHANNELS__-2 and initializes some properties in them. """ # First define the nested event handling classes. class CboSynthHandler(ActionListener): """ Handles actions on cboSynth selector for synth output. """ def __init__(self, master, control, chan): """ self is this handler object. master is the enclosing keyboard object. control is the control triggering this event. chan is the 0..15 channel. """ self.master = master self.control = control self.chan = chan def actionPerformed(self, event): index = self.control.getSelectedIndex() oldindex = self.master.midioutdevindex[self.chan] if (oldindex == index): # This is not a change of output (spurious GUI event). return if self.master.midioutrcvr[self.chan]: try: keyboard.send(self.master.midioutrcvr[self.chan], ShortMessage.CONTROL_CHANGE, self.chan,__ALL_NOTES_OFF__,0) self.master.midioutrcvr[self.chan].close() self.master.mididevOpenCount[oldindex] -= 1 if self.master.mididevOpenCount[oldindex] == 0: self.master.mididev[oldindex].close() except Exception, estr: print "ERROR, CANNOT CLOSE PRIOR DEVICE AT CHANNEL", \ str(self.chan+1), str(estr) self.master.midioutrcvr[self.chan] = None if (index > 0): # 0 is none -- no output try: if self.master.mididevOpenCount[index] == 0: self.master.mididev[index].open() self.master.mididevOpenCount[index] += 1 self.master.midioutrcvr[self.chan] \ = self.master.mididev[index].getReceiver() self.master.midioutdevindex[self.chan] = index keyboard.send(self.master.midioutrcvr[self.chan], ShortMessage.PROGRAM_CHANGE,self.chan, self.master.spnPatch[self.chan].getValue(),0) keyboard.send(self.master.midioutrcvr[self.chan], ShortMessage.CONTROL_CHANGE,self.chan, __VOLUME__,self.master.spnVol[self.chan].getValue()) except Exception, estr: print "ERROR, CANNOT INIT DEVICE AT CHANNEL", \ str(self.chan+1), str(estr) if self.master.midioutrcvr[self.chan]: try: self.master.midioutrcvr[self.chan].close() self.master.midioutrcvr[self.chan] = None except Exception: self.master.midioutrcvr[self.chan] = None self.master.midioutdevindex[self.chan] = 0 else: self.master.midioutdevindex[self.chan] = 0 class BtnScaleHandler(ActionListener): """ Handles actions on btnScale that applies cboScale. """ def __init__(self, master, control, chan): """ self is this handler object. master is the enclosing keyboard object. control is the cboScale for the pressed btnScale. chan is the 0..15 channel. """ self.master = master self.control = control self.chan = chan def actionPerformed(self, event): index = self.control.getSelectedIndex() if (index >= len(__intervals__)): # Copy settings from nother channel. index -= len(__intervals__) if (index >= 0 and index < __CHANNELS__ and index != self.chan): self.master.scales[self.chan] = copy.copy( self.master.scales[index]) self.master.octave[self.chan] \ = self.master.octave[index] self.master.spnOct[self.chan].setValue( self.master.octave[self.chan]) self.master.tonic[self.chan] = copy.copy( self.master.tonic[index]) midiname = __midiNoteNames__[ \ self.master.getMidiNoteFromScale( self.master.scales[self.chan]) % 12] self.master.lblFreq[self.chan].setText( midiname + ' ' \ + ("%.2f" % self.master.tonic[self.chan])) # self.control.setSelectedIndex(0) return if (index): # 0 is current tonic oldnote = self.master.scales[self.chan][0] newnote = oldnote + index if (newnote <= 127): newfreq = self.master.scales[self.chan][1][newnote][0] newscales = getJustScale(newfreq) if (newscales): octdiff = int(math.floor((newscales[0] - self.master.startNote) / 12.0)) newoct = __INIT_OCTAVE__ + octdiff # Set reference before spinner change. self.master.octave[self.chan] = newoct self.master.spnOct[self.chan].setValue(newoct) self.master.scales[self.chan] = newscales self.master.tonic[self.chan] \ = newscales[1][newscales[0]][0] midiname = __midiNoteNames__[ \ self.master.getMidiNoteFromScale( self.master.scales[self.chan]) % 12] self.master.lblFreq[self.chan].setText( midiname + ' ' \ + ("%.2f" % self.master.tonic[self.chan])) # self.control.setSelectedIndex(0) class SpnPatchHandler(ChangeListener): """ Handles actions on spnPatch spinner for patch number. """ def __init__(self, master, control, chan): """ self is this handler object. master is the enclosing keyboard object. control is the control triggering this event. chan is the 0..15 channel. """ self.master = master self.control = control self.chan = chan def stateChanged(self, event): if self.master.midioutrcvr[self.chan]: try: keyboard.send(self.master.midioutrcvr[self.chan], ShortMessage.PROGRAM_CHANGE,self.chan, self.control.getValue(),0) except Exception, estr: print "ERROR, CANNOT SEND PATCH CHANGE AT CHANNEL", \ str(self.chan+1), str(estr) class SpnVolHandler(ChangeListener): """ Handles actions on spnVol spinner for channel volume. """ def __init__(self, master, control, chan): """ self is this handler object. master is the enclosing keyboard object. control is the control triggering this event. chan is the 0..15 channel. """ self.master = master self.control = control self.chan = chan def stateChanged(self, event): if self.master.midioutrcvr[self.chan]: try: keyboard.send(self.master.midioutrcvr[self.chan], ShortMessage.CONTROL_CHANGE,self.chan, __VOLUME__,self.control.getValue()) except Exception, estr: print "ERROR, CANNOT SEND PATCH CHANGE AT CHANNEL", \ str(self.chan+1), str(estr) class SpnOctHandler(ChangeListener): """ Handles spnOct spinner for note octave offset from center. """ def __init__(self, master, control, chan): """ self is this handler object. master is the enclosing keyboard object. control is the control triggering this event. chan is the 0..15 channel. """ self.master = master self.control = control self.chan = chan def stateChanged(self, event): newoct = self.control.getValue() if (newoct != self.master.octave[self.chan]): diff = newoct - self.master.octave[self.chan] newtonic = self.master.scales[self.chan][0] + (diff * 12) if (newtonic >= 0 and newtonic <= 127): multiplier = 2 ** diff self.master.octave[self.chan] = newoct self.master.tonic[self.chan] *= multiplier self.master.scales[self.chan] = (newtonic, self.master.scales[self.chan][1]) midiname = __midiNoteNames__[ \ self.master.getMidiNoteFromScale( self.master.scales[self.chan]) % 12] self.master.lblFreq[self.chan].setText( midiname + ' ' \ + ("%.2f" % self.master.tonic[self.chan])) else: self.control.setValue(self.master.octave[self.chan]) class BtnNoteHandler(MouseAdapter): """ Handles press and release of mouse on a just note's key. """ def __init__(self,master,control,chkbox,chan,keyix,step,rats): """ self is this handler object. master is the enclosing keyboard object. control is the control triggering this event. chkbox is the latch chkbox for this channel. chan is the 0..15 channel. keyix is the index of this JButton in chan, starting at 0. step is the scale step 0..11 on the keyboard for chan. rats is list of BtnNoteHandlers for chan, 1 per JButton. """ self.master = master self.control = control self.chkbox = chkbox self.chan = chan self.keyix = keyix self.step = step self.ratListeners = rats self.bend = None # holds current pitch bend on this chan self.note = None # current noteon pitch self.velocity = 0 # velocity of note that was sent self.latched = False def mousePressed(self, event): self.delegateEvent(event, True) if self.master.midioutrcvr[self.chan]: if (self.latched): self.latched = False if self.master.lastLatched[self.chan] is self: self.master.lastLatched[self.chan] = None self.control.setForeground(Color.BLACK) # Use None so no delegation triggered here. self.mouseReleased(None) return if (self.master.lastLatched[self.chan] \ and not self.master.lastLatched[self.chan] is self): # Use None so no delegation triggered here. self.master.lastLatched[self.chan].mousePressed(None) noteindex = self.master.scales[self.chan][0] + self.step if (noteindex < 0 \ or noteindex > len(self.master.scales[self.chan][1])): return self.velocity = self.master.spnVel[self.chan].getValue() bendtype = self.master.cboBend[self.chan].getSelectedItem() notetuple = self.master.scales[self.chan][1][noteindex] if (notetuple[1] == None and notetuple[3] == None): return # This note is not mapped. if (bendtype != "none"): if (bendtype == "both"): if notetuple[1] == None: self.note = notetuple[3] self.bend = notetuple[4] elif notetuple[3] == None: self.note = notetuple[1] self.bend = notetuple[2] elif abs(notetuple[2]) <= abs(notetuple[4]): # Note below is closer than note above. self.note = notetuple[1] self.bend = notetuple[2] else: self.note = notetuple[3] self.bend = notetuple[4] elif (bendtype == "up"): if notetuple[1] == None: return # Cannot bend up from below. self.note = notetuple[1] self.bend = notetuple[2] else: # "down" if notetuple[3] == None: return # Cannot bend down from above. self.note = notetuple[3] self.bend = notetuple[4] else: self.note = noteindex self.bend = 0 if (self.bend != None): self.bend += 0x02000 # Bend centering for MIDI. try: if (self.bend != None): data1 = self.bend & 0x07f # 7 bottom bits data2 = (self.bend >> 7) & 0x07f keyboard.send(self.master.midioutrcvr[self.chan], ShortMessage.PITCH_BEND,self.chan, data1, data2) keyboard.send(self.master.midioutrcvr[self.chan], ShortMessage.NOTE_ON,self.chan, self.note, self.velocity) if (self.chkbox.isSelected()): self.latched = True self.control.setForeground(Color.RED) self.master.lastLatched[self.chan] = self except Exception, estr: print "ERROR, CANNOT SEND NOTE CHANGE AT CHANNEL", \ str(self.chan+1), str(estr) def mouseReleased(self, event): self.delegateEvent(event, False) if (self.latched): return if self.master.midioutrcvr[self.chan]: if (self.note != None): try: keyboard.send(self.master.midioutrcvr[self.chan], ShortMessage.NOTE_OFF,self.chan, self.note, self.velocity) except Exception, estr: print "ERROR, CANNOT SEND NOTE OFF AT CHANNEL", \ str(self.chan+1), str(estr) self.bend = None self.note = None self.velocity = 0 def delegateEvent(self, event, isPress): """ After handling the event, delegate a mousePressed() (when isPress is True) or mouseReleased (when isPress is False) to any other BtnNoteHandler object sharing a crossbar with me, calling any other handler object at most once. Pass an event value of None as a flag showing delegation. """ if event: called = set([self]) for colix in range(0,__CHKXBARS__): if self.ratListeners in self.master.crossbar[colix]: for ratListeners in self.master.crossbar[colix]: if self.ratListeners is ratListeners: continue listner = ratListeners[self.keyix] if not listner in called: called.add(listner) if isPress: listner.mousePressed(None) else: listner.mouseReleased(None) class ChkLatchedHandler(ActionListener): """ Handles actions on chkLatched key latch. """ def __init__(self, master, control, chan): """ self is this handler object. master is the enclosing keyboard object. control is the control triggering this event. chan is the 0..15 channel. """ self.master = master self.control = control self.chan = chan def actionPerformed(self, event): if (self.master.lastLatched[self.chan] \ and not self.control.isSelected()): self.master.lastLatched[self.chan].mousePressed(event) class ChkXbarHandler(ActionListener): """ Handles actions on chkXbar crossbar latch. """ def __init__(self, master, control, chan, colix, ratListeners): """ self is this handler object. master is the enclosing keyboard object. control is the control triggering this event. chan is the 0..15 channel. colix is index 0 .. __CHKXBARS__-1 of crossbar column. ratListeners is list of mouse listeners for chan, 1 per JButton. """ self.master = master self.control = control self.chan = chan self.colix = colix self.ratListeners = ratListeners def actionPerformed(self, event): if (self.control.isSelected()): if not self.ratListeners \ in self.master.crossbar[self.colix]: self.master.crossbar[self.colix].append( self.ratListeners) else: if self.ratListeners in self.master.crossbar[self.colix]: self.master.crossbar[self.colix].remove( self.ratListeners) class BtnCodeDialog(JDialog): """ Implements test editing window for BtnCodeHandler. """ def __init__(self, parentFrame, scope, chan, btnCode): super(BtnCodeDialog, self).__init__(parentFrame, False) self.master = parentFrame self.scope = scope self.chan = chan self.btnCode = btnCode self.setLayout(BorderLayout()) pnlButtons = JPanel(GridLayout(1,0)) btnCompile = JButton("Compile") pnlButtons.add(btnCompile) btnCancel = JButton("Cancel") pnlButtons.add(btnCancel) btnSilence = JButton("Silence") pnlButtons.add(btnSilence) self.add(pnlButtons, BorderLayout.SOUTH) self.txtEditor = JTextArea() if (self.scope.genstr): self.txtEditor.setText(self.scope.genstr) else: codetemplate = "def genfunc(s, sc, ch):\n\t" self.txtEditor.setText(codetemplate) self.add(self.txtEditor, BorderLayout.CENTER) class BtnCancelListener(ActionListener): def __init__(self, dialog): self.dialog = dialog def actionPerformed(self, event): self.dialog.dispose() class BtnSilenceListener(ActionListener): def __init__(self, dialog, scope, btnCode): self.dialog = dialog self.scope = scope self.btnCode = btnCode def actionPerformed(self, event): codestring = self.dialog.txtEditor.getText() self.scope.saveAndSilence(codestring) self.btnCode.setForeground(Color.BLACK) self.dialog.dispose() class BtnCompileListener(ActionListener): def __init__(self, mainFrame, dialog, scope, chan, btnCode): self.mainFrame = mainFrame self.dialog = dialog self.scope = scope self.chan = chan self.btnCode = btnCode def actionPerformed(self, event): codestring = self.dialog.txtEditor.getText() try: self.scope.compile(codestring) except Exception, estr: self.dialog.txtEditor.append("\nERROR: " + str(estr)) self.scope.genfunc = None self.btnCode.setForeground(Color.BLACK) return # The remainder of compilation and scheduling gets # performed inside self.scope.compile. self.btnCode.setForeground(Color.RED) self.dialog.dispose() btnCancel.addActionListener(BtnCancelListener(self)) btnCompile.addActionListener(BtnCompileListener( self.master, self, self.scope, self.chan, self.btnCode)) btnSilence.addActionListener(BtnSilenceListener( self, self.scope, self.btnCode)) self.setTitle("Editor for channel " + str(self.chan+1)) self.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); self.setSize(800, 500) self.setLocationRelativeTo(self.master.btnCode[10]) self.setVisible(True) class BtnCodeHandler(ActionListener): """ Handles actions on btnCode, brings up an editor. """ def __init__(self, master, scope, chan, btnCode): """ self is this handler object. master is the enclosing keyboard object. scope is the environ scope object for this code. chan is the 0..15 channel. btnCode is the button triggering this event. """ self.master = master self.scope = scope self.chan = chan self.btnCode = btnCode def actionPerformed(self, event): ui = BtnCodeDialog(self.master, self.scope, self.chan, self.btnCode) i = -1 self.keys = [] self.msPerTick = msPerTick for flag in keymap: i += 1 if flag: self.keys.append((i, __intervals__[i])) self.startTonic = tonic self.tonic = [tonic for chan in range(0,__CHANNELS__)] self.octave = [__INIT_OCTAVE__ for chan in range(0,__CHANNELS__)] self.scales = [getJustScale(tonic) for chan in range(0,__CHANNELS__)] self.startNote = self.scales[0][0] # chan,freq,keys...,octave,vel,volume,patch,cboBend,cboScale, # btnScale,cboSynth self.columns = len(self.keys) + 12 mididevinfo = [None] + list(MidiSystem.getMidiDeviceInfo()) self.mididevstr = ["none"] # descriptions of midi devices with output self.mididev = [None] # the devices themselves self.mididevOpenCount = [0] for devinfo in mididevinfo[1:]: dev = MidiSystem.getMidiDevice(devinfo) if (dev and dev.getMaxReceivers()): self.mididevstr.append(devinfo.getDescription()) self.mididev.append(dev) self.mididevOpenCount.append(0) self.setLayout(GridLayout(__CHANNELS__+1, self.columns, 1, 1)) # Pre-allocate lists of per-channel controls that we need to update # or access later, based on channel number. Also, midioutrcvr is the # open midi output receiver for this channel, initially None open. # midioutdevindex is the midioutrcvr's MidiDevice's index. self.midioutrcvr = [None for i in range (0,__CHANNELS__)] self.midioutdevindex = [0 for i in range (0,__CHANNELS__)] self.scope = [] for i in range (0,__CHANNELS__): self.scope.append(keyboard.environ(self.scope, self, i)) self.lastLatched = [None for i in range (0,__CHANNELS__)] self.lblFreq = [None for i in range(0,__CHANNELS__)] self.chkLatched = [None for i in range(0,__CHANNELS__)] self.spnOct = [None for i in range(0,__CHANNELS__)] self.spnVel = [None for i in range(0,__CHANNELS__)] self.spnVol = [None for i in range(0,__CHANNELS__)] self.spnPatch = [None for i in range(0,__CHANNELS__)] self.cboBend = [None for i in range(0,__CHANNELS__)] self.cboScale = [None for i in range(0,__CHANNELS__)] self.btnScale = [None for i in range(0,__CHANNELS__)] self.cboSynth = [None for i in range(0,__CHANNELS__)] self.crossbar = [[] for j in range(0,__CHKXBARS__)] self.ratListeners = [[] for i in range(0,__CHANNELS__)] self.btnCode = [None for i in range(0,__CHANNELS__)] chkXbar = [[None for j in range(0,__CHKXBARS__)] for i in range(0,__CHANNELS__)] headings = ['channel', 'code'] hdgx = "x0..x" + str(__CHKXBARS__ - 1) headings.append(hdgx) headings = headings + ['freq', 'l'] keyix = -1 for key in self.keys: keyix += 1 headings.append("b"+str(keyix)+",k"+key[1]) headings = headings + ['o', 'vel', 'vol', 'p', 'b', 'i', 't', 's'] for h in headings: lblhdg = JLabel(h, SwingConstants.CENTER) self.add(lblhdg) for chan in range(1, __CHANNELS__+1): chan_1 = chan - 1 lblchan = JLabel(str(chan), SwingConstants.CENTER) self.add(lblchan) self.btnCode[chan_1] = JButton('code') self.add(self.btnCode[chan_1]) self.scope[chan_1].setButton(self.btnCode[chan_1]) self.btnCode[chan_1].addActionListener( BtnCodeHandler(self, self.scope[chan_1], chan_1, self.btnCode[chan_1])) pnlXbar = JPanel(GridLayout(1,0)) for p in range(0,__CHKXBARS__): chkXbar[chan_1][p] = PJCheckBox() pnlXbar.add(chkXbar[chan_1][p]) # Add widget to live coding scope. pname = 'x' + str(p) # xINDEX crossbar property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, chkXbar[chan_1][p]) self.add(pnlXbar) midiname = __midiNoteNames__[self.getMidiNoteFromScale( self.scales[chan_1]) % 12] self.lblFreq[chan_1] = JLabel(midiname + ' ' \ + ("%.2f" % self.tonic[chan_1]), SwingConstants.CENTER) self.add(self.lblFreq[chan_1]) self.chkLatched[chan_1] = PJCheckBox("latch") self.add(self.chkLatched[chan_1]) # Add widget to live coding scope. pname = 'l' # l latch property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.chkLatched[chan_1]) self.chkLatched[chan_1].addActionListener( ChkLatchedHandler(self, self.chkLatched[chan_1], chan_1)) keyix = -1 for key in self.keys: keyix += 1 btnChan = PJButton(key[1], timeMaster=self) self.add(btnChan) # Add widget to live coding scope. pname = 'b' + str(keyix) # bINDEX bend property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, btnChan) pname = 'k' + key[1] # kINTERVAL key property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, btnChan) hndlr = BtnNoteHandler( self, btnChan, self.chkLatched[chan_1], chan_1, keyix, key[0], self.ratListeners[chan_1]) btnChan.addMouseListener(hndlr) self.ratListeners[chan_1].append(hndlr) for p in range(0,__CHKXBARS__): xhndlr = ChkXbarHandler(self, chkXbar[chan_1][p], chan_1, p, self.ratListeners[chan_1]) chkXbar[chan_1][p].addActionListener(xhndlr) choct = int(self.scales[chan_1][0] / 12) self.spnOct[chan_1] = PJSpinner(SpinnerNumberModel( __INIT_OCTAVE__,-choct,12-choct,1)) self.add(self.spnOct[chan_1]) # Add widget to live coding scope. pname = 'o' # o octave property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.spnOct[chan_1]) self.spnOct[chan_1].addChangeListener( SpnOctHandler(self, self.spnOct[chan_1], chan_1)) self.spnVel[chan_1] = PJSpinner(SpinnerNumberModel(64,0,127,1)) self.add(self.spnVel[chan_1]) # Add widget to live coding scope. pname = 'vel' # vel velocity property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.spnVel[chan_1]) self.spnVol[chan_1] = PJSpinner(SpinnerNumberModel(64,0,127,1)) self.add(self.spnVol[chan_1]) # Add widget to live coding scope. pname = 'vol' # vol volume property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.spnVol[chan_1]) self.spnVol[chan_1].addChangeListener( SpnVolHandler(self, self.spnVol[chan_1], chan_1)) self.spnPatch[chan_1] = PJSpinner(SpinnerNumberModel(64,0,127,1)) self.add(self.spnPatch[chan_1]) # Add widget to live coding scope. pname = 'p' # p patch property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.spnPatch[chan_1]) self.spnPatch[chan_1].addChangeListener( SpnPatchHandler(self, self.spnPatch[chan_1], chan_1)) self.cboBend[chan_1] = PJComboBox(["both", "up", "down", "none"]) self.cboBend[chan_1].setEditable(False) self.add(self.cboBend[chan_1]) # Add widget to live coding scope. pname = 'b' # b bend (direction) property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.cboBend[chan_1]) exintervals = copy.copy(__intervals__) for ix in range(1, __CHANNELS__+1): exintervals.append("ch " + str(ix)) self.cboScale[chan_1] = PJComboBox(exintervals) self.cboScale[chan_1].setEditable(False) self.add(self.cboScale[chan_1]) # Add widget to live coding scope. pname = 'i' # i next interval property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.cboScale[chan_1]) self.btnScale[chan_1] = PJButton("newroot", timeMaster=self) self.add(self.btnScale[chan_1]) # Add widget to live coding scope. pname = 't' # t set tonic to next interval property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.btnScale[chan_1]) self.btnScale[chan_1].addActionListener( BtnScaleHandler(self, self.cboScale[chan_1], chan_1)) self.cboSynth[chan_1] = PJComboBox(self.mididevstr) self.cboSynth[chan_1].setEditable(False) # Add widget to live coding scope. pname = 's' # s synth property if (chan_1 == 0): keyboard.environ.addprop(pname) self.scope[chan_1].initprop(pname, self.cboSynth[chan_1]) self.cboSynth[chan_1].addActionListener( CboSynthHandler(self, self.cboSynth[chan_1], chan_1)) self.add(self.cboSynth[chan_1]) self.setTitle("Just Intonation MIDI Keyboard, D. Parson, NYE 2010-2011") self.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); self.setSize(1350, 750) self.setLocationRelativeTo(None) self.setVisible(True) if (envInitFunc): envInitFunc(self.scope) def getMSPerTick(self): return self.msPerTick def setMSPerTick(self, value): self.msPerTick = value def getMidiNoteFromScale(self, scale, scaleindex=None): """ Look up scaleindex in scale and return closest MIDI note number, above or below. scale is ordered pair (tonicindex, list-of-5-tuples), where each 5-tuple is (freq, notebelow, bendup, noteabove, benddown). The value returned is notebelow or noteabove, whichever has the least magnitude of bend. When scaleindex is None it uses tonicindex. """ if (scaleindex == None): scaleindex = scale[0] fivetuple = scale[1][scaleindex] if (fivetuple[3] == None or abs(fivetuple[2]) <= abs(fivetuple[4])): return fivetuple[1] else: return fivetuple[3] @staticmethod def send(midioutrcvr, cmd, chan, data1, data2): """ Send midi message cmd-chan-data1-data2 to midioutrcvr by building & sending a ShortMessagewith a timestamp of -1. """ msg = ShortMessage() msg.setMessage(cmd, chan, data1, data2) try: midioutrcvr.send(msg, -1) except Exception, estr: print "ERROR trying to send midi packet:", str(estr) def __envInitFunc__(scope): for c in range(0,__CHANNELS__): if c == 13: scope[c].s = "Software wavetable synthesizer and receiver" scope[c].p = 17 # cool organ else: scope[c].s = "IAC Driver" # synth scope[c].i = 4 # advance by circle of 4ths scope[c].o = -1 if __name__ == '__main__': kb = keyboard([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1], 375, eqt[74], __envInitFunc__) # kb = keyboard([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 375, eqt[74]) pass