def trackDefPattern(name, ln): """ Define a pattern for a track. Use the type-name for all defines.... check the track names and if it has a '-' in it, we use only the part BEFORE the '-'. So DRUM-Snare becomes DRUM. """ ln = ln[:] name = name.split("-")[0] trackAlloc(name, 1) if ln: pattern = ln.pop(0).upper() else: error("Define is expecting a pattern name") if pattern in ("z", "Z", "-"): error("Pattern name '%s' is reserved" % pattern) if pattern.startswith("_"): error("Names with a leading underscore are reserved") if not ln: error("No pattern list given for '%s %s'" % (name, pattern)) ln = " ".join(ln) gbl.tnames[name].definePattern(pattern, ln)
def trackDefPattern(name, ln): """ Define a pattern for a track. Use the type-name for all defines.... check the track names and if it has a '-' in it, we use only the part BEFORE the '-'. So DRUM-Snare becomes DRUM. """ ln = ln[:] name = name.split('-')[0] trackAlloc(name, 1) if ln: pattern = ln.pop(0).upper() else: error("Define is expecting a pattern name") if pattern in ('z', 'Z', '-'): error("Pattern name '%s' is reserved" % pattern) if pattern.startswith('_'): error("Names with a leading underscore are reserved") if not ln: error("No pattern list given for '%s %s'" % (name, pattern)) ln = ' '.join(ln) gbl.tnames[name].definePattern(pattern, ln)
def trackSequence(name, ln): """ Define a sequence for a track. The format for a sequence: TrackName Seq1 [Seq2 ... ] Note, that SeqX can be a predefined seq or { seqdef } The {} is dynamically interpreted into a def. """ if not ln: error("Use: %s Sequence NAME [...]" % name) ln = ' '.join(ln) self = gbl.tnames[name] # this is the pattern class if self.vtype == "SOLO": warning("Sequences for SOLO tracks are not saved in Grooves.") """ Before we do extraction of {} stuff make sure we have matching {}s. Count the number of { and } and if they don't match read more lines and append. If we get to the EOF then we're screwed and we error out. Only trick is to make sure we do macro expansion! This code lets one have long sequence lines without bothering with '\' continuations. """ oLine = gbl.lineno # in case we error out, report start line while ln.count('{') != ln.count('}'): l = gbl.inpath.read() if l is None: # reached eof, error gbl.lineno = oLine error("%s Sequence {}s do not match" % name) l = ' '.join(macros.expand(l)) if l[-1] != '}' and l[-1] != ';': error("%s: Expecting multiple sequence lines to end in ';'" % name) ln += ' ' + l """ Extract out any {} definitions and assign them to new define variables (__1, __99, etc) and melt them back into the string. """ ids = 1 while 1: sp = ln.find("{") if sp < 0: break ln, s = pextract(ln, "{", "}", onlyone=True) if not s: error("Did not find matching '}' for '{'") pn = "_%s" % ids ids += 1 trk = name.split('-')[0] trackAlloc(trk, 1) """ We need to mung the plectrum classes. Problem is that we define all patterns in the base class (plectrum-banjo is created in PLECTRUM) which is fine, but the def depends on the number of strings in the instrument (set by the tuning option). So, we save the tuning for the base class, copy the real tuning, and restore it. NOTE: at this point the base and current tracks have been initialized. """ if trk == 'PLECTRUM' and name != trk: z = gbl.tnames[trk]._tuning[:] gbl.tnames[trk]._tuning = gbl.tnames[name]._tuning else: z = None gbl.tnames[trk].definePattern(pn, s[0]) # 'trk' is a base class! if z: gbl.tnames[trk]._tuning = z ln = ln[:sp] + ' ' + pn + ' ' + ln[sp:] ln = ln.split() """ We now have a sequence we can save for the track. All the {} defs have been converted to special defines (_1, _2, etc.). First we expand ln to the proper length. lnExpand() also duplicates '/' to the previous pattern. Then we step though ln: - convert 'z', 'Z' and '-' to empty patterns. - duplicate the existing pattern for '*' - copy the defined pattern for everything else. There's a bit of Python reference trickery here. Eg, if we have the line: Bass Sequence B1 B2 the sequence is set with pointers to the existing patterns defined for B1 and B2. Now, if we later change the definitions for B1 or B2, the stored pointer DOESN'T change. So, changing pattern definitions has NO EFFECT. """ ln = lnExpand(ln, '%s Sequence' % self.name) tmp = [None] * len(ln) for i, n in enumerate(ln): n = n.upper() if n in ('Z', '-'): tmp[i] = None elif n == '*': tmp[i] = self.sequence[i] else: p = (self.vtype, n) if not p in pats: error("Track %s does not have pattern '%s'" % p) tmp[i] = pats[p] self.sequence = seqBump(tmp) if MMA.debug.seqshow: msg = ["%s sequence set:" % self.name] for a in ln: if a in "Zz-": msg.append("-") else: msg.append(a) dPrint(' '.join(msg))
def parse(inpath): """ Process a mma input file. """ global beginData, lastChord gbl.inpath = inpath curline = None while 1: curline = inpath.read() if curline is None: # eof, exit parser break l = macros.expand(curline) if not l: continue """ Handle BEGIN and END here. This is outside of the Repeat/End and variable expand loops so SHOULD be pretty bullet proof. Note that the beginData stuff is global to this module ... the Include/Use directives check to make sure we're not doing that inside a Begin/End. beginData[] is a list which we append to as more Begins are encountered. The placement here is pretty deliberate. Variable expand comes later so you can't macroize BEGIN ... I think this makes sense. The tests for 'begin', 'end' and the appending of the current begin[] stuff have to be here, in this order. """ action = l[0].upper() # 1st arg in line if action == "BEGIN": if not l[1:]: error("Use: Begin STUFF") beginPoints.append(len(beginData)) beginData.extend(l[1:]) continue if action == "END": if len(l) > 1: error("No arguments permitted for END") if not beginData: error("No 'BEGIN' for 'END'") beginData = beginData[: beginPoints.pop(-1)] continue if beginData: l = beginData + l action = l[0].upper() if gbl.showExpand and action != "REPEAT": print(l) # If the command is in the simple function table, jump & loop. if action in simpleFuncs: simpleFuncs[action](l[1:]) continue """ We have several possibilities ... 1. The command is a valid assigned track name, 2. The command is a valid track name, but needs to be dynamically allocated, 3. It's really a chord action """ if not action in gbl.tnames: # no track allocated? trackAlloc(action, 0) # Try to create. Always returns. if action in gbl.tnames: # BASS/DRUM/APEGGIO/CHORD name = action if len(l) < 2: error("Expecting argument after '%s'" % name) action = l[1].upper() # Got trackname and action if action in trackFuncs: # perfect, execute trackFuncs[action](name, l[2:]) continue elif action in simpleFuncs: # opps, not track func error("%s is not a track function. Use global form." % action) else: # opps, not any kind of func error("%s is not a %s track function." % (action, name)) ### Gotta be a chord data line! """ A data line can have an optional bar number at the start of the line. Makes debugging input easier. The next block strips leading integers off the line. Note that a line number on a line by itself it okay. """ if action.isdigit(): # isdigit() matches '1', '1234' but not '1a'! gbl.barLabel = l[0] l = l[1:] if not l: # ignore empty lines continue else: gbl.barLabel = "" """ A bar can have an optional repeat count. This must be at the end of bar in the form '* xx'. """ if len(l) > 1 and l[-2] == "*": rptcount = stoi(l[-1], "Expecting integer after '*'") l = l[:-2] else: rptcount = 1 """ Extract solo(s) from line ... this is anything in {}s. The solo data is pushed into RIFFs and discarded from the current line. """ l = " ".join(l) l = MMA.patSolo.extractSolo(l, rptcount) """ Set lyrics from [stuff] in the current line. NOTE: lyric.extract() inserts previously created data from LYRICS SET and inserts the chord names if that flag is active. """ l, lyrics = lyric.extract(l, rptcount) l = l.split() """ At this point we have only chord info. A number of sanity checks are made: 1. Make sure there is some chord data, 2. Ensure the correct number of chords. """ if not l: error("Expecting music (chord) data. Even lines with\n" " lyrics or solos still need a chord") """ We now have a chord line. It'll look something like: ['Cm', '/', 'z', 'F#@4.5'] or ['/' 'C@3' ] For each bar we create a list of CTables, one for each chord in the line. Each entry has the start/end (in beats), chordname, etc. """ ctable = parseChordLine(l) # parse the chord line # Create MIDI data for the bar for rpt in range(rptcount): # for each bar in the repeat count ( Cm * 3) """ Handle global (de)cresc by popping a new volume off stack. """ if MMA.volume.futureVol: MMA.volume.volume = MMA.volume.futureVol.pop(0) if MMA.volume.futureVol: MMA.volume.nextVolume = MMA.volume.futureVol[0] else: MMA.volume.nextVolume = None """ Set up for rnd seq. This may set the current seq point. If return is >=0 then we're doing track rnd. """ rsq, seqlist = MMA.seqrnd.setseq() """ Process each track. It is important that the track classes are written so that the ctable passed to them IS NOT MODIFIED. This applies especially to chords. If the track class changes the chord, then the function called MUST restore it before returning!!! """ for a in gbl.tnames.values(): if rsq >= 0: seqSave = gbl.seqCount if a.name in seqlist: # for seqrnd with tracklist gbl.seqCount = rsq a.bar(ctable) # process entire bar! if rsq >= 0: # for track rnd gbl.seqCount = seqSave # Adjust counters """ After processsing each bar we update a dictionary of bar pointers. This table is used when the MIDI data is written when -b or -B is set to limit output. """ if MMA.truncate.length: nextOffset = MMA.truncate.length MMA.truncate.countDown() else: nextOffset = gbl.barLen # barPtrs is used by the -B/b options to strip unwanted sections. gbl.barPtrs[gbl.barNum + 1] = [gbl.barLabel, gbl.tickOffset, gbl.tickOffset + nextOffset - 1] gbl.totTime += float(nextOffset / gbl.BperQ) / gbl.tempo gbl.tickOffset += nextOffset if gbl.printProcessed: if gbl.barLabel: gbl.barLabels.append(gbl.barLabel) else: gbl.barLabels.append("?") gbl.barNum += 1 gbl.seqCount = (gbl.seqCount + 1) % gbl.seqSize if gbl.barNum > gbl.maxBars: error("Capacity exceeded. Maxbar setting is %s. Use -m option" % gbl.maxBars) MMA.grooves.nextGroove() # using groove list? Advance. # Enabled with the -r command line option if gbl.showrun: if lyrics: # we print lyric as a list ly = lyrics # with the []s else: ly = "" # no lyric print("%3d: %s %s" % (gbl.barNum, " ".join(l), ly))
def midiinc(ln): """ Include a MIDI file into MMA generated files. """ filename = '' doLyric = 0 doText = 0 channels = [] transpose = None stripSilence = -1 report = 0 istart = 0 # istart/end are in ticks iend = 0xffffff # but are set in options in Beats verbose = 0 octAdjust = 0 velAdjust = 100 ignorePC = 1 stretch = None notopt, ln = opt2pair(ln) if notopt: error("MidiInc: Expecting cmd=opt pairs, not '%s'." % ' '.join(notopt)) for cmd, opt in ln: cmd = cmd.upper() if cmd == 'FILE': filename = MMA.file.fixfname(opt) elif cmd == 'VOLUME': velAdjust = stoi(opt) elif cmd == 'OCTAVE': octAdjust = stoi(opt) if octAdjust < -4 or octAdjust > 4: error("MidiInc: 'Octave' adjustment must be -4 to 4, not %s" % opt) octAdjust *= 12 elif cmd == 'TRANSPOSE': transpose = stoi(opt) if transpose < -24 or transpose > 24: error("MidiInc: 'Transpose' must be -24 to 24, not %s" % opt) elif cmd == 'START': if opt[-1].upper() == 'M': # measures istart = int(stof(opt[:-1]) * gbl.barLen) elif opt[-1].upper() == 'T': # ticks istart = int(stof(opt[:-1])) else: # must be digits, stof() catches errors istart = int((stof(opt)-1) * gbl.BperQ) if istart < 0: error("MidiInc: 'Start' must be > 0.") elif cmd == 'END': if opt[-1].upper() == 'M': iend = int((stof(opt[:-1])-1) * gbl.barLen) elif opt[-1].upper() == 'T': iend = int(stof(opt[:-1])) else: iend = int((stof(opt)-1) * gbl.BperQ) if iend < 0: error("MidiInc: 'End' must be > 0.") elif cmd == 'TEXT': opt = opt.upper() if opt in ("ON", '1'): doText = 1 elif opt in ("OFF", '0'): doText = 0 else: error("MidiInc: 'Text' expecting 'ON' or 'OFF'") elif cmd == 'LYRIC': opt = opt.upper() if opt in ("ON", '1'): doLyric = 1 elif opt in ("OFF", '0'): doLyric = 0 else: error("MidiInc: 'Lyric' expecting 'ON' or 'OFF'") elif cmd == "REPORT": opt = opt.upper() if opt in ("ON", '1'): report = 1 elif opt in ("OFF", '0'): report = 0 else: error("MidiInc: 'Report' expecting 'ON' or 'OFF'") elif cmd == "VERBOSE": opt = opt.upper() if opt in ("ON", '1'): verbose = 1 elif opt in ("OFF", '0'): verbose = 0 else: error("MidiInc: 'Verbose' expecting 'ON' or 'OFF'") elif cmd == "STRIPSILENCE": opt = opt.upper() if opt in ("OFF", '0'): stripSilence = 0 elif opt == "ON": # this is the default stripSilence = -1 else: stripSilence = stoi(opt, "MIdiInc StripSilence= expecting " "'value', 'On' or 'Off', not %s" % opt) elif cmd == "IGNOREPC": opt = opt.upper() if opt in ("TRUE", "ON", "1"): # default ignorePC = 1 elif opt in ("FALSE", "OFF", "0"): # use program change in imported ignorePC = 0 else: error("MIdiInc: 'IncludePC' expecting 'True' or 'False', not %s" % opt) elif cmd == "STRETCH": v = stof(opt) if v < 1 or v > 500: error("MidiInc: 'Stretch' range of 1 to 500, not %s." % opt) stretch = v/100. # If none of the above matched a CMD we assume that it is # a trackname. Keep this as the last test! else: trackAlloc(cmd, 0) if not cmd in gbl.tnames: error("MidiInc: %s is not a valid MMA track" % cmd) opt = opt.split(',') riffmode = 0 printriff = 0 ch = None for o in opt: o = o.upper() if o == 'RIFF': riffmode = 1 elif o == 'SEQUENCE': riffmode = 2 elif o == 'PRINT': printriff = 1 if not riffmode: riffmode = 1 else: if ch is not None: error("MidiInc: Only one channel assignment per track.") ch = stoi(o) if ch < 1 or ch > 16: error("MidiInc: MIDI channel for import must be 1..16, not %s" % ch) channels.append((cmd, ch-1, riffmode, printriff)) # If transpose was NOT set, use the global transpose value # Note special riff value as well. Need to double adjust since # the riff import will do its own adjustment. # this needs to be done BEFORE reading the midi file if transpose is None: transpose = gbl.transpose riffTranspose = -gbl.transpose else: riffTranspose = 0 octAdjust += transpose # this takes care of octave and transpose mf = MidiData() mf.octaveAdjust = octAdjust mf.velocityAdjust = velAdjust mf.ignorePC = ignorePC try: mf.readFile(filename) except RuntimeError as e: error("MidiInc: %s" % e) if mf.beatDivision != gbl.BperQ: warning("MIDI file '%s' tick/beat of %s differs from MMA's " "%s. Will try to compensate" % (filename, mf.beatDivision, gbl.BperQ)) mf.adjustBeats( gbl.BperQ / float(mf.beatDivision)) if report or verbose: # try to be helpful print("MIDI File %s successfully read." % filename) print("Total Text events: %s" % len(mf.textEvents)) print("Total Lyric events: %s" % len(mf.lyricEvents)) print('\n') for ch in sorted(mf.events.keys()): if not mf.events[ch]: continue if verbose and not report: # in verbose mode only list info for tracks we're using doit = 0 for z in channels: if z[1] == ch: doit = 1 break if not doit: continue fnote = fevent = 0xffffff ncount = 0 for ev in mf.events[ch]: delta = ev[0] if delta < fevent: fevent = delta if ev[1] >> 4 == 0x9: if delta < fnote: fnote = delta if ev[3]: ncount += 1 msg = ["Channel %2s: First event %-8s" % (ch+1, fevent)] if ncount: msg.append("First Note %-8s Total Notes %-4s" % (fnote, ncount)) print(' '.join(msg)) if report: print("\nNo data generated!") sys.exit(0) if not channels: if doLyric or doText: warning("MidiInc: no import channels specified, " "only text or lyrics imported") else: error("MidiInc: A channel to import and a destination " "track must be specified") if (istart >= iend) or (istart < 0) or (iend < 0): error("MidiInc: Range invalid, start=%s, end=%s" % (istart, iend)) if gbl.debug: print("MidiInc: file=%s, Volume=%s, Octave=%s, Transpose=%s, Lyric=%s, " "Text=%s, Range=%s..%s StripSilence=%s Verbose=%s" % (filename, velAdjust, octAdjust, transpose, doLyric, doText, istart, iend, stripSilence, verbose)) msg = [] for t, ch, riffmode, riffprint in channels: o = '' if riffmode == 1: o = ',riff' elif riffmode == 2: o = ',sequence' elif printriff: o += ',print' msg.append("MidiInc: Channel %s-->%s%s" % (ch+1, t, o)) print(' '.join(msg)) if stretch: if verbose: print("Applying stretch to all events. Deltas will be multiplied by %s" % stretch) for tr in mf.events: for e in mf.events[tr]: e[0] = int(e[0] * stretch) # e[0] is the offset for e in mf.textEvents: e[0] = int(e[0] * stretch) for e in mf.lyricEvents: e[0] = int(e[0] * stretch) # Midi file parsed, add selected events to mma data if stripSilence == 0: if verbose: print("Firstnote offset was %s. Being reset to start of file by StripSilence=Off." % mf.firstNote) mf.firstNote = 0 if verbose: print("First note offset: %s" % mf.firstNote) if doText: inst = 0 disc = 0 if verbose: print("Scanning %s textevents." % len(mf.textEvents)) for tm, tx in mf.textEvents: delta = tm-mf.firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addText(gbl.tickOffset + delta, tx) inst += 1 else: disc += 1 if gbl.debug: print("MidiInc text events: %s inserted, %s out of range." % (inst, disc)) if doLyric: inst = 0 disc = 0 if verbose: print("Scanning %s LyricEvents." % len(mf.lyricEvents)) for tm, tx in mf.lyricEvents: delta = tm-mf.firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addLyric(gbl.tickOffset + delta, tx) inst += 1 else: disc += 1 if gbl.debug: print("MidiInc lyric events: %s inserted, %s out of range." % (inst, disc)) for n, c, riffmode, printriff in channels: if not len(mf.events[c]): warning("No data to assign from imported channel %s to track %s" % (c+1, n)) inst = 0 disc = 0 for tr, ch, riffmode, printriff in channels: onNotes = [] if gbl.tnames[tr].disable: # skip if disabled track if verbose: print("Skipping import of channel %s since track %s is disabled." % (ch, tr)) continue t = gbl.tnames[tr] if not t.channel: t.setChannel() if riffmode: riff = [] if t.vtype not in ('MELODY', 'SOLO'): error("MidiInc: Riff only works on Melody/Solo tracks, not '%s'." % t.name) t.clearPending() if t.voice[0] != t.ssvoice: gbl.mtrks[t.channel].addProgChange(gbl.tickOffset, t.voice[0], t.ssvoice) channel = t.channel track = gbl.mtrks[channel] if verbose: print("Parsing imported file. Channel=%s Track=%s MIDI Channel=%s" % (ch, tr, channel)) if len(mf.events[ch]): print(" Total events: %s; Event range: %s %s; Start/End Range: %s %s" % (len(mf.events[ch]), mf.events[ch][0][0], mf.events[ch][-1][0], istart, iend)) else: print("No events in Channel %s" % ch) # If we're processing midi voice changes (ignorePC=off) and there # are events BEFORE the first note, w eneed to insert # them before the notes. We put them all at the current midi offset. if ignorePC==0: for ev in mf.events[ch]: if ev[0] > mf.firstNote: break if ev[1] >> 4 == 0xc: track.addToTrack(gbl.tickOffset, packBytes(ev[1] | channel-1, *ev[2:])) inst += 1 disc -= 1 for ev in mf.events[ch]: delta = ev[0]-mf.firstNote if delta >= istart and delta <= iend: if riffmode: offset = delta-istart x = ev[1] >> 4 if x != 0x09 and x != 0x08: # skip non note events continue pitch = ev[2] velocity = ev[3] if x == 0x8: velocity = 0 riff.append([offset, pitch, velocity]) else: offset = gbl.tickOffset + (delta-istart) # add note on/off, key pressure, etc. track.addToTrack(offset, packBytes(ev[1] | channel-1, *ev[2:])) # Track on/off events to avoid stuck midi notes. x = ev[1] >> 4 if x == 0x09 and ev[3] and ev[2] not in onNotes: onNotes.append(ev[2]) # append this pitch to onNotes if x == 0x09 and not ev[3] or x == 0x08 and ev[2] in onNotes: onNotes.remove(ev[2]) # remove this as being ON inst += 1 else: disc += 1 if onNotes: for x in onNotes: track.addToTrack(offset, packBytes(0x90 | channel-1, [x,0])) warning("MidiINC: Stuck MIDI note(s) '%s' turned off in %s." % (', '.join([str(x) for x in onNotes]), tr)) if riffmode: evlist = createRiff(riff, tr, riffTranspose) if riffmode == 2: txt = [] for a in sorted(evlist): if printriff and riffmode == 1: print("%s Riff %s" % (tr, evlist[a])) elif riffmode == 2: # sequence mode, create sequence line and push into input txt.append("{%s}" % evlist[a]) else: # riffmode==1, printriff=0 - just add to the riff stack gbl.tnames[tr].setRiff(evlist[a]) if riffmode == 2 and txt: if printriff: print("%s Sequence %s" % (tr, ' '.join(txt))) else: MMA.sequence.trackSequence(tr, txt) if gbl.debug: print("MidiInc events: %s inserted, %s out of range." % (inst, disc))
def midiinc(ln): """ Include a MIDI file into MMA generated files. """ global midifile, offset filename = '' doLyric = 0 doText = 0 volAdjust = 100 octAdjust = 0 transpose = None channels = [] # These are the start/end points for the included file. They are in # beats, but are adjusted after the file is opened to ticks. istart = 0 iend = 0xffffff for a in ln: cmd, opt = a.split('=') cmd = cmd.upper() if cmd == 'FILE': filename = os.path.expanduser(opt) elif cmd == 'VOLUME': volAdjust = stoi(opt) elif cmd == 'OCTAVE': octAdjust = stoi(opt) if octAdjust < -4 or octAdjust > 4: error("Octave adjustment must be -4 to 4, not %s" % opt) octAdjust *= 12 elif cmd == 'TRANSPOSE': transpose = stoi(opt) if transpose < -24 or transpose > 24: error("Tranpose must be -24 to 24, not %s" % opt) elif cmd == 'START': istart = stof(opt) elif cmd == 'END': iend = stof(opt) elif cmd == 'TEXT': opt = opt.upper() if opt in ("ON", 1): doText = 1 elif opt in ("OFF", 0): doText = 0 else: error("MidiInc Text= expecting 'ON' or 'OFF'") elif cmd == 'LYRIC' and opt != '0': opt = opt.upper() if opt in ("ON", 1): doLyric = 1 elif opt in ("OFF", 0): doLyric = 0 else: error("MidiInc Lyric= expecting 'ON' or 'OFF'") # make sure this is last option ... it has to be a TRACKNAME=CHANNEL-NUMBER else: trackAlloc(cmd, 0) if not cmd in gbl.tnames: error("%s is not a valid MMA track" % cmd) ch = stoi(opt) if ch < 1 or ch > 16: error("MIDI channel for import must be 1..16, not %s" % ch) channels.append((cmd, ch - 1)) if not channels: if doLyric or doText: warning( "MidiInc: no import channels specified, only text or lyrics imported" ) else: error( "MidiInc: A channel to import and a destination track must be specified" ) if (istart >= iend) or (istart < 0) or (iend < 0): error("MidiInc range invalid: start=%s, end=%s" % (istart, iend)) if gbl.debug: print "MidiInc: file=%s, Volume=%s, Octave=%s, Transpose=%s, Lyric=%s, Text=%s, Range=%s..%s"\ % (filename, volAdjust, octAdjust, transpose, doLyric, doText, istart, iend) for t, ch in channels: print "MidiInc: Channel %s --> Track %s" % (ch + 1, t) # If transpose was NOT set, use the global transpose value if transpose == None: transpose = gbl.transpose octAdjust += transpose # this takes care of octave and transpose try: inpath = file(filename, "rb") except: error("Unable to open MIDI file %s for reading" % filename) midifile = inpath.read() inpath.close() # Create our storage: # A dic with the channels 0-15 as keys for the midi note events # 2 lists for lyrics and text events. These have tuples for (time, text) events = {} for c in range(0, 16): events[c] = [] textEvs = [] lyricEvs = [] # Ensure this is valid header hd = midifile[0:4] if hd != 'MThd': error("Expecting 'HThd', %s not a standard midi file" % filename) offset = 4 a = m32i() if a != 6: error("Expecting a 32 bit value of 6 in header") format = m16i() if format not in (0, 1): error("MIDI file format %s not recognized" % format) ntracks = m16i() beatDivision = m16i() if beatDivision != gbl.BperQ: warning("MIDI file '%s' tick/beat of %s differs from MMA's " "%s. Will try to compensate" % (filename, beatDivision, gbl.BperQ)) # Adjust start/end to the file's tick istart *= beatDivision iend *= beatDivision midievents = {} firstNote = 0xffffff for tr in range(ntracks): tm = 0 hdr = midifile[offset:offset + 4] offset += 4 if hdr != 'MTrk': error("Malformed MIDI file in track header") trlen = m32i() # track length, not used? lastevent = None """ Parse the midi file. We have to parse off each event, even though many will just be thrown away. You can't just skip around in a midi file :) In the future we might decide to include meta stuff, etc. Or, we may not :) For now, we keep: - note on - note off - key pressure - control change - program change - channel pressure - pitch blend - text event - lyric event """ while 1: tm += mvarlen() # adjust total offset by delta ev = m1i() if ev < 0x80: if not lastevent: error("Illegal running status in %s at %s" % (midifile, offset)) offset -= 1 ev = lastevent sValue = ev >> 4 # Shift MSBs to get a 4 bit value channel = ev & 0x0f if sValue == 0x8: # note off event note = m1i() vel = m1i() if octAdjust and channel != 10: note += octAdjust if note < 0 or note > 127: continue events[channel].append([tm, ev & 0xf0, chr(note) + chr(vel)]) elif sValue == 0x9: # note on event if tm < firstNote: firstNote = tm note = m1i() vel = m1i() if octAdjust and channel != 10: note += octAdjust if note < 0 or note > 127: continue if volAdjust != 100: vel = int((vel * volAdjust) / 100) if vel < 0: vel = 1 if vel > 127: vel = 127 events[ev & 0xf].append([tm, ev & 0xf0, chr(note) + chr(vel)]) elif sValue == 0xa: # key pressure events[ev & 0xf].append([tm, ev & 0xf0, chars(2)]) elif sValue == 0xb: # control change events[ev & 0xf].append([tm, ev & 0xf0, chars(2)]) elif sValue == 0xc: # program change events[ev & 0xf].append([tm, ev & 0xf0, chars(1)]) elif sValue == 0xd: # channel pressure events[ev & 0xf].append([tm, ev & 0xf0, chars(1)]) elif sValue == 0xe: # pitch blend events[ev & 0xf].append([tm, ev & 0xf0, chars(2)]) elif sValue == 0xf: # system, mostly ignored if ev == 0xff: # meta events a = m1i() if a == 0x00: # sequence number l = mvarlen() offset += l elif a == 0x01: # text (could be lyrics) textEvs.append((tm, chars(mvarlen()))) elif a == 0x02: # copyright l = mvarlen() offset += l elif a == 0x03: # seq/track name l = mvarlen() offset += l elif a == 0x04: # instrument name l = mvarlen() offset += l elif a == 0x05: # lyric lyricEvs.append((tm, chars(mvarlen()))) elif a == 0x06: # marker l = mvarlen() offset += l elif a == 0x07: # cue point l = mvarlen() offset += l elif a == 0x21: # midi port l = mvarlen() offset += l elif a == 0x2f: # end of track l = mvarlen() offset += l break elif a == 0x51: #tempo l = mvarlen() offset += l elif a == 0x54: # SMPTE offset l = mvarlen() offset += l elif a == 0x58: # time sig l = mvarlen() offset += l elif a == 0x59: # key sig l = mvarlen() offset += l else: # probably 0x7f, proprietary event l = mvarlen() offset += l elif ev == 0xf0: # system exclusive l = mvarlen() offset += l elif ev == 0xf2: # song position pointer, 2 bytes offset += 2 elif ev == 0xf3: # song select, 1 byte offset += 1 else: # all others are single byte commands pass if ev >= 0x80 and ev <= 0xef: lastevent = ev # Midi file parsed, add selected events to mma data beatad = gbl.BperQ / float(beatDivision) if doText: inst = 0 disc = 0 for tm, tx in textEvs: delta = tm - firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addText(gbl.tickOffset + int(delta * beatad), tx) inst += 1 else: disc += 1 if gbl.debug: print "MidiInc text events: %s inserted, %s out of range." % (inst, disc) if doLyric: inst = 0 disc = 0 for tm, tx in lyricEvs: delta = tm - firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addLyric(gbl.tickOffset + int(delta * beatad), tx) inst += 1 else: disc += 1 if gbl.debug: print "MidiInc lyric events: %s inserted, %s out of range." % ( inst, disc) for n, c in channels: if not len(events[c]): warning("No data to assign from imported channel %s to track %s" % (c + 1, n)) inst = 0 disc = 0 for tr, ch in channels: t = gbl.tnames[tr] if not t.channel: t.setChannel() t.clearPending() if t.voice[0] != t.ssvoice: gbl.mtrks[t.channel].addProgChange(gbl.tickOffset, t.voice[0]) channel = t.channel track = gbl.mtrks[channel] for ev in events[ch]: delta = ev[0] - firstNote if delta >= istart and delta <= iend: track.addToTrack(gbl.tickOffset + int(delta * beatad), chr(ev[1] | channel - 1) + ev[2]) inst += 1 else: disc += 1 if gbl.debug: print "MidiInc events: %s inserted, %s out of range." % (inst, disc)
def parse(inpath): """ Process a mma input file. """ global beginData, lastChord gbl.inpath = inpath curline = None while 1: MMA.after.check() curline = inpath.read() if curline is None: # eof, exit parser break l = macros.expand(curline) if not l: continue """ Handle BEGIN and END here. This is outside of the Repeat/End and variable expand loops so SHOULD be pretty bullet proof. Note that the beginData stuff is global to this module ... the Include/Use directives check to make sure we're not doing that inside a Begin/End. beginData[] is a list which we append to as more Begins are encountered. The placement here is pretty deliberate. Variable expand comes later so you can't macroize BEGIN ... I think this makes sense. The tests for 'begin', 'end' and the appending of the current begin[] stuff have to be here, in this order. """ action = l[0].upper() # 1st arg in line if action == 'BEGIN': if not l[1:]: error("Use: Begin STUFF") beginPoints.append(len(beginData)) beginData.extend(l[1:]) continue if action == 'END': if len(l) > 1: error("No arguments permitted for END") if not beginData: error("No 'BEGIN' for 'END'") beginData = beginData[:beginPoints.pop(-1)] continue if beginData: l = beginData + l action = l[0].upper() if MMA.debug.showExpand and action != 'REPEAT': dPrint(l) if action in simpleFuncs: simpleFuncs[action](l[1:]) continue """ We have several possibilities ... 1. The command is a valid assigned track name, 2. The command is a valid track name, but needs to be dynamically allocated, 3. It's really a chord action """ if not action in gbl.tnames: # no track allocated? trackAlloc(action, 0) # Try to create. Always returns. if action in gbl.tnames: # BASS/DRUM/APEGGIO/CHORD name = action if len(l) < 2: error("Expecting argument after '%s'" % name) action = l[1].upper() # Got trackname and action if action in trackFuncs: # perfect, execute trackFuncs[action](name, l[2:]) continue elif action in simpleFuncs: # opps, not track func error("%s is not a track function. Use global form." % action) else: # opps, not any kind of func error("%s is not a %s track function." % (action, name)) ### Gotta be a chord data line! """ A data line can have an optional bar number at the start of the line. Makes debugging input easier. The next block strips leading integers off the line. Note that a line number on a line by itself it okay. """ if action.isdigit(): # isdigit() matches '1', '1234' but not '1a'! gbl.barLabel = l[0].lstrip('0') l = l[1:] if not l: # ignore empty lines continue else: gbl.barLabel = '' ## A bar can have an optional repeat count. This must ## be at the end of bar in the form '* xx'. if len(l) > 1 and l[-2] == '*': rptcount = stoi(l[-1], "Expecting integer after '*'") l = l[:-2] else: rptcount = 1 # Dataplugins all start with '@'. Code is in the plugin code. # A data plugin modifies the existing data line and returns it. if l[0].startswith('@'): p = l[0].upper() if p not in dataFuncs: error("Unknown data plugin '%s' called." % p) l = dataFuncs[p](l[1:]) # Extract solo(s) from line ... this is anything in {}s. # The solo data is pushed into RIFFs and discarded from # the current line. l = ' '.join(l) l = MMA.patSolo.extractSolo(l, rptcount) # set lyrics from [stuff] in the current line. # NOTE: lyric.extract() inserts previously created # data from LYRICS SET and inserts the chord names # if that flag is active. l, lyrics = MMA.lyric.lyric.extract(l, rptcount) l = l.split() # At this point we have only chord info. A number # of sanity checks are made: # 1. Make sure there is some chord data, # 2. Ensure the correct number of chords. if not l: error("Expecting music (chord) data. Even lines with " "lyrics or solos still need a chord. If you " "don't want a chord use 'z'.") # We now have a chord line. It'll look something like: # # ['Cm', '/', 'z', 'F#@4.5'] or ['/' 'C@3' ] # # For each bar we create a list of CTables, one for each # chord in the line. Each entry has the start/end (in beats), chordname, etc. # ctable = parseChordLine(l) # parse the chord line # Create MIDI data for the bar for rpt in range( rptcount): # for each bar in the repeat count ( Cm * 3) """ Handle global (de)cresc by popping a new volume off stack. """ if MMA.volume.futureVol: MMA.volume.volume = MMA.volume.futureVol.pop(0) if MMA.volume.futureVol: MMA.volume.nextVolume = MMA.volume.futureVol[0] else: MMA.volume.nextVolume = None # Set up for rnd seq. This may set the current seq point. # If return is >=0 then we're doing track rnd. rsq, seqlist = MMA.seqrnd.setseq() """ Process each track. It is important that the track classes are written so that the ctable passed to them IS NOT MODIFIED. This applies especially to chords. If the track class changes the chord, then the function called MUST restore it before returning!!! """ for a in gbl.tnames.values(): if rsq >= 0: seqSave = gbl.seqCount if a.name in seqlist: # for seqrnd with tracklist gbl.seqCount = rsq a.bar(ctable) # process entire bar! if rsq >= 0: # for track rnd gbl.seqCount = seqSave # Adjust counters """ After processsing each bar we update a dictionary of bar pointers. This table is used when the MIDI data is written when -b or -B is set to limit output. """ if MMA.truncate.length: nextOffset = MMA.truncate.length MMA.truncate.countDown() else: nextOffset = gbl.barLen # barPtrs is used by the -B/b options to strip unwanted sections. gbl.barPtrs[gbl.barNum + 1] = [ gbl.barLabel, gbl.tickOffset, gbl.tickOffset + nextOffset - 1 ] gbl.totTime += float(nextOffset / gbl.BperQ) / gbl.tempo gbl.tickOffset += nextOffset if gbl.printProcessed: if gbl.barLabel: gbl.barLabels.append(gbl.barLabel) else: gbl.barLabels.append('?') gbl.barNum += 1 gbl.seqCount = (gbl.seqCount + 1) % gbl.seqSize if gbl.barNum > gbl.maxBars: error( "Capacity exceeded. Maxbar setting is %s. Use -m option" % gbl.maxBars) MMA.grooves.nextGroove() # using groove list? Advance. # Enabled with the -r command line option if MMA.debug.showrun: if lyrics: # we print lyric as a list ly = lyrics # with the []s else: ly = '' # no lyric dPrint("%3d: %s %s" % (gbl.barNum, ' '.join(l), ly)) # if repeat count is set with dupchord we push # the chord back and get lyric.extract to add the # chord to the midi file again. A real lyric is # just ignored ... 2 reasons: the lyric is mangled and # and it makes sense to only have it once! if rptcount > 1 and MMA.lyric.lyric.dupchords: _, lyrics = MMA.lyric.lyric.extract(' '.join(l), 0) # The barNum and other pointers have been incremented # and a bar of data has been processed. If we are repeating # due to a "*" we do a AGAIN test. Without a rpt this would # be done at the start of a data line. if rptcount > 1 and MMA.after.needed(): MMA.after.check(recurse=True)
def midiinc(ln): """ Include a MIDI file into MMA generated files. """ global midifile, offset, octAdjust, volAdjust, firstNote, istart, iend, ignorePC filename = '' doLyric = 0 doText = 0 channels = [] transpose = None stripSilence = 1 report = 0 notopt, ln = opt2pair(ln) if notopt: error("MidiInc expecting cmd=opt pairs, not '%s'." % ' '.join(notopt) ) for cmd, opt in ln: cmd=cmd.upper() if cmd == 'FILE': filename = MMA.file.fixfname(opt) elif cmd == 'VOLUME': volAdjust = stoi(opt) elif cmd == 'OCTAVE': octAdjust = stoi(opt) if octAdjust < -4 or octAdjust > 4: error("Octave adjustment must be -4 to 4, not %s" % opt) octAdjust *= 12 elif cmd == 'TRANSPOSE': transpose = stoi(opt) if transpose < -24 or transpose > 24: error("Transpose must be -24 to 24, not %s" % opt) elif cmd == 'START': istart = stof(opt) elif cmd == 'END': iend = stof(opt) elif cmd == 'TEXT': opt=opt.upper() if opt in ("ON", '1'): doText=1 elif opt in ("OFF", '0'): doText=0 else: error("MidiInc Text= expecting 'ON' or 'OFF'") elif cmd == 'LYRIC': opt=opt.upper() if opt in ("ON", '1'): doLyric=1 elif opt in ("OFF", '0'): doLyric=0 else: error("MidiInc Lyric: expecting 'ON' or 'OFF'") elif cmd == "REPORT": opt=opt.upper() if opt in ("ON", '1'): report=1 elif opt in ("OFF", '0'): report=0 else: error("MidiInc Report: expecting 'ON' or 'OFF'") elif cmd == "STRIPSILENCE": opt=opt.upper() if opt in ("OFF", '0'): stripSilence = 0 elif opt == "ON": # this is the default stripSilence = -1 else: stripSilence = stoi(opt, "MIdiInc StripSilence= expecting 'value', 'On' or 'Off', " "not %s" % opt) elif cmd == "INCLUDEPC": opt=op.upper() if opt in ("TRUE", "ON", "1"): # default ignorePC=1 elif opt in ("FALSE", "OFF", "0"): # use program change in imported ignorePC=0 else: error ("MIdiInc IncludePC= expecting 'True' or 'False', not %s" % opt) # If none of the above matched a CMD we assume that it is # a trackname. Keep this as the last test! else: trackAlloc(cmd, 0) if not cmd in gbl.tnames: error("MidiInc: %s is not a valid MMA track" % cmd) opt = opt.split(',') riffmode=0 printriff=0 ch = None for o in opt: o=o.upper() if o == 'RIFF': riffmode = 1 elif o == 'PRINT': printriff = 1 riffmode = 1 else: if ch != None: error("MidiInc: Only one channel assignment per track.") ch = stoi(o) if ch < 1 or ch > 16: error("MidiInc: MIDI channel for import must be 1..16, not %s" % ch) channels.append( (cmd, ch-1, riffmode, printriff)) if report: # don't bother with channel tests, etc. gbl.noWarn=1 events, textEvs, lyricEvs = readMidi(filename) print "MIDI File %s successfully read." % filename print "Total Text events: %s" % len(textEvs) print "Total Lyric events: %s" % len(lyricEvs) if beatDivision != gbl.BperQ: s = "(MMA uses %s, MidiInc will compensate)" % gbl.BperQ else: s='' print "Ticks per quarter note: %s %s" % (beatDivision, s) print for ch in sorted(events.keys()): if not events[ch]: continue fnote = fevent = 0xffffff ncount = 0 for ev in events[ch]: delta = ev[0] if delta < fevent: fevent = delta if ev[1]>>4 == 0x9: if delta < fnote: fnote = delta if ord(ev[2][1]): ncount +=1 print "Channel %2s: First event %-8s" % (ch+1, fevent), if ncount: print "First Note %-8s Total Notes %-4s" % (fnote, ncount) else: print print print "No data generated!" sys.exit(0) if not channels: if doLyric or doText: warning("MidiInc: no import channels specified, " "only text or lyrics imported") else: error("MidiInc: A channel to import and a destination " "track must be specified") if (istart >= iend) or (istart < 0) or (iend < 0): error("MidiInc range invalid: start=%s, end=%s" % (istart, iend)) if gbl.debug: print "MidiInc: file=%s, Volume=%s, Octave=%s, Transpose=%s, Lyric=%s, " \ "Text=%s, Range=%s..%s StripSilence=%s" \ % (filename, volAdjust, octAdjust, transpose, doLyric, doText, \ istart, iend, stripSilence) for t, ch, riffmode, riffprint in channels: o='' if riffmode: o=',riff' elif printriff: o=',riff,print' print "MidiInc: Channel %s-->%s%s" % (ch+1, t, o), print # If transpose was NOT set, use the global transpose value # Note special riff value as well. Need to double adjust since # the riff import will do its own adjustment. if transpose == None: transpose = gbl.transpose riffTranspose = -gbl.transpose else: riffTranspose = 0 octAdjust += transpose # this takes care of octave and transpose events, textEvs, lyricEvs = readMidi(filename) # Midi file parsed, add selected events to mma data beatad = gbl.BperQ / float(beatDivision) if not stripSilence: firstNote = 0 elif stripSilence > 0: firstNote = stripSilence if doText: inst=0 disc=0 for tm,tx in textEvs: delta = tm-firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addText(gbl.tickOffset + int(delta * beatad), tx) inst+=1 else: disc+=1 if gbl.debug: print"MidiInc text events: %s inserted, %s out of range." % (inst, disc) if doLyric: inst=0 disc=0 for tm, tx in lyricEvs: delta = tm-firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addLyric(gbl.tickOffset + int(delta * beatad), tx) inst+=1 else: disc+=1 if gbl.debug: print"MidiInc lyric events: %s inserted, %s out of range." % (inst, disc) for n,c, riffmode, printriff in channels: if not len(events[c]): warning("No data to assign from imported channel %s to track %s" % (c+1, n)) inst=0 disc=0 for tr, ch, riffmode, printriff in channels: if gbl.tnames[tr].disable: # skip if disabled track continue t=gbl.tnames[tr] if not t.channel: t.setChannel() if riffmode: riff = [] if t.vtype not in ('MELODY', 'SOLO'): error("MidiInc Riff only works on Melody/Solo tracks, not '%s'." % t.name) t.clearPending() if t.voice[0] != t.ssvoice: gbl.mtrks[t.channel].addProgChange( gbl.tickOffset, t.voice[0], t.ssvoice) channel = t.channel track = gbl.mtrks[channel] for ev in events[ch]: delta = ev[0]-firstNote if delta >= istart and delta <= iend: if riffmode: offset = int(delta * beatad) x=ev[1]>>4 if x != 0x09 and x != 0x08: continue pitch=ord(ev[2][0]) velocity=ord(ev[2][1]) if x == 0x8: velocity = 0 riff.append([offset, pitch, velocity]) else: offset = gbl.tickOffset + int(delta * beatad) track.addToTrack( offset, chr(ev[1] | channel-1) + ev[2] ) inst+=1 else: disc+=1 if riffmode: createRiff(riff, tr, printriff, riffTranspose, beatad) if gbl.debug: print"MidiInc events: %s inserted, %s out of range." % (inst, disc)
def trackSequence(name, ln): """ Define a sequence for a track. The format for a sequence: TrackName Seq1 [Seq2 ... ] Note, that SeqX can be a predefined seq or { seqdef } The {} is dynamically interpreted into a def. """ if not ln: error("Use: %s Sequence NAME [...]" % name) ln = ' '.join(ln) self = gbl.tnames[name] # this is the pattern class if self.vtype == "SOLO": warning("Sequences for SOLO tracks are not saved in Grooves.") """ Before we do extraction of {} stuff make sure we have matching {}s. Count the number of { and } and if they don't match read more lines and append. If we get to the EOF then we're screwed and we error out. Only trick is to make sure we do macro expansion! This code lets one have long sequence lines without bothering with '\' continuations. """ oLine = gbl.lineno # in case we error out, report start line while ln.count('{') != ln.count('}'): l = gbl.inpath.read() if l is None: # reached eof, error gbl.lineno = oLine error("%s Sequence {}s do not match" % name) l = ' '.join(macros.expand(l)) if l[-1] != '}' and l[-1] != ';': error("%s: Expecting multiple sequence lines to end in ';'" % name) ln += ' ' + l """ Extract out any {} definitions and assign them to new define variables (__1, __99, etc) and melt them back into the string. """ ids = 1 while 1: sp = ln.find("{") if sp < 0: break ln, s = pextract(ln, "{", "}", onlyone=True) if not s: error("Did not find matching '}' for '{'") pn = "_%s" % ids ids += 1 trk = name.split('-')[0] trackAlloc(trk, 1) """ We need to mung the plectrum classes. Problem is that we define all patterns in the base class (plectrum-banjo is created in PLECTRUM) which is fine, but the def depends on the number of strings in the instrument (set by the tuning option). So, we save the tuning for the base class, copy the real tuning, and restore it. NOTE: at this point the base and current tracks have been initialized. """ if trk == 'PLECTRUM' and name != trk: z = gbl.tnames[trk]._tuning[:] gbl.tnames[trk]._tuning = gbl.tnames[name]._tuning else: z = None gbl.tnames[trk].definePattern(pn, s[0]) # 'trk' is a base class! if z: gbl.tnames[trk]._tuning = z ln = ln[:sp] + ' ' + pn + ' ' + ln[sp:] ln = ln.split() """ We now have a sequence we can save for the track. All the {} defs have been converted to special defines (_1, _2, etc.). First we expand ln to the proper length. lnExpand() also duplicates '/' to the previous pattern. Then we step though ln: - convert 'z', 'Z' and '-' to empty patterns. - duplicate the existing pattern for '*' - copy the defined pattern for everything else. There's a bit of Python reference trickery here. Eg, if we have the line: Bass Sequence B1 B2 the sequence is set with pointers to the existing patterns defined for B1 and B2. Now, if we later change the definitions for B1 or B2, the stored pointer DOESN'T change. So, changing pattern definitions has NO EFFECT. """ ln = lnExpand(ln, '%s Sequence' % self.name) tmp = [None] * len(ln) for i, n in enumerate(ln): n = n.upper() if n in ('Z', '-'): tmp[i] = None elif n == '*': tmp[i] = self.sequence[i] else: p = (self.vtype, n) if not p in pats: error("Track %s does not have pattern '%s'" % p) tmp[i] = pats[p] self.sequence = seqBump(tmp) if gbl.seqshow: msg = ["%s sequence set:" % self.name] for a in ln: if a in "Zz-": msg.append("-") else: msg.append(a) print(' '.join(msg))
def midiinc(ln): """ Include a MIDI file into MMA generated files. """ global midifile, offset, octAdjust, volAdjust, firstNote, ignorePC, verbose filename = '' doLyric = 0 doText = 0 channels = [] transpose = None stripSilence = -1 report = 0 istart = 0 # istart/end are in ticks iend = 0xffffff # but are set in options in Beats verbose = 0 octAdjust = 0 volAdjust = 100 firstNote = 0 ignorePC = 1 stretch = None notopt, ln = opt2pair(ln) if notopt: error("MidiInc: Expecting cmd=opt pairs, not '%s'." % ' '.join(notopt) ) for cmd, opt in ln: cmd=cmd.upper() if cmd == 'FILE': filename = MMA.file.fixfname(opt) elif cmd == 'VOLUME': volAdjust = stoi(opt) elif cmd == 'OCTAVE': octAdjust = stoi(opt) if octAdjust < -4 or octAdjust > 4: error("MidiInc: 'Octave' adjustment must be -4 to 4, not %s" % opt) octAdjust *= 12 elif cmd == 'TRANSPOSE': transpose = stoi(opt) if transpose < -24 or transpose > 24: error("MidiInc: 'Transpose' must be -24 to 24, not %s" % opt) elif cmd == 'START': if opt[-1].upper() == 'M': # measures istart = int( (stof(opt[:-1])-1) * gbl.BperQ * gbl.QperBar) elif opt[-1].upper() == 'T': # ticks istart = int(stof(opt[:-1])) else: # must be digits, stof() catches errors istart = int((stof(opt)-1) * gbl.BperQ) if istart < 0: error("MidiInc: 'Start' must be > 0.") elif cmd == 'END': if opt[-1].upper() == 'M': iend = int( (stof(opt[:-1])-1) * gbl.BperQ * gbl.QperBar) elif opt[-1].upper() == 'T': istart = int(stof(opt[:-1])) else: iend = int((stof(opt)-1) * gbl.BperQ) if iend < 0: error("MidiInc: 'End' must be > 0.") elif cmd == 'TEXT': opt=opt.upper() if opt in ("ON", '1'): doText=1 elif opt in ("OFF", '0'): doText=0 else: error("MidiInc: 'Text' expecting 'ON' or 'OFF'") elif cmd == 'LYRIC': opt=opt.upper() if opt in ("ON", '1'): doLyric=1 elif opt in ("OFF", '0'): doLyric=0 else: error("MidiInc: 'Lyric' expecting 'ON' or 'OFF'") elif cmd == "REPORT": opt=opt.upper() if opt in ("ON", '1'): report=1 elif opt in ("OFF", '0'): report=0 else: error("MidiInc: 'Report' expecting 'ON' or 'OFF'") elif cmd == "VERBOSE": opt=opt.upper() if opt in ("ON", '1'): verbose=1 elif opt in ("OFF", '0'): verbose=0 else: error("MidiInc: 'Verbose' expecting 'ON' or 'OFF'") elif cmd == "STRIPSILENCE": opt=opt.upper() if opt in ("OFF", '0'): stripSilence = 0 elif opt == "ON": # this is the default stripSilence = -1 else: stripSilence = stoi(opt, "MIdiInc StripSilence= expecting 'value', 'On' or 'Off', " "not %s" % opt) elif cmd == "IGNOREPC": opt=op.upper() if opt in ("TRUE", "ON", "1"): # default ignorePC=1 elif opt in ("FALSE", "OFF", "0"): # use program change in imported ignorePC=0 else: error ("MIdiInc: 'IncludePC' expecting 'True' or 'False', not %s" % opt) elif cmd == "STRETCH": v = stof(opt) if v < 1 or v > 500: error("MidiInc: 'Stretch' range of 1 to 500, not %s." % opt) stretch = v/100. # If none of the above matched a CMD we assume that it is # a trackname. Keep this as the last test! else: trackAlloc(cmd, 0) if not cmd in gbl.tnames: error("MidiInc: %s is not a valid MMA track" % cmd) opt = opt.split(',') riffmode=0 printriff=0 ch = None for o in opt: o=o.upper() if o == 'RIFF': riffmode = 1 elif o == 'SEQUENCE': riffmode = 2 elif o == 'PRINT': printriff = 1 if not riffmode: riffmode = 1 else: if ch != None: error("MidiInc: Only one channel assignment per track.") ch = stoi(o) if ch < 1 or ch > 16: error("MidiInc: MIDI channel for import must be 1..16, not %s" % ch) channels.append( (cmd, ch-1, riffmode, printriff)) events, textEvs, lyricEvs = readMidi(filename) if report or verbose: # try to be helpful gbl.noWarn=1 print "MIDI File %s successfully read." % filename print "Total Text events: %s" % len(textEvs) print "Total Lyric events: %s" % len(lyricEvs) print for ch in sorted(events.keys()): if not events[ch]: continue if verbose and not report: # in verbose mode only list info for tracks we're using doit = 0 for z in channels: if z[1] == ch: doit = 1 break if not doit: continue fnote = fevent = 0xffffff ncount = 0 for ev in events[ch]: delta = ev[0] if delta < fevent: fevent = delta if ev[1]>>4 == 0x9: if delta < fnote: fnote = delta if ord(ev[2][1]): ncount +=1 print "Channel %2s: First event %-8s" % (ch+1, fevent), if ncount: print "First Note %-8s Total Notes %-4s" % (fnote, ncount) else: print if report: print "\nNo data generated!" sys.exit(0) if not channels: if doLyric or doText: warning("MidiInc: no import channels specified, " "only text or lyrics imported") else: error("MidiInc: A channel to import and a destination " "track must be specified") if (istart >= iend) or (istart < 0) or (iend < 0): error("MidiInc: Range invalid, start=%s, end=%s" % (istart, iend)) if gbl.debug: print "MidiInc: file=%s, Volume=%s, Octave=%s, Transpose=%s, Lyric=%s, " \ "Text=%s, Range=%s..%s StripSilence=%s Verbose=%s" \ % (filename, volAdjust, octAdjust, transpose, doLyric, doText, \ istart, iend, stripSilence, verbose) for t, ch, riffmode, riffprint in channels: o='' if riffmode==1: o=',riff' elif riffmode == 2: o=',sequence' elif printriff: o+=',print' print "MidiInc: Channel %s-->%s%s" % (ch+1, t, o), print # If transpose was NOT set, use the global transpose value # Note special riff value as well. Need to double adjust since # the riff import will do its own adjustment. if transpose == None: transpose = gbl.transpose riffTranspose = -gbl.transpose else: riffTranspose = 0 octAdjust += transpose # this takes care of octave and transpose if stretch: if verbose: print "Applying stretch to all events. Deltas will be multiplied by", stretch for tr in events: for e in events[tr]: e[0] = int(e[0] * stretch) for e in textEvs: e[0] = int(e[0] * stretch) for e in lyricEvs: e[0] = int(e[0] * stretch) # Midi file parsed, add selected events to mma data if stripSilence == 0: if verbose: print "Firstnote offset was %s. Being reset to start of file by stripSilence." \ % firstNote firstNote = 0 if verbose: print "First note offset: %s" % firstNote if doText: inst=0 disc=0 if verbose: print "Scanning %s textevents." % len(textEvs) for tm,tx in textEvs: delta = tm-firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addText(gbl.tickOffset + delta, tx) inst+=1 else: disc+=1 if gbl.debug: print"MidiInc text events: %s inserted, %s out of range." % (inst, disc) if doLyric: inst=0 disc=0 if verbose: print "Scanning %s LyricEvents." % len(lyricEvs) for tm, tx in lyricEvs: delta = tm-firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addLyric(gbl.tickOffset + delta, tx) inst+=1 else: disc+=1 if gbl.debug: print"MidiInc lyric events: %s inserted, %s out of range." % (inst, disc) for n,c, riffmode, printriff in channels: if not len(events[c]): warning("No data to assign from imported channel %s to track %s" % (c+1, n)) inst=0 disc=0 for tr, ch, riffmode, printriff in channels: if gbl.tnames[tr].disable: # skip if disabled track if verbose: print "Skipping import of channel %s since track %s is disabled." \ % (ch, tr) continue t=gbl.tnames[tr] if not t.channel: t.setChannel() if riffmode: riff = [] if t.vtype not in ('MELODY', 'SOLO'): error("MidiInc: Riff only works on Melody/Solo tracks, not '%s'." % t.name) t.clearPending() if t.voice[0] != t.ssvoice: gbl.mtrks[t.channel].addProgChange( gbl.tickOffset, t.voice[0], t.ssvoice) channel = t.channel track = gbl.mtrks[channel] if verbose: print "Parsing imported file. Channel=%s Track=%s MIDI Channel=%s" \ % (ch, tr, channel) print " Total events: %s; Event range: %s %s; Start/End Range: %s %s" \ % (len(events[ch]),events[ch][0][0], events[ch][-1][0], istart, iend) for ev in events[ch]: delta = ev[0]-firstNote if delta >= istart and delta <= iend: if riffmode: offset = delta-istart x=ev[1]>>4 if x != 0x09 and x != 0x08: continue pitch=ord(ev[2][0]) velocity=ord(ev[2][1]) if x == 0x8: velocity = 0 riff.append([offset, pitch, velocity]) else: offset = gbl.tickOffset + (delta-istart) track.addToTrack( offset, chr(ev[1] | channel-1) + ev[2] ) inst+=1 else: disc+=1 if riffmode: evlist = createRiff(riff, tr, riffTranspose) if riffmode == 2: txt = [] for a in sorted(evlist): if printriff and riffmode == 1: print "%s Riff %s" % (tr, evlist[a]) elif riffmode == 2: # sequence mode, create sequence line and push into input txt.append("{%s}" % evlist[a]) else: # riffmode==1, printriff=0 - just add to the riff stack gbl.tnames[tr].setRiff(evlist[a]) if riffmode == 2 and txt: if printriff: print "%s Sequence %s" % (tr, ' '.join(txt)) else: MMA.sequence.trackSequence(tr, txt) if gbl.debug: print"MidiInc events: %s inserted, %s out of range." % (inst, disc)
def midiinc(ln): """ Include a MIDI file into MMA generated files. """ global midifile, offset, octAdjust, volAdjust, firstNote, ignorePC, verbose filename = '' doLyric = 0 doText = 0 channels = [] transpose = None stripSilence = -1 report = 0 istart = 0 # istart/end are in ticks iend = 0xffffff # but are set in options in Beats verbose = 0 octAdjust = 0 volAdjust = 100 firstNote = 0 ignorePC = 1 stretch = None notopt, ln = opt2pair(ln) if notopt: error("MidiInc: Expecting cmd=opt pairs, not '%s'." % ' '.join(notopt)) for cmd, opt in ln: cmd = cmd.upper() if cmd == 'FILE': filename = MMA.file.fixfname(opt) elif cmd == 'VOLUME': volAdjust = stoi(opt) elif cmd == 'OCTAVE': octAdjust = stoi(opt) if octAdjust < -4 or octAdjust > 4: error("MidiInc: 'Octave' adjustment must be -4 to 4, not %s" % opt) octAdjust *= 12 elif cmd == 'TRANSPOSE': transpose = stoi(opt) if transpose < -24 or transpose > 24: error("MidiInc: 'Transpose' must be -24 to 24, not %s" % opt) elif cmd == 'START': if opt[-1].upper() == 'M': # measures istart = int((stof(opt[:-1]) - 1) * gbl.BperQ * gbl.QperBar) elif opt[-1].upper() == 'T': # ticks istart = int(stof(opt[:-1])) else: # must be digits, stof() catches errors istart = int((stof(opt) - 1) * gbl.BperQ) if istart < 0: error("MidiInc: 'Start' must be > 0.") elif cmd == 'END': if opt[-1].upper() == 'M': iend = int((stof(opt[:-1]) - 1) * gbl.BperQ * gbl.QperBar) elif opt[-1].upper() == 'T': istart = int(stof(opt[:-1])) else: iend = int((stof(opt) - 1) * gbl.BperQ) if iend < 0: error("MidiInc: 'End' must be > 0.") elif cmd == 'TEXT': opt = opt.upper() if opt in ("ON", '1'): doText = 1 elif opt in ("OFF", '0'): doText = 0 else: error("MidiInc: 'Text' expecting 'ON' or 'OFF'") elif cmd == 'LYRIC': opt = opt.upper() if opt in ("ON", '1'): doLyric = 1 elif opt in ("OFF", '0'): doLyric = 0 else: error("MidiInc: 'Lyric' expecting 'ON' or 'OFF'") elif cmd == "REPORT": opt = opt.upper() if opt in ("ON", '1'): report = 1 elif opt in ("OFF", '0'): report = 0 else: error("MidiInc: 'Report' expecting 'ON' or 'OFF'") elif cmd == "VERBOSE": opt = opt.upper() if opt in ("ON", '1'): verbose = 1 elif opt in ("OFF", '0'): verbose = 0 else: error("MidiInc: 'Verbose' expecting 'ON' or 'OFF'") elif cmd == "STRIPSILENCE": opt = opt.upper() if opt in ("OFF", '0'): stripSilence = 0 elif opt == "ON": # this is the default stripSilence = -1 else: stripSilence = stoi( opt, "MIdiInc StripSilence= expecting 'value', 'On' or 'Off', " "not %s" % opt) elif cmd == "IGNOREPC": opt = op.upper() if opt in ("TRUE", "ON", "1"): # default ignorePC = 1 elif opt in ("FALSE", "OFF", "0"): # use program change in imported ignorePC = 0 else: error( "MIdiInc: 'IncludePC' expecting 'True' or 'False', not %s" % opt) elif cmd == "STRETCH": v = stof(opt) if v < 1 or v > 500: error("MidiInc: 'Stretch' range of 1 to 500, not %s." % opt) stretch = v / 100. # If none of the above matched a CMD we assume that it is # a trackname. Keep this as the last test! else: trackAlloc(cmd, 0) if not cmd in gbl.tnames: error("MidiInc: %s is not a valid MMA track" % cmd) opt = opt.split(',') riffmode = 0 printriff = 0 ch = None for o in opt: o = o.upper() if o == 'RIFF': riffmode = 1 elif o == 'SEQUENCE': riffmode = 2 elif o == 'PRINT': printriff = 1 if not riffmode: riffmode = 1 else: if ch != None: error( "MidiInc: Only one channel assignment per track.") ch = stoi(o) if ch < 1 or ch > 16: error( "MidiInc: MIDI channel for import must be 1..16, not %s" % ch) channels.append((cmd, ch - 1, riffmode, printriff)) events, textEvs, lyricEvs = readMidi(filename) if report or verbose: # try to be helpful gbl.noWarn = 1 print "MIDI File %s successfully read." % filename print "Total Text events: %s" % len(textEvs) print "Total Lyric events: %s" % len(lyricEvs) print for ch in sorted(events.keys()): if not events[ch]: continue if verbose and not report: # in verbose mode only list info for tracks we're using doit = 0 for z in channels: if z[1] == ch: doit = 1 break if not doit: continue fnote = fevent = 0xffffff ncount = 0 for ev in events[ch]: delta = ev[0] if delta < fevent: fevent = delta if ev[1] >> 4 == 0x9: if delta < fnote: fnote = delta if ord(ev[2][1]): ncount += 1 print "Channel %2s: First event %-8s" % (ch + 1, fevent), if ncount: print "First Note %-8s Total Notes %-4s" % (fnote, ncount) else: print if report: print "\nNo data generated!" sys.exit(0) if not channels: if doLyric or doText: warning("MidiInc: no import channels specified, " "only text or lyrics imported") else: error("MidiInc: A channel to import and a destination " "track must be specified") if (istart >= iend) or (istart < 0) or (iend < 0): error("MidiInc: Range invalid, start=%s, end=%s" % (istart, iend)) if gbl.debug: print "MidiInc: file=%s, Volume=%s, Octave=%s, Transpose=%s, Lyric=%s, " \ "Text=%s, Range=%s..%s StripSilence=%s Verbose=%s" \ % (filename, volAdjust, octAdjust, transpose, doLyric, doText, \ istart, iend, stripSilence, verbose) for t, ch, riffmode, riffprint in channels: o = '' if riffmode == 1: o = ',riff' elif riffmode == 2: o = ',sequence' elif printriff: o += ',print' print "MidiInc: Channel %s-->%s%s" % (ch + 1, t, o), print # If transpose was NOT set, use the global transpose value # Note special riff value as well. Need to double adjust since # the riff import will do its own adjustment. if transpose == None: transpose = gbl.transpose riffTranspose = -gbl.transpose else: riffTranspose = 0 octAdjust += transpose # this takes care of octave and transpose if stretch: if verbose: print "Applying stretch to all events. Deltas will be multiplied by", stretch for tr in events: for e in events[tr]: e[0] = int(e[0] * stretch) for e in textEvs: e[0] = int(e[0] * stretch) for e in lyricEvs: e[0] = int(e[0] * stretch) # Midi file parsed, add selected events to mma data if stripSilence == 0: if verbose: print "Firstnote offset was %s. Being reset to start of file by stripSilence." \ % firstNote firstNote = 0 if verbose: print "First note offset: %s" % firstNote if doText: inst = 0 disc = 0 if verbose: print "Scanning %s textevents." % len(textEvs) for tm, tx in textEvs: delta = tm - firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addText(gbl.tickOffset + delta, tx) inst += 1 else: disc += 1 if gbl.debug: print "MidiInc text events: %s inserted, %s out of range." % (inst, disc) if doLyric: inst = 0 disc = 0 if verbose: print "Scanning %s LyricEvents." % len(lyricEvs) for tm, tx in lyricEvs: delta = tm - firstNote if delta >= istart and delta <= iend: gbl.mtrks[0].addLyric(gbl.tickOffset + delta, tx) inst += 1 else: disc += 1 if gbl.debug: print "MidiInc lyric events: %s inserted, %s out of range." % ( inst, disc) for n, c, riffmode, printriff in channels: if not len(events[c]): warning("No data to assign from imported channel %s to track %s" % (c + 1, n)) inst = 0 disc = 0 for tr, ch, riffmode, printriff in channels: if gbl.tnames[tr].disable: # skip if disabled track if verbose: print "Skipping import of channel %s since track %s is disabled." \ % (ch, tr) continue t = gbl.tnames[tr] if not t.channel: t.setChannel() if riffmode: riff = [] if t.vtype not in ('MELODY', 'SOLO'): error( "MidiInc: Riff only works on Melody/Solo tracks, not '%s'." % t.name) t.clearPending() if t.voice[0] != t.ssvoice: gbl.mtrks[t.channel].addProgChange(gbl.tickOffset, t.voice[0], t.ssvoice) channel = t.channel track = gbl.mtrks[channel] if verbose: print "Parsing imported file. Channel=%s Track=%s MIDI Channel=%s" \ % (ch, tr, channel) print " Total events: %s; Event range: %s %s; Start/End Range: %s %s" \ % (len(events[ch]),events[ch][0][0], events[ch][-1][0], istart, iend) for ev in events[ch]: delta = ev[0] - firstNote if delta >= istart and delta <= iend: if riffmode: offset = delta - istart x = ev[1] >> 4 if x != 0x09 and x != 0x08: continue pitch = ord(ev[2][0]) velocity = ord(ev[2][1]) if x == 0x8: velocity = 0 riff.append([offset, pitch, velocity]) else: offset = gbl.tickOffset + (delta - istart) track.addToTrack(offset, chr(ev[1] | channel - 1) + ev[2]) inst += 1 else: disc += 1 if riffmode: evlist = createRiff(riff, tr, riffTranspose) if riffmode == 2: txt = [] for a in sorted(evlist): if printriff and riffmode == 1: print "%s Riff %s" % (tr, evlist[a]) elif riffmode == 2: # sequence mode, create sequence line and push into input txt.append("{%s}" % evlist[a]) else: # riffmode==1, printriff=0 - just add to the riff stack gbl.tnames[tr].setRiff(evlist[a]) if riffmode == 2 and txt: if printriff: print "%s Sequence %s" % (tr, ' '.join(txt)) else: MMA.sequence.trackSequence(tr, txt) if gbl.debug: print "MidiInc events: %s inserted, %s out of range." % (inst, disc)
def parse(inpath): """ Process a mma input file. """ global beginData, lastChord gbl.inpath = inpath curline = None while 1: curline = inpath.read() if curline == None: # eof, exit parser break l = macros.expand(curline) """ Handle BEGIN and END here. This is outside of the Repeat/End and variable expand loops so SHOULD be pretty bullet proof. Note that the beginData stuff is global to this module ... the Include/Use directives check to make sure we're not doing that inside a Begin/End. beginData[] is a list which we append to as more Begins are encountered. The placement here is pretty deliberate. Variable expand comes later so you can't macroize BEGIN ... I think this makes sense. The tests for 'begin', 'end' and the appending of the current begin[] stuff have to be here, in this order. """ action = l[0].upper() # 1st arg in line if action == 'BEGIN': if not l[1:]: error("Use: Begin STUFF") beginPoints.append(len(beginData)) beginData.extend(l[1:]) continue if action == 'END': if len(l) > 1: error("No arguments permitted for END") if not beginData: error("No 'BEGIN' for 'END'") beginData = beginData[:beginPoints.pop(-1)] continue if beginData: l = beginData + l action = l[0].upper() if gbl.showExpand and action != 'REPEAT': print l # If the command is in the simple function table, jump & loop. if action in simpleFuncs: simpleFuncs[action](l[1:]) continue """ We have several possibilities ... 1. The command is a valid assigned track name, 2. The command is a valid track name, but needs to be dynamically allocated, 3. It's really a chord action """ if not action in gbl.tnames: trackAlloc(action, 0) # ensure that track is allocated if action in gbl.tnames: # BASS/DRUM/APEGGIO/CHORD name = action if len(l) < 2: error("Expecting argument after '%s'" % name) action = l[1].upper() if action in trackFuncs: trackFuncs[action](name, l[2:]) else: error("Don't know '%s'" % curline) continue ### Gotta be a chord data line! """ A data line can have an optional bar number at the start of the line. Makes debugging input easier. The next block strips leading integers off the line. Note that a line number on a line by itself it okay. """ if action.isdigit(): # isdigit() matches '1', '1234' but not '1a'! barLabel = l[0] l = l[1:] if not l: # ignore empty lines continue else: barLabel = '' """ A bar can have an optional repeat count. This must be at the end of bar in the form '* xx'. """ if len(l) > 1 and l[-2] == '*': rptcount = stoi(l[-1], "Expecting integer after '*'") l = l[:-2] else: rptcount = 1 """ Extract solo(s) from line ... this is anything in {}s. The solo data is pushed into RIFFs and discarded from the current line. """ l = ' '.join(l) l = MMA.patSolo.extractSolo(l, rptcount) """ Set lyrics from [stuff] in the current line. NOTE: lyric.extract() inserts previously created data from LYRICS SET and inserts the chord names if that flag is active. """ l, lyrics = lyric.extract(l, rptcount) l = l.split() """ At this point we have only chord info. A number of sanity checks are made: 1. Make sure there is some chord data, 2. Ensure the correct number of chords. """ if not l: error("Expecting music (chord) data. Even lines with\n" " lyrics or solos still need a chord") """ We now have a chord line. It'll look something like: ['Cm', '/', 'z', 'F#@4.5'] or ['/' 'C@3' ] For each bar we create a list of CTables, one for each chord in the line. Each entry has the start/end (in beats), chordname, etc. """ ctable = parseChordLine(l) # parse the chord line # Create MIDI data for the bar for rpt in range( rptcount): # for each bar in the repeat count ( Cm * 3) """ Handle global (de)cresc by popping a new volume off stack. """ if MMA.volume.futureVol: MMA.volume.volume = MMA.volume.futureVol.pop(0) if MMA.volume.futureVol: MMA.volume.nextVolume = MMA.volume.futureVol[0] else: MMA.volume.nextVolume = None """ Set up for rnd seq. This may set the current seq point. If return is >=0 then we're doing track rnd. """ rsq, seqlist = MMA.seqrnd.setseq() """ Process each track. It is important that the track classes are written so that the ctable passed to them IS NOT MODIFIED. This applies especially to chords. If the track class changes the chord, then the function called MUST restore it before returning!!! """ for a in gbl.tnames.values(): if rsq >= 0: seqSave = gbl.seqCount if a.name in seqlist: # for seqrnd with tracklist gbl.seqCount = rsq a.bar(ctable) ## process entire bar! if rsq >= 0: # for track rnd gbl.seqCount = seqSave # Adjust counters """ After processsing each bar we update a dictionary of bar pointers. This table is used when the MIDI data is written when -b or -B is set to limit output. """ if MMA.truncate.length: nextOffset = MMA.truncate.length MMA.truncate.countDown() else: nextOffset = gbl.QperBar * gbl.BperQ gbl.barPtrs[gbl.barNum + 1] = [ barLabel, gbl.tickOffset, gbl.tickOffset + nextOffset - 1 ] gbl.totTime += float(nextOffset / gbl.BperQ) / gbl.tempo gbl.tickOffset += nextOffset gbl.barNum += 1 gbl.seqCount = (gbl.seqCount + 1) % gbl.seqSize if gbl.barNum > gbl.maxBars: error( "Capacity exceeded. Maxbar setting is %s. Use -m option" % gbl.maxBars) MMA.grooves.nextGroove() # using groove list? Advance. # Enabled with the -r command line option if gbl.showrun: print "%3d:" % gbl.barNum, for c in l: print c, if lyrics: print lyrics, print