#!/usr/bin/python ##### changes since v3beta: ##### added gaussian volume ##### .etc files become .mid files instead of .etc.mid files ##### changes on 2006-11-13: ##### ##### added a more intelligent way of handling note_off text ##### with how it works with the gaussian rhythmic variance function-- ##### basically, I added an "off_miditime" variable, which works by backtracking... ##### ##### added regex parsing function to allow for a more sophisticated ##### and closer to 'acbm2ps' style of note naming--- ##### mainly, we will have something like ##### f^2.20@64 which would mean: ##### "play f-sharp for a length of 2 at 20% sound, 80% silence (staccato) ##### (100% would be complete legato) and a midi attack value of 64 (out of 127)" ##### TODO: ##### 1) add "-" for ties ##### ##### 2) finish support for arbitrary controller values using "cntrl = val" syntax ##### and dictonaries storing in midi_functions.py ##### ##### 3) online help with "--help" ##### ##### 4) HEWM/Sagittal JI composition ##### ##### 5) importing .scl files from string import * from sys import argv from os import system from readline import * from time import sleep from random import gauss from math import floor import re ### my libraries: from midi_functions import * from microtonal import * from math_stuff import * ##### default variables: div = 31 ### 31-equal default, use "ji", or "1,200,000" for JI (not yet implemented) tmp = 120 ### tempo in beats per minute (quarter-note) attack = 96 ### default midi note volume (attack) gar = 0 ### gaussian rhythmic variance gav = 0 ### gaussian volume variance length = .5 midi_length = 59 octave = 5 degree = 0 cents = 0 miditime = 0 log = 0 old_channel = 0 channel = 0 old_note = 60 articulation = 99 old_articulation = 99 channel_base = 0 channel_hocket = 0 channel_hocket_max = 2 tie = 0 rest = 0 virgin = 1 midiport = '' old_miditime_offset = 0 controller_types = {'bnk':0,'pan':10,'leg':68, 'cvol':7} ###################################### ######### helper functions: def midi_log(resolution): global log log = 1 midi_text_open('/tmp/et_miditext',0,1,resolution) if len(argv) > 1: file = 1 input_file = open(argv[1],'r') else: file = 0 def next_channel(): global log,miditime,old_channel,channel,old_note,old_tmp,tmp, \ channel_base,channel_hocket_max,channel_hocket,virgin,play if play: all_notes_off() virgin = 1 rest = 0 tie = 0 if play: note_off(old_channel,old_note) if log: midi_text_note_off(miditime,old_channel,old_note) miditime = 0 channel_base = channel_base+1 channel_hocket = 0 channel = channel_base*channel_hocket_max def cleaning(): global miditime,old_channel,old_note,log,play if log: midi_text_note_off(miditime,old_channel,old_note) if play: all_notes_off() ####################################### def change_representation(): global notes if div == 31: notes = {'c':'+0','d':'+5','e':'+10','f':'+13','g':'+18','a':'+23','b':'+28',"^":'+2',"v":'-2',"_":'-2',"'":'+31',",":'-31' } elif div == 19: notes = {'c':'+0','d':'+3','e':'+6','f':'+8','g':'+11','a':'+14','b':'+17',"^":'+1',"v":'-1',"_":'-1',"'":'+19',",":'-19' } elif div == 17: notes = {'c':'+0','d':'+3','e':'+6','f':'+7','g':'+10','a':'+13','b':'+16',"^":'+2',"v":'-2',"_":'-2',"'":'+17',",":'-17' } elif div == 22: notes = {'c':'+0','d':'+4','e':'+8','f':'+9','g':'+13','a':'+17','b':'+21',"^":'+3',"v":'-3',"_":'-3',"'":'+22',",":'-22' } def notevalue(matchobj): global notes return notes[matchobj.group(0)] def solfege2et(text): a = re.sub("[a-g^_v',]", notevalue, text) return eval(a) ######### parsing/playing function, the heart of the program: def et_parser(line): global div,tmp,prg,pan,attack,vol,gar,gav,length,midi_length,miditime,old_miditime,octave,degree,cents, \ old_note,note,bend,log,channel_base,channel,old_channel,articulation,old_articulation,\ channel_hocket,channel_hocket_max,play,tie,virgin,midiport,old_miditime_offset,miditime_offset,\ off_miditime,rest if '#' in line: ### for comments pre_comment = line.split('#')[0] event_groups = pre_comment.split() else: event_groups = line.split() # try: for event in event_groups: if "(" in event: exec(event) in globals() continue elif '=' in event: exec(event) in globals() global_variable = event.split('=') type,val = global_variable[0],global_variable[1] if type == 'prg' and log: xchanmin = channel_base*channel_hocket_max xchanmax = xchanmin+channel_hocket_max for xchan in range(xchanmin,xchanmax,1): midi_text_program_change(miditime, xchan, int(val)) elif type in controller_types and log: xchanmin = channel_base*channel_hocket_max xchanmax = xchanmin+channel_hocket_max for xchan in range(xchanmin, xchanmax,1): midi_text_controller(miditime, xchan, controller_types[type], int(val)) elif type == 'tmp' and log: midi_text_tempo(miditime,int(tmp)) elif type == 'div': change_representation() continue elif event == '|': continue elif '/' in event: parts = event.split('/') numerator = float(parts[0]) denominator = float(parts[1]) length = (numerator/denominator) * 4 midi_length = int(round(length*resolution)) continue elif event[0] == '@': attack = int(event.split('@')[1]) continue elif event[0] == '.': articulation = int(event.split('.')[1]) if articulation < 0: articulation = 10 elif articulation > 100: articulation = 100 continue elif event[0] == 'r': tie = 0 rest = 1 factor = event.split('r')[1] if factor != '': midi_length_factor = int(factor) else: midi_length_factor = 1 elif event[0] == 't': tie = 1 factor = event.split('t')[1] if factor != '': midi_length_factor = int(factor) else: midi_length_factor = 1 elif re.match(r"[0-9]*[.]?[-]?[0-9]+", event) is not None: ### tie = 0 ### rest = 0 ### this section if '.' in event: ### is for old-style octave=int(event.split('.')[0]) ### 'et_compose' degree=int(event.split('.')[1]) ### "octave.degree" else: ### syntax degree=int(event) ### (now deprecated) cents = cents_et(degree,div) note,bend = note_and_bend(cents) note = (octave*12)+note else: tie = 0 rest = 0 event_group = re.search(r"([a-g^v_',]*)([0-9]*)[.]?([0-9]*)[@]?([0-9]*)", event).groups() if event_group[0] != '': degree = solfege2et(event_group[0]) if event_group[1] != '': midi_length_factor = int(event_group[1]) else: midi_length_factor = 1 if event_group[2] != '': articulation = int(event_group[2]) if articulation < 0: articulation = 10 elif articulation >= 100: articulation = 100 else: articulation = 99 if event_group[3] != '': attack = int(event_group[3]) ###### calculate pitch data: cents = cents_et(degree,div) note,bend = note_and_bend(cents) note = (octave*12)+note ######## send it: if play: ### command line interaction if not tie: if not virgin: note_off(old_channel,old_note) if channel_hocket_max < 1: channel_hocket_max = 1 channel_hocket = channel_hocket+1 channel = (channel_base*channel_hocket_max)+(channel_hocket%channel_hocket_max) if not rest: vol = attack else: vol = 0 pitch_bend(channel,bend) note_on(channel,note,vol) if articulation != old_articulation: if articulation == 100: port.write("%c%c%c" % (0xB0+channel,68,127)) port.flush() else: port.write("%c%c%c" % (0xB0+channel,68,0)) port.flush() old_channel = channel old_note = note old_articulation = articulation sleep(length*midi_length_factor(60.0/tmp)) if log: #### usual 'compiler' mode if not tie: if not virgin: off_miditime = old_miditime+floor((miditime-old_miditime)*(articulation/100.0)) midi_text_note_off(off_miditime,old_channel,old_note) if channel_hocket_max < 1: channel_hocket_max = 1 channel_hocket = channel_hocket+1 channel = (channel_base*channel_hocket_max)+(channel_hocket%channel_hocket_max) if virgin: miditime = abs(int(gauss(0,1)*gar)) old_miditime_offset = miditime*-1 if not rest: vol = attack + int(floor(gauss(0,1)*gav)) if vol < 0: vol = abs(vol) if vol > 127: vol = 127 if rest: vol = 0 midi_text_pitch_bend(miditime,channel,bend) midi_text_note_on(miditime,channel,note,vol) virgin=0 if articulation != old_articulation: if articulation == 100: midi_text_controller(miditime,channel,68,127) else: midi_text_controller(miditime,channel,68,0) old_channel = channel old_note = note old_articulation = articulation ########################### we store the current miditime value ################## old_miditime = miditime ### for comparison when we do a note off midi_text message ################################################################################## ################################################################################### miditime_offset = int(gauss(0,1)*gar) #### gaussian rhythmic variance for humanizing #################################################################################### ########################### advance the miditime timestamp to reflect the onset of the next note ## miditime = miditime +(midi_length*midi_length_factor)+miditime_offset+old_miditime_offset #### ################################################################################################### ############################################### next we must reverse effects of gaussian variance and old_miditime_offset = miditime_offset*-1 #### return to normal (center) figure for next round's ############################################### calculation # except ValueError: # print 'syntax error--try again' # print event # except: # pass ################################################################ ####### main, opening the file on the command line: if file: play = 0 output_file = open('/tmp/et','w') ###### read for channels in header line = input_file.readline() line_number = 1 words = line.split() try: if words[0] != 'channels': raise ValueError channel_max = int(words[1]) except: print """ the first line must contain number of channels. For example: channels 4 """ exit() ###### read for resolution in header line = input_file.readline() line_number+=1 words = line.split() try: if words[0] != 'resolution': raise ValueError resolution = int(words[1]) midi_log(resolution) midi_length = resolution except: print """ the second line must contain the midi quarter-note resolution. For example: resolution 4620 """ exit() ####### arrange the channels in order: current = 1 current_string = '%s:' % current if channel_max > 1: try: while current <= channel_max: while line != '' : words = line.split() if len(words) > 0 and (words[0] == current_string): output_file.write('%s\n' % (join(words[1:]))) line = input_file.readline() line_number += 1 if current != channel_max: output_file.write('next_channel()\n') current = current+1 current_string = '%s:' % current input_file.seek(0) line = input_file.readline() line_number += 1 output_file.close() input_file.close() except: print 'there were errors at line number %i which reads:' % line_number print line exit() else: for line in input_file.readlines(): output_file.write("%s\n" % rstrip(line)) output_file.close() input_file.close() ###### now parse the channels in order, and sort them: file = open('/tmp/et','r') chunk = file.readlines() for line in chunk: et_parser(line) file.close() cleaning() midi_text_close() system('sort -s -n -k1,1 /tmp/et_miditext > /tmp/et_miditext_sorted') system('echo \'TrkEnd\' >> /tmp/et_miditext_sorted') system('t2mf /tmp/et_miditext_sorted %s.mid' % (argv[1].replace('.etc',''))) # exit() else: ######## if no command line argument, process in real-time midiport = raw_input("Enter the midi port device you want to test with: ") open_port(midiport) channel_max = 1 channel_hocket_max = 0 resolution = 120 play = 1 while 1: line = raw_input('et--> ') et_parser(line) if virgin == 0: note_on(old_channel,old_note,0) cleaning() close_port()