Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/City/Tracks.py
diff options
context:
space:
mode:
Diffstat (limited to 'City/Tracks.py')
-rwxr-xr-xCity/Tracks.py512
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..."
+
+
+
+
+
+
+
+
+
+