diff options
Diffstat (limited to 'City/Tracks.py')
-rwxr-xr-x | City/Tracks.py | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/City/Tracks.py b/City/Tracks.py new file mode 100755 index 0000000..de9e3d9 --- /dev/null +++ b/City/Tracks.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python +""" +This module defines Notes, Scores, Beats and Players and a convenience class for midi file parsing. +TEST THIS PLAYER! +""" + +#Note has no instrument number +#This is determined by the Track +ONSETINDEX = 0 +DURINDEX = 1 +VELINDEX = 2 +PITINDEX = 3 + + +import bisect, random, math, logging, thread +from midiImport import * +from CsHelpers import * + +log = logging.getLogger( 'City run' ) +log.setLevel( logging.DEBUG ) + + +class Note: + def __init__(self, note = [0,0,0,0]): + self.data = note[:] + self.index = 0 + def __repr__(self): + return 'Note: '+' '.join(map(str,(self.data))) + def __getitem__(self,index): + return self.data[index] + def __setitem__(self,index, value): + self.data[index] = value + def __getslice__(self,a,b): + return self.data[a:b] + def __len__(self): + return len(self.data) + def onset(self): + return self[ONSETINDEX] + def duration(self): + return self[DURINDEX] + def velocity(self): + return self[VELINDEX] + def pitch(self): + return self[PITINDEX] + def setOnset(self, val): + self[ONSETINDEX] = val + def setDuration(self, val): + self[DURINDEX] = val + def setVelocity(self, val): + self[VELINDEX] = val + def setPitch(self, val): + self[PITINDEX] = val + def offTime(self): + return self.duration() + self.onset() + + +class Track: + def __init__(self, data = []): + "A Track can be initialised with a list of notes" + self.data = data[:] + self.index = 0 + self.instrument = 1 + def setInstrument(self, num): + self.instrument = num + def addNote(self, note): + index = bisect.bisect_left(self.data, [note[ONSETINDEX]]) + self.data.insert(index, note) + def __delitem__(self, index): + del(self.data[index]) + def __getitem__(self, index): + if len(self) == 0: + return False + else: + return self.data[index] + def __getslice__(self, a,b): + return Track(self.data[a:b]) + def __len__(self): + return len(self.data) + def resetIterator(self, ndx = 0): + self.index = ndx + def findNoteAtTime(self, time): + return bisect.bisect_left([n.data for n in self.data], [time]) + def getNoteAtTime(self, time): + return self.data[self.findNoteAtTime(time)] + def getParameter(self, ndx): + return [i[ndx] for i in self.data] + def getOnsets(self): + return self.getParameter(ONSETINDEX) + def getDurations(self): + return self.getParameter(DURINDEX) + def getVelocities(self): + return self.getParameter(VELINDEX) + def getPitches(self): + return self.getParameter(PITINDEX) + def modParameter(self, pndx, mult): + "a bit of a hack, while I try and work out tempo" + for n in self: + n[pndx] *= mult + def __repr__(self): + return 'Track :' + str(self.data) + + +class Beat(Track): + def __init__(self, beatnum, offset = 0, data = Track()): + "Beat can be initialised with an existing track. The track is automatically sliced" + self.data = data.data[:] + self.index = 0 + self.instrument = 1 + self.offset = offset + self.beatnum = beatnum + def setBeatNum(self, beatnum): + self.beatnum = beatnum + def setOffset(self, beatnum): + self.offset = offset + def nudgeParameter(self, parameter, shiftAmount): + "returns a new beat with onsets shifted by shiftAmount" + newbeat = Beat(self.beatnum) + for i in self.data: + newp = i[parameter] + shiftAmount + newl = [] + for p in range(len(i)): + if p == parameter: + newl.append(newp) + else: newl.append(i[p]) + newNote = Note(newl) + newbeat.addNote(newNote) + return newbeat + def relativeOnsets(self, start = 0): + shift = -self[0].onset() - self.offset + return self.nudgeParameter(ONSETINDEX, shift + start) + def __repr__(self): + return 'Beat ' + str(self.beatnum) + ':' + str(self.data) + +def beat(track, time, beatlen, beatnum): + "beat function extracts a beat object from a track, and assigns it a 'beat' number" + ndx = track.findNoteAtTime(time) + result = Beat(beatnum) + if ndx >= len(track): return result + timeOffset = track[ndx].onset() - time + if timeOffset > beatlen: + return result + else: + for n in track[ndx: ]: + if n.onset() < track[ndx].onset() + beatlen: + result.addNote(n) + else: break + return result + +class Midi2Score: + def __init__(self, path): + midiData = MidiFile() + midiData.open(path) + midiData.read() + midiData.close() + self.midiData = midiData + self.ticksPerBeat = midiData.ticksPerQuarterNote + def numTracks(self): + return len(self.midiData.tracks) + def getTrack(self, ndx): + return self.midiData.tracks[ndx] + def time2beats(self,time): + return float(time) / self.ticksPerBeat + def findNO(self, noff, nlst): + pitlst = [n.pitch() for n in reversed(nlst)] + return (len(pitlst) - 1) - pitlst.index(noff.pitch) + def midiTrack2Notes(self, track): + "returns a list of notes from a track" + result = [] + for event in track.events: + if event.type == "NOTE_ON": + result.append(Note([self.time2beats(event.time), 0, event.velocity, event.pitch])) + elif event.type == "NOTE_OFF": + ndx = self.findNO(event, result) + dur = self.time2beats(event.time) - result[ndx].onset() + result[ndx].setDuration(dur) + else: + pass + return result + def midiTrack2ScoreTrack(self, mtrack): + "returns a Track object from a midi file" + notelist = self.midiTrack2Notes(mtrack) + return Track(notelist) + + +class Scale: + "a class for manipulating pitch data" + def __init__(self, keyname, modality): + "where keyname is a letter and modality is a string e.g. Scale('C#','minor'))" + self.key = keyname + self.modality = modality + self.chromatic = range(12, 109) + self.keyMap = {'C':0, 'C#':1,'D':2, 'D#':3, 'E':4, 'F':5, 'F#':6, 'G':7, 'G#':8,'A':9,'A#':10, 'B':11} + self.scale = self.Scalemap(self.Mode(keyname, modality)) + self.subscale = self.Scalemap(self.Mode(keyname, "subscale")) + def Scalemap(self, scale): + lim = True + oct = 0 + result = [] + stop = len(self.chromatic) + while lim: + for i in scale: + ndx = i + oct * 12 + if ndx >= stop: + lim = False + break + else: + result.append(self.chromatic[ndx]) + oct += 1 + return result + def Transpose(self, incr, mode): + result = [(n + incr) % 12 for n in mode] + result.sort() + return result + def Mode(self, key, name): + if name == "chromatic": + return range(12) + elif name == "major": + return self.Transpose(self.keyMap[key], [0,2,4,5,7,9,11]) + elif name == "minor": + return self.Transpose(self.keyMap[key], [0,2,3,5,7,8,10]) + elif name == "minor pentatonic": + return self.Transpose(self.keyMap[key], [0,2,3,5,7,8]) + elif name == "subscale": + return self.Transpose(self.keyMap[key], [0,2,5,7]) + else: return [] + def pitchMap(self, pitch, subscale = False): + "Integers only. Given a pitch, recalculates a new pitch within the key and scale" + if subscale: + scale = self.subscale + else: + scale = self.scale + if pitch in scale: + return pitch + else: + pup = pitch + 1 + pdown = pitch - 1 + result = None + for i in xrange(len(scale)): + if pup in scale: + result = pup + break + elif pdown in scale: + result = pdown + break + else: + pup += 1 + pdown -= 1 + return result + def basePit(self, strack): + "return the lowest tonic note in the track" + tonics = filter(lambda x: x % 12 == self.keyMap[self.key], strack.getPitches()) + if tonics: + return min(tonics) + else: + return self.keyMap[self.key] + 48 + + +class beatDebugPlayer: + "This player expects beats to be relative to zero already" + TRACKDATA_NDX = 0 + INSNUM_NDX = 1 + def __init__(self, cssynth, timer, perimeter, scaleObj): + "params is the parameter object" + print "THREAD ID: PLAYER INIT" , thread.get_ident() + self.cs = cssynth + self.beat = 0 + self.scoreTracks = {} + self.drumPitches = [] + self.trackMap = {} + self.activity = {} + self.timer = timer + self.perimeter = perimeter + self.scale = scaleObj + self.basepits = {} + self.tempoMult = 1 #the tempo() method changes this + self.den = 0 + self.beatlimit = 0 + self.sendSync = False + self.loop_idcounter = 1 + self.mutelist = [] + self.picture_cycle = [] + self.frozen = False + self.mute_all = False + def pause(self): + self.mute_all = True + def oldpause(self, *lid): + if lid and self.activity.has_key(lid[0]): + self.activity[lid[0]] = 'Pause' + return True + else: + #if no loop id is specified, or the id is just plain wrong, try and stop the last loop added set to play. + for k in sorted(self.activity.keys(), reverse = True): + if self.activity[k] == 'Play': + self.activity[k] = 'Pause' + return True + else: + return False + def Stop(self): + self.activity = 'Stop' + self.resetBeat() + self.cs.Stop() + def Cease(self, *lid): + if lid and self.activity.has_key(lid[0]): + self.activity[lid[0]] = 'Cease' + return True + else: + for k in sorted(self.activity.keys(), reverse = True): + if self.activity[k] == 'Play' or self.activity[k] == 'Pause': + self.activity[k] = 'Cease' + log.info("Ceasing loop id %s" %k) + return True + else: + return False + def resume(self): + self.mute_all = False + def oldresume(self, *lid): + if lid and self.activity.has_key(lid[0]): + self.activity[lid[0]] = 'Play' + return True + else: + for k in sorted(self.activity.keys(), reverse = True): + if self.activity[k] == 'Pause': + self.activity[k] = 'Play' + print "resuming id ", k + return True + else: + return False + def resetBeat(self, *num): + if num: + self.beat = num[0] + else: + self.beat = 0 + def Track2beatList(self, Strack, beatlen, tracklen): + "return a list of beats from the track, reletavised to zero" + result = [] + for i in range(tracklen): + if i > len(Strack) - 1: + result.append(Beat(i)) + else: + result.append(beat(Strack, i, beatlen - 0.01, i)) + return [(b.relativeOnsets(b[0].onset() % 1) if b[0] else b) for b in result] + def beatInstrumentMap(self, beatlength, **kargs): + "kargs is a dictionary. Assign beatlists to Csound instruments kbeats. Keys = 5" + for key in kargs: + strack = kargs[key][self.TRACKDATA_NDX] + self.scoreTracks[key] = strack + if key == 'Drums': + self.drumPitches = sorted(list(set(self.scoreTracks["Drums"].getPitches()))) + tracklen = int(sorted(list(set(self.scoreTracks["Drums"].getOnsets())))[-1]) + while tracklen % 4: + tracklen += 1 + kargs[key][self.TRACKDATA_NDX] = self.Track2beatList(strack, beatlength, tracklen) + self.basepits[key] = self.scale.basePit(strack) + self.trackMap = kargs + self.beatlimit = self.beatLimit() + def getInstrument(self, trackname): + "Get an Instrument associated with the track" + return self.trackMap[trackname][self.INSNUM_NDX] + def getBeatData(self, trackname): + "The following three functions ought to be improved" + return self.trackMap[trackname][self.TRACKDATA_NDX] + def beatLists(self): + "return a list of all the trackdata" + return [self.getBeatData(keyname) for keyname in self.trackMap.keys()] + def beatLimit(self): + return len(self.beatLists()[0]) + def setBPM(self, bpm): + "tempomult * 1 = 120bpm. Turns out ticksPerQuarter in midiImport.py always returns 480 " + print "bpm, " ,bpm, bpm / 120.0 + self.tempoMult = (60.0 / bpm) + def tempo(self): + "sets current tempo" + print "tempo" + tempoparam = self.perimeter.getValue('Tempo', 'Drums') #Only look at one parameter, since tempo is global. + self.tempoMult = 1/(tempoparam + 1.5) + def articulate(self, insname): + if insname == 'Drums': + return 1 + else: + pval = self.perimeter.getValue('Length', insname) + 0.5 + if pval < 1: + return pval + else: + return rescale(pval, 1, 1.5, 1, 6) + def noteChecker(self, keyname, t, modulo, varGreaterThan, varLessThan): + dval = self.density(keyname) + densval = (dval + (random.uniform(-.112, .112)) if dval != 1 or 0 else (0.99 if dval == 1 else 0.01)) + return (True if t % modulo == 0 and varGreaterThan <= limit(densval, 0, 1) < varLessThan else False) + def density(self, insname): + "could introduce some randomness here" + if insname == "Keys": + regions = [0, 1, 0.75, 2] + else: + regions = [0, 0.5, 0.5, 1, 0.75, 2] + val = 1 - self.perimeter.getValue('Density', insname) + valnew = rescale(val, 0, 1, 0, len(regions) - 1) + valnew -= random.uniform(0, 0.6) + if valnew < 0: valnew = 0 + return regions[int(round(valnew))] + def pitchCalc(self, insname, pitch, onset): + "Calculate a new pitch based on the old" + pitchparam = self.perimeter.getValue('Pitch', insname) + if pitchparam > 0.4 and pitchparam < 0.6: + return pitch + if insname == 'Drums': + l = self.drumPitches + if pitchparam <= 0.4: + if pitch in l[0:int(math.ceil(len(l) * (pitchparam + 0.01) * 2))]: + return pitch + else: + return False + elif pitchparam >= 0.6: + if pitch in l[int(math.floor((len(l) - 1) * (pitchparam - 0.5) * 2)):len(l)]: + return pitch + else: return False + else: + return False + else: + basepit = self.basepits[insname] + pitchparam = self.perimeter.getValue('Pitch', insname) + newpitch = basepit + (pitch - basepit) * pitchparam * 2 + if onset % 1 == 0: + return self.scale.pitchMap(int(round(newpitch)), True) + else: + return self.scale.pitchMap(int(round(newpitch))) + def freeze(self): + "stop access to all input into the player, usually done when reloading a scene" + self.cs.perf.SetProcessCallback(lambda: None, None) + for lids in self.activity: + self.activity[lids] = 'Cease' + self.frozen = True + print "frozen = ", self.frozen + def playBeat(self, time, beatnum): + if self.frozen: return None + if self.sendSync: + self.timer.schedEvent(time, olpcgames.mesh.broadcast, "Beat|%s" %beatnum) + self.sendSync = False + if self.picture_cycle: + activity = self.picture_cycle[0] + pics = activity.snap_store + updated = self.picture_cycle[1] + if not pics: + pass + elif updated: + ndx = len(pics) - 1 + activity.feedbackgroundImage = pics[ndx] + else: + ndx = beatnum % len(pics) + activity.feedbackgroundImage = pics[ndx] + self.picture_cycle[1] = False + for keyname in [i for i in self.trackMap.keys() if i not in self.mutelist]: + ins = self.getInstrument(keyname) + beatCollect = self.getBeatData(keyname)[beatnum] + if beatCollect: + for note in beatCollect: + den = self.density(keyname) + pitch = self.pitchCalc(keyname, note[PITINDEX], note[ONSETINDEX]) + if not pitch: + pass + elif den == 0: + self.cs.playParams(ins, (note[ONSETINDEX] * self.tempoMult) + (time - self.cs.perfTime()), + (note[DURINDEX] * self.articulate(keyname) if keyname == 'Drums' or keyname == 'Keys' + else note[DURINDEX] * self.tempoMult * self.articulate(keyname)), + note[VELINDEX], + pitch) + elif (note[ONSETINDEX] + beatCollect.beatnum) % den == 0: + self.cs.playParams(ins, (note[ONSETINDEX] * self.tempoMult) + (time - self.cs.perfTime()), + max(note[DURINDEX] * self.tempoMult, (den * 0.8) * 0.5) * self.articulate(keyname), + note[VELINDEX], + pitch) + else: + pass + def playLoop(self, time, reset_beat = False, loop_ID = False): + if self.frozen: return None + elif loop_ID: + lid = loop_ID + else: + lid = self.loop_idcounter + self.activity.update({lid:'Play'}) + self.loop_idcounter += 1 + if reset_beat: + self.beat = reset_beat + #self.tempo() # was used to dynamically calculate tempo. Now disabled. + if self.mute_all: + self.timer.schedEvent(time + 0.5 * self.tempoMult, self.playLoop, time + 1 * self.tempoMult, False, lid) + elif self.activity[lid] == 'Play': + self.playBeat(time, self.beat) + self.beat = (self.beat + 1) % self.beatlimit + self.timer.schedEvent(time, self.playLoop, time + (1 * self.tempoMult), False, lid) + elif self.activity[lid] == 'Pause': + self.timer.schedEvent(time + 0.5 * self.tempoMult, self.playLoop, time + 1 * self.tempoMult, False, lid) + elif self.activity[lid] == 'Cease': + pass + else: + self.resume() + + +if __name__ == '__main__': + print "running test code for Tracks.py \n" + mpath = raw_input("enter a path to a midi file :") + mfile = Midi2Score(mpath) + print "just reading track one for now..." + + + + + + + + + + |