diff --git a/examples/ex-basics.py b/examples/ex-basics.py index 7287028..3929049 100755 --- a/examples/ex-basics.py +++ b/examples/ex-basics.py @@ -1,30 +1,29 @@ #!/usr/bin/python - -from isobar import * +import isobar as iso # create a repeating sequence with scalar transposition: # [ 36, 38, 43, 39, ... ] -a = PSeq([ 0, 2, 7, 3 ]) + 36 +a = iso.PSeq([0, 2, 7, 3]) + 36 # apply pattern-wise transposition # [ 36, 50, 43, 51, ... ] -a = a + PSeq([ 0, 12 ]) +a = a + iso.PSeq([0, 12]) # create a geometric chromatic series, repeated back and forth -b = PSeries(0, 1, 12) + 72 -b = PPingPong(b) -b = PLoop(b) +b = iso.PSeries(0, 1, 12) + 72 +b = iso.PPingPong(b) +b = iso.PLoop(b) # create an velocity series, with emphasis every 4th note, # plus a random walk to create gradual dynamic changes -amp = PSeq([ 50, 35, 25, 35 ]) + PBrown(0, 1, -20, 20) +amp = iso.PSeq([50, 35, 25, 35]) + iso.PBrown(0, 1, -20, 20) # a Timeline schedules events at a given BPM. # by default, send these over the first MIDI output. -timeline = Timeline(120, debug = True) +output_device = iso.io.midi.MidiOut("IAC Driver IAC Bus 1") +timeline = iso.Timeline(120, device=output_device, debug=True) # assign each of our Patterns to particular properties -timeline.sched({ 'note': a, 'dur': 1, 'gate': 2 }) -timeline.sched({ 'note': b, 'dur': 0.25, 'amp': amp }) +timeline.sched({'note': a, 'dur': 1, 'gate': 2}) timeline.run() diff --git a/examples/ex-lsystem-grapher.py b/examples/ex-lsystem-grapher.py index a22b293..32687b0 100755 --- a/examples/ex-lsystem-grapher.py +++ b/examples/ex-lsystem-grapher.py @@ -9,11 +9,11 @@ import random import time -seq = PLSys("N[+N+N]?N[-N]+N", depth = 3) +seq = PLSys("N[+N+N]?N[-N]+N", depth=3) notes = seq.all() note_min = min(notes) note_max = max(notes) for note in seq: - note = note - note_min - print "#" * note + note = note - note_min + print "#" * note diff --git a/examples/ex-lsystem-rhythm.py b/examples/ex-lsystem-rhythm.py index 572f498..5f754e7 100755 --- a/examples/ex-lsystem-rhythm.py +++ b/examples/ex-lsystem-rhythm.py @@ -18,12 +18,12 @@ # - = transpose down one semitone # [ = enter recursive branch # ] = leave recursive branch -notes = PLSys("N+[+N+N--N+N]+N[++N]", depth = 4) +notes = PLSys("N+[+N+N--N+N]+N[++N]", depth=4) notes = notes + 60 # use another l-system to generate time intervals. # take absolute values so that intervals are always positive. -times = PLSys("[N+[NN]-N+N]+N-N+N", depth = 3) +times = PLSys("[N+[NN]-N+N]+N-N+N", depth=3) times = PAbs(PDiff(times)) * 0.25 # delay = PDelay(notes, times) @@ -32,5 +32,5 @@ velocity = PAbs(velocity) timeline = Timeline(120) -timeline.sched({ 'note' : notes, 'amp' : velocity, 'dur' : times }) +timeline.sched({'note': notes, 'amp': velocity, 'dur': times}) timeline.run() diff --git a/examples/ex-lsystem-stochastic.py b/examples/ex-lsystem-stochastic.py index eeecabe..006bbb7 100755 --- a/examples/ex-lsystem-stochastic.py +++ b/examples/ex-lsystem-stochastic.py @@ -10,7 +10,7 @@ import random import time -notes = PLSys("N[+N--?N]+N[+?N]", depth = 4) +notes = PLSys("N[+N--?N]+N[+?N]", depth=4) notes = PDegree(notes, Scale.majorPenta) notes = notes % 36 + 52 @@ -19,5 +19,5 @@ timeline = Timeline(120) timeline.output(midi) -timeline.sched({ 'note': notes, 'dur': 0.25 }) +timeline.sched({'note': notes, 'dur': 0.25}) timeline.run() diff --git a/examples/ex-markov-learner.py b/examples/ex-markov-learner.py index efbb0a9..051aeb8 100755 --- a/examples/ex-markov-learner.py +++ b/examples/ex-markov-learner.py @@ -2,7 +2,7 @@ # parallel markov chain learner: # 1. takes MIDI input, and constructs three markov chains for pitch, duration -# and amplitude. +# and amplitude. # 2. after receiving a low "C", plays back melodies which are statistically # similar to the input. # after francois pachet's "continuator". @@ -12,7 +12,7 @@ import time -m_in = MidiIn() +m_in = MidiIn() m_out = MidiOut() learner = MarkovLParallel(3) @@ -29,7 +29,7 @@ dur = clock - clock0 dur = round(dur, 2) - learner.register([ note.midinote, round(note.velocity, -1), dur ]) + learner.register([note.midinote, round(note.velocity, -1), dur]) clock0 = clock print "----------------------------------------------------" @@ -38,12 +38,11 @@ chains = learner.chains() pitch = PMarkov(chains[0]) -amp = PMarkov(chains[1]) -dur = PMarkov(chains[2]) +amp = PMarkov(chains[1]) +dur = PMarkov(chains[2]) t = Timeline(120, m_out) print " - nodes: %s" % p.markov.nodes print " - edges: %s" % p.markov.edges -t.sched({ 'note': pitch, 'dur': dur, 'amp': amp, 'channel': 0 }) +t.sched({'note': pitch, 'dur': dur, 'amp': amp, 'channel': 0}) t.run() - diff --git a/examples/ex-permut-degree.py b/examples/ex-permut-degree.py index f0d54e0..2b79e22 100755 --- a/examples/ex-permut-degree.py +++ b/examples/ex-permut-degree.py @@ -5,23 +5,35 @@ import random # create a pitch line comprised of multiple permutations on a pelog scale -ppitch = PShuffle([ random.randint(-6, 6) for n in range(6) ]) +ppitch = PShuffle([random.randint(-6, 6) for n in range(6)]) ppitch = PPermut(ppitch) ppitch = PDegree(ppitch, Key("F#", "pelog")) # create permuted sets of durations and amplitudes # different lengths mean poly-combinations -pdur = PShuffle([ 1, 1, 2, 2, 4 ], 1) -pdur = PPermut(pdur) * 0.25 +pdur = PShuffle([1, 1, 2, 2, 4], 1) +pdur = PPermut(pdur) * 0.25 -pamp = PShuffle([ 10, 15, 20, 35 ], 2) -pamp = PPermut(pamp) +pamp = PShuffle([10, 15, 20, 35], 2) +pamp = PPermut(pamp) # schedule on a 60bpm timeline and send to MIDI output timeline = Timeline(60) -timeline.sched({ 'note': ppitch + 60, 'dur': pdur, 'channel': 0, 'gate': 1, 'amp': pamp }) -timeline.sched({ 'note': ppitch + 24, 'dur': pdur * 4, 'channel': 1, 'gate': 2, 'amp': pamp }) -timeline.sched({ 'note': ppitch + 72, 'dur': pdur / 2, 'channel': 1, 'gate': 1, 'amp': pamp / 2 }) +timeline.sched({'note': ppitch + 60, + 'dur': pdur, + 'channel': 0, + 'gate': 1, + 'amp': pamp}) +timeline.sched({'note': ppitch + 24, + 'dur': pdur * 4, + 'channel': 1, + 'gate': 2, + 'amp': pamp}) +timeline.sched({'note': ppitch + 72, + 'dur': pdur / 2, + 'channel': 1, + 'gate': 1, + 'amp': pamp / 2}) # add some continuous warping warp = PWRamp(2, PBrown(-0.1, 0.01, -0.15, -0.05)) diff --git a/examples/ex-piano-phase.py b/examples/ex-piano-phase.py index fd276ac..1e9d67f 100755 --- a/examples/ex-piano-phase.py +++ b/examples/ex-piano-phase.py @@ -9,7 +9,7 @@ from isobar import * # melody line -seq = PSeq([ -7, -5, 0, 2, 3, -5, -7, 2, 0, -5, 3, 2 ]) +seq = PSeq([-7, -5, 0, 2, 3, -5, -7, 2, 0, -5, 3, 2]) # create a timeline at 160BPM timeline = Timeline(160) @@ -17,9 +17,12 @@ # schedule two identical melodies. # we must copy the note sequence or else the position will be stepped # by two every note... try removing the .copy() and see what happens! -timeline.sched({ 'note': seq.copy() + 60, 'dur': 0.5 }) -timeline.sched({ 'note': seq.copy() + 72, 'dur': 0.5 * 1.01 }) +timeline.sched({'note': seq.copy() + 60, 'dur': 0.5}) +timeline.sched({'note': seq.copy() + 72, 'dur': 0.5 * 1.01}) # start playing, and block forever. # alternatively, use timeline.background() to retain foreground control. -timeline.run() +timeline.background() + +while True: + pass diff --git a/examples/ex-subsequence.py b/examples/ex-subsequence.py index 605d361..367d7c7 100755 --- a/examples/ex-subsequence.py +++ b/examples/ex-subsequence.py @@ -2,9 +2,9 @@ from isobar import * -scale = Scale([ 0, 2, 3, 6, 7 ]) +scale = Scale([0, 2, 3, 6, 7]) scale = Scale.pelog -seq = PDegree(PBrown(0, 3, -12, 12, repeats = False), scale) +seq = PDegree(PBrown(0, 3, -12, 12, repeats=False), scale) offset = PStutter(PWhite(0, 4), 2) seq = PSubsequence(seq, offset, 4) @@ -12,14 +12,19 @@ seq = seq + 64 seq = PReset(seq, PImpulse(24)) -amp = PSeq([ 45, 35, 25, 40 ]) + PBrown(0, 1, -15, 10) -amp = PSeq([ 45, 35, 35, 40 ]) + PBrown(0, 1, -15, 10) +amp = PSeq([45, 35, 25, 40]) + PBrown(0, 1, -15, 10) +amp = PSeq([45, 35, 35, 40]) + PBrown(0, 1, -15, 10) gate = PBrown(1.5, 0.01, 0.6, 2.5) timeline = Timeline(120) -timeline.sched({ 'note': seq, 'amp': amp, 'dur': 0.25, 'gate': gate }) -timeline.sched({ 'note': seq.copy() + 24, 'amp': amp.copy(), 'dur': 0.5, 'gate': gate.copy() }) -timeline.sched({ 'note': seq.copy() - 24, 'amp': 10 + amp.copy() * 0.5, 'dur': PChoice([ 4, 4, 6, 8 ]), 'gate': gate.copy() }) +timeline.sched({'note': seq, 'amp': amp, 'dur': 0.25, 'gate': gate}) +timeline.sched({'note': seq.copy() + 24, + 'amp': amp.copy(), + 'dur': 0.5, + 'gate': gate.copy()}) +timeline.sched({'note': seq.copy() - + 24, 'amp': 10 + + amp.copy() * + 0.5, 'dur': PChoice([4, 4, 6, 8]), 'gate': gate.copy()}) timeline.run() - diff --git a/examples/ex-walk.py b/examples/ex-walk.py index bd4fab1..cc9637d 100755 --- a/examples/ex-walk.py +++ b/examples/ex-walk.py @@ -5,15 +5,15 @@ #------------------------------------------------------------------------ # walk up and down a minor scale #------------------------------------------------------------------------ -scale = Scale([ 0, 2, 3, 7, 9, 11 ]) -degree = PBrown(0, 2, -8, 16, repeats = False) +scale = Scale([0, 2, 3, 7, 9, 11]) +degree = PBrown(0, 2, -8, 16, repeats=False) notes = PDegree(degree, scale) + 60 #------------------------------------------------------------------------ # add a slight 4/4 emphasis and moderate variation in velocity #------------------------------------------------------------------------ -amp = PSeq([ 40, 30, 20, 25 ]) + PBrown(0, 2, -10, 10) +amp = PSeq([40, 30, 20, 25]) + PBrown(0, 2, -10, 10) timeline = Timeline(170) -timeline.sched({ 'note': notes, 'dur': 0.25, 'gate': 0.9, 'amp' : amp }) +timeline.sched({'note': notes, 'dur': 0.25, 'gate': 0.9, 'amp': amp}) timeline.run() diff --git a/isobar/__init__.py b/isobar/__init__.py index 139a0d0..0863498 100644 --- a/isobar/__init__.py +++ b/isobar/__init__.py @@ -1,17 +1,17 @@ __version__ = "0" __author__ = "Daniel Jones " -from isobar.pattern import * -from isobar.note import * -from isobar.scale import * -from isobar.chord import * -from isobar.key import * -from isobar.util import * -from isobar.timeline import * +from pattern import * +from note import * +from scale import * +from chord import * +from key import * +from util import * +from timeline import * import sys -FOREVER = sys.maxint +FOREVER = sys.maxsize # REST = -sys.maxint - 1 # END = -sys.maxint + 1 diff --git a/isobar/chord.py b/isobar/chord.py index 66e8ae9..a7d2075 100644 --- a/isobar/chord.py +++ b/isobar/chord.py @@ -1,57 +1,63 @@ import random import copy + class Chord: - """ Represents a chord made up of 1 or more note intervals. - """ - - dict = { } - - def __init__(self, intervals = [], root = 0, name = "unnamed chord"): - self.intervals = intervals - self.name = name - self.root = root - if not Chord.dict.has_key(name): - Chord.dict[name] = self - - def __str__(self): - return "%s %s%s" % (self.name, self.semitones(), (" + %d" % self.root) if self.root > 0 else "") - - @property - def semitones(self): - semitones = [0] + map(lambda n: sum(self.intervals[0:n+1]), range(len(self.intervals))) - return semitones - - @staticmethod - def byname(name): - return Chord.dict[name] - - @staticmethod - def random(): - key = random.choice(Chord.dict.keys()) - c = copy.deepcopy(Chord.dict[key]) - c.root = random.randint(0, 12) - return c - - @staticmethod - def arbitrary(name = "chord"): - intervals_poss = [ 2, 3, 3, 4, 4, 5, 6 ] - intervals = [] - top = random.randint(12, 18) - n = 0 - while True: - interval = random.choice(intervals_poss) - n += interval - if n > top: - break - intervals.append(interval) - - return Chord(intervals, 0, name if name else "chord", random.randint(0, 12)) - -Chord.major = Chord([ 4, 3, 5 ], 0, "major") -Chord.minor = Chord([ 3, 4, 5 ], 0, "minor") -Chord.diminished = Chord([ 3, 3, 6 ], 0, "diminished") -Chord.augmented = Chord([ 4, 4, 4 ], 0, "diminished") - -Chord.sus4 = Chord([ 5, 2, 5 ], 0, "sus4") -Chord.sus2 = Chord([ 7, 2, 5 ], 0, "sus4") + + """ Represents a chord made up of 1 or more note intervals. + """ + + dict = {} + + def __init__(self, intervals=[], root=0, name="unnamed chord"): + self.intervals = intervals + self.name = name + self.root = root + if name not in Chord.dict: + Chord.dict[name] = self + + def __str__(self): + return "%s %s%s" % (self.name, + self.semitones(), + (" + %d" % self.root) if self.root > 0 else "") + + @property + def semitones(self): + semitones = [ + 0] + map(lambda n: sum(self.intervals[0:n + 1]), range(len(self.intervals))) + return semitones + + @staticmethod + def byname(name): + return Chord.dict[name] + + @staticmethod + def random(): + key = random.choice(Chord.dict.keys()) + c = copy.deepcopy(Chord.dict[key]) + c.root = random.randint(0, 12) + return c + + @staticmethod + def arbitrary(name="chord"): + intervals_poss = [2, 3, 3, 4, 4, 5, 6] + intervals = [] + top = random.randint(12, 18) + n = 0 + while True: + interval = random.choice(intervals_poss) + n += interval + if n > top: + break + intervals.append(interval) + + return Chord( + intervals, 0, name if name else "chord", random.randint(0, 12)) + +Chord.major = Chord([4, 3, 5], 0, "major") +Chord.minor = Chord([3, 4, 5], 0, "minor") +Chord.diminished = Chord([3, 3, 6], 0, "diminished") +Chord.augmented = Chord([4, 4, 4], 0, "diminished") + +Chord.sus4 = Chord([5, 2, 5], 0, "sus4") +Chord.sus2 = Chord([7, 2, 5], 0, "sus4") diff --git a/isobar/io/midi.py b/isobar/io/midi.py index b4154c3..4f598cf 100644 --- a/isobar/io/midi.py +++ b/isobar/io/midi.py @@ -1,118 +1,124 @@ try: - import rtmidi + import mido except: - print "rtmidi not found, no MIDI support available." - -import random -import time + print "mido not found, no MIDI support available." import isobar -from isobar.note import * MIDIIN_DEFAULT = "IAC Driver A" MIDIOUT_DEFAULT = "IAC Driver A" class MidiIn: - def __init__(self, target = MIDIOUT_DEFAULT): - self.midi = rtmidi.MidiIn() - - #------------------------------------------------------------------------ - # don't ignore MIDI clock messages (is on by default) - #------------------------------------------------------------------------ - self.midi.ignore_types(timing = False) - self.clocktarget = None - ports = self.midi.get_ports() - if len(ports) == 0: - raise Exception, "No MIDI output ports found" + def __init__(self, target=MIDIOUT_DEFAULT): + self.midi = None + # don't ignore MIDI clock messages (is on by default) + # TODO: check + # self.midi.ignore_types(timing=False) + self.clocktarget = None - for index, name in enumerate(ports): - if name == target: - isobar.log("Found MIDI input (%s)", name) - self.midi.open_port(index) + ports = mido.get_input_names() + if len(ports) == 0: + raise Exception("No MIDI output ports found") - if self.midi is None: - isobar.log("Could not find MIDI source %s, using default", target) - self.midi.open_port(0) + for name in ports: + if name == target: + isobar.log("Found MIDI input (%s)", name) + self.midi = mido.open_input(name) - def callback(self, message, timestamp): - message = message[0] - data_type = message[0] + if self.midi is None: + isobar.log("Could not find MIDI source %s, using default", target) + self.midi = mido.open_input() - if data_type == 248: - if self.clocktarget is not None: - self.clocktarget.tick() + def callback(self, message, timestamp): + message = message[0] + data_type = message[0] - elif data_type == 250: - # TODO: is this the right midi code? - if self.clocktarget is not None: - self.clocktarget.reset_to_beat() + if data_type == 248: + if self.clocktarget is not None: + self.clocktarget.tick() - # print "%d %d (%d)" % (data_type, data_note, data_vel) + elif data_type == 250: + # TODO: is this the right midi code? + if self.clocktarget is not None: + self.clocktarget.reset_to_beat() + # print "%d %d (%d)" % (data_type, data_note, data_vel) - def run(self): - self.midi.set_callback(self.callback) + def run(self): + self.midi.set_callback(self.callback) - def poll(self): - """ used in markov-learner -- can we refactor? """ - message = self.midi.get_message() - if not message: - return + def poll(self): + """ used in markov-learner -- can we refactor? """ + message = self.midi.get_message() + if not message: + return - print message - data_type, data_note, data_vel = message[0] + print message + data_type, data_note, data_vel = message[0] - if (data_type & 0x90) > 0 and data_vel > 0: - # note on - return Note(data_note, data_vel) + if (data_type & 0x90) > 0 and data_vel > 0: + # note on + return isobar.Note(data_note, data_vel) - def close(self): - del self.midi + def close(self): + del self.midi class MidiOut: - def __init__(self, target = MIDIOUT_DEFAULT): - self.midi = rtmidi.MidiOut() - - ports = self.midi.get_ports() - if len(ports) == 0: - raise Exception, "No MIDI output ports found" - - for index, name in enumerate(ports): - if name == target: - isobar.log("Found MIDI output (%s)" % name) - self.midi.open_port(index) - - if self.midi is None: - print "Could not find MIDI target %s, using default" % target - self.midi.open_port(0) - - def tick(self, ticklen): - pass - - def noteOn(self, note = 60, velocity = 64, channel = 0): - if self.debug: - print "[midi] channel %d, noteOn: (%d, %d)" % (channel, note, velocity) - self.midi.send_message([ 0x90 + channel, int(note), int(velocity) ]) - - def noteOff(self, note = 60, channel = 0): - if self.debug: - print "[midi] channel %d, noteOff: %d" % (channel, note) - self.midi.send_message([ 0x80 + channel, int(note), 0 ]) - - def allNotesOff(self, channel = 0): - if self.debug: - print "[midi] channel %d, allNotesOff" - for n in range(128): - self.noteOff(n, channel) - - def control(self, control = 0, value = 0, channel = 0): - print "*** [CTRL] channel %d, control %d: %d" % (channel, control, value) - if self.debug: - print "[midi] channel %d, control %d: %d" % (channel, control, value) - self.midi.send_message([ 0xB0 + channel, int(control), int(value) ]) - - def __destroy__(self): - del self.midi + + def __init__(self, target=MIDIOUT_DEFAULT): + self.midi = None + self.debug = False + ports = mido.get_output_names() + if len(ports) == 0: + raise Exception("No MIDI output ports found") + + for name in ports: + if name == target: + isobar.log("Found MIDI output (%s)" % name) + self.midi = mido.open_output(name) + + if self.midi is None: + print "Could not find MIDI target %s, using default" % target + self.midi = mido.open_output() + + def tick(self, ticklen): + pass + + def noteOn(self, note=60, velocity=64, channel=0): + if self.debug: + print "[midi] channel %d, noteOn: (%d, %d)" % ( + channel, note, velocity + ) + msg = mido.Message( + 'note_on', channel=channel, note=note, velocity=velocity + ) + self.midi.send(msg) + + def noteOff(self, note=60, channel=0): + if self.debug: + print "[midi] channel %d, noteOff: %d" % (channel, note) + msg = mido.Message( + 'note_off', channel=channel, note=note + ) + self.midi.send(msg) + + def allNotesOff(self, channel=0): + if self.debug: + print "[midi] channel %d, allNotesOff" + for n in range(128): + self.noteOff(n, channel) + + def control(self, control=0, value=0, channel=0): + print "*** [CTRL] channel %d, control %d: %d" % (channel, control, value) + if self.debug: + print "[midi] channel %d, control %d: %d" % (channel, control, value) + msg = mido.Message( + 'control_change', channel=channel, control=control, value=value + ) + self.midi.send(msg) + + def __destroy__(self): + del self.midi diff --git a/isobar/io/midifile.py b/isobar/io/midifile.py index 28219fb..511012e 100644 --- a/isobar/io/midifile.py +++ b/isobar/io/midifile.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import absolute_import from isobar.note import * from isobar.pattern.core import * @@ -6,194 +6,220 @@ import midi + class MidiFileIn: - def __init__(self): - pass - - def read(self, filename, quantize = 0.25): - reader = midi.FileReader() - data = reader.read(file(filename)) - - class Note: - def __init__(self, pitch, velocity, location, duration = None): - # pitch = MIDI 0..127 - self.pitch = pitch - # velocity = MIDI 0..127 - self.velocity = velocity - # location in time, beats - self.location = location - # duration in time, beats - self.duration = duration - - notes = [] - for track in data: - track.make_ticks_abs() - for event in filter(lambda event: isinstance(event, midi.events.NoteEvent), track): - location = event.tick / 96.0 - if isinstance(event, midi.events.NoteOnEvent) and event.velocity > 0: - print "(%.2f beats) %s. \t(note = %d, velocity = %d)" % (location, miditoname(event.pitch), event.pitch, event.velocity) - - #------------------------------------------------------------------------ - # anomaly: sometimes a midi file might have a note on when the previous - # note has not finished; end it automatically - #------------------------------------------------------------------------ - for note in reversed(notes): - if note.pitch == event.pitch: - if not note.duration: - print "note-on found without note-off; cancelling previous note" - duration = location - note.location - note.duration = duration - if duration == 0: - print "*** DURATION OF ZERO??" - break - - note = Note(event.pitch, event.velocity, location) - notes.append(note) - - #------------------------------------------------------------------------ - # A NoteOn event with velocity == 0 also acts as a NoteOff. - #------------------------------------------------------------------------ - if isinstance(event, midi.events.NoteOffEvent) or (isinstance(event, midi.events.NoteOnEvent) and event.velocity == 0): - print "(%.2f beats) %s off \t(note = %d, velocity = %d)" % (location, miditoname(event.pitch), event.pitch, event.velocity) - found = False - for note in reversed(notes): - if note.pitch == event.pitch: - duration = location - note.location - # print " -> duration = %.2f beats" % duration - note.duration = duration - found = True - break - if not found: - print "*** NOTE-OFF FOUND WITHOUT NOTE-ON ***" - - #------------------------------------------------------------------------ - # Construct a sequence which honours chords and relative lengths. - # First, group all notes by their starting time. - #------------------------------------------------------------------------ - notes_by_time = {} - for note in notes: - print "(%.2f) %d/%d, %s" % (note.location, note.pitch, note.velocity, note.duration) - location = note.location - if quantize is not None: - location = round(location / quantize) * quantize - if location in notes_by_time: - notes_by_time[location].append(note) - else: - notes_by_time[location] = [ note ] - - note_dict = { - "note" : [], - "amp" : [], - "gate" : [], - "dur" : [] - } - - #------------------------------------------------------------------------ - # Next, iterate through groups of notes chronologically, figuring out - # appropriate parameters for duration (eg, inter-note distance) and - # gate (eg, proportion of distance note extends across). - #------------------------------------------------------------------------ - times = sorted(notes_by_time.keys()) - for i in range(len(times)): - time = times[i] - notes = notes_by_time[time] - - #------------------------------------------------------------------------ - # Our duration is always determined by the time of the next note event. - # If a next note does not exist, this is the last note of the sequence; - # use the maximal length of note currently playing (assuming a chord) - #------------------------------------------------------------------------ - if i < len(times) - 1: - next_time = times[i + 1] - else: - next_time = time + max([ note.duration for note in notes ]) - - dur = next_time - time - note_dict["dur"].append(dur) - - if len(notes) > 1: - note_dict["note"].append(tuple(note.pitch for note in notes)) - note_dict["amp"].append(tuple(note.velocity for note in notes)) - note_dict["gate"].append(tuple(note.duration / dur for note in notes)) - else: - note = notes[0] - note_dict["note"].append(note.pitch) - note_dict["amp"].append(note.velocity) - note_dict["gate"].append(note.duration / dur) - - return note_dict + + def __init__(self): + pass + + def read(self, filename, quantize=0.25): + reader = midi.FileReader() + data = reader.read(file(filename)) + + class Note: + + def __init__(self, pitch, velocity, location, duration=None): + # pitch = MIDI 0..127 + self.pitch = pitch + # velocity = MIDI 0..127 + self.velocity = velocity + # location in time, beats + self.location = location + # duration in time, beats + self.duration = duration + + notes = [] + for track in data: + track.make_ticks_abs() + for event in filter( + lambda event: isinstance( + event, + midi.events.NoteEvent), + track): + location = event.tick / 96.0 + if isinstance( + event, + midi.events.NoteOnEvent) and event.velocity > 0: + print "(%.2f beats) %s. \t(note = %d, velocity = %d)" % (location, miditoname(event.pitch), event.pitch, event.velocity) + + #---------------------------------------------------------- + # anomaly: sometimes a midi file might have a note on when the previous + # note has not finished; end it automatically + #---------------------------------------------------------- + for note in reversed(notes): + if note.pitch == event.pitch: + if not note.duration: + print "note-on found without note-off; cancelling previous note" + duration = location - note.location + note.duration = duration + if duration == 0: + print "*** DURATION OF ZERO??" + break + + note = Note(event.pitch, event.velocity, location) + notes.append(note) + + #-------------------------------------------------------------- + # A NoteOn event with velocity == 0 also acts as a NoteOff. + #-------------------------------------------------------------- + if isinstance( + event, + midi.events.NoteOffEvent) or ( + isinstance( + event, + midi.events.NoteOnEvent) and event.velocity == 0): + print "(%.2f beats) %s off \t(note = %d, velocity = %d)" % (location, miditoname(event.pitch), event.pitch, event.velocity) + found = False + for note in reversed(notes): + if note.pitch == event.pitch: + duration = location - note.location + # print " -> duration = %.2f beats" % duration + note.duration = duration + found = True + break + if not found: + print "*** NOTE-OFF FOUND WITHOUT NOTE-ON ***" + + #---------------------------------------------------------------------- + # Construct a sequence which honours chords and relative lengths. + # First, group all notes by their starting time. + #---------------------------------------------------------------------- + notes_by_time = {} + for note in notes: + print "(%.2f) %d/%d, %s" % (note.location, note.pitch, note.velocity, note.duration) + location = note.location + if quantize is not None: + location = round(location / quantize) * quantize + if location in notes_by_time: + notes_by_time[location].append(note) + else: + notes_by_time[location] = [note] + + note_dict = { + "note": [], + "amp": [], + "gate": [], + "dur": [] + } + + #---------------------------------------------------------------------- + # Next, iterate through groups of notes chronologically, figuring out + # appropriate parameters for duration (eg, inter-note distance) and + # gate (eg, proportion of distance note extends across). + #---------------------------------------------------------------------- + times = sorted(notes_by_time.keys()) + for i in range(len(times)): + time = times[i] + notes = notes_by_time[time] + + #------------------------------------------------------------------ + # Our duration is always determined by the time of the next note event. + # If a next note does not exist, this is the last note of the sequence; + # use the maximal length of note currently playing (assuming a chord) + #------------------------------------------------------------------ + if i < len(times) - 1: + next_time = times[i + 1] + else: + next_time = time + max([note.duration for note in notes]) + + dur = next_time - time + note_dict["dur"].append(dur) + + if len(notes) > 1: + note_dict["note"].append(tuple(note.pitch for note in notes)) + note_dict["amp"].append(tuple(note.velocity for note in notes)) + note_dict["gate"].append( + tuple( + note.duration / + dur for note in notes)) + else: + note = notes[0] + note_dict["note"].append(note.pitch) + note_dict["amp"].append(note.velocity) + note_dict["gate"].append(note.duration / dur) + + return note_dict + class MidiFileOut: - """ Write events to a MIDI file. - Requires the MIDIUtil package: - https://code.google.com/p/midiutil/ """ - - def __init__(self, numtracks = 16): - from midiutil.MidiFile import MIDIFile - - self.score = MIDIFile(numtracks) - self.track = 0 - self.channel = 0 - self.volume = 64 - self.time = 0 - - def tick(self, ticklen): - self.time += ticklen - - def noteOn(self, note = 60, velocity = 64, channel = 0, duration = 1): - #------------------------------------------------------------------------ - # avoid rounding errors - #------------------------------------------------------------------------ - time = round(self.time, 5) - self.score.addNote(channel, channel, note, time, duration, velocity) - - def noteOff(self, note = 60, channel = 0): - time = round(self.time, 5) - self.score.addNote(channel, channel, note, time, duration, 0) - - def writeFile(self, filename = "score.mid"): - fd = open(filename, 'wb') - self.score.writeFile(fd) - fd.close() + + """ Write events to a MIDI file. + Requires the MIDIUtil package: + https://code.google.com/p/midiutil/ """ + + def __init__(self, numtracks=16): + from midiutil.MidiFile import MIDIFile + + self.score = MIDIFile(numtracks) + self.track = 0 + self.channel = 0 + self.volume = 64 + self.time = 0 + + def tick(self, ticklen): + self.time += ticklen + + def noteOn(self, note=60, velocity=64, channel=0, duration=1): + #---------------------------------------------------------------------- + # avoid rounding errors + #---------------------------------------------------------------------- + time = round(self.time, 5) + self.score.addNote(channel, channel, note, time, duration, velocity) + + def noteOff(self, note=60, channel=0): + time = round(self.time, 5) + self.score.addNote(channel, channel, note, time, duration, 0) + + def writeFile(self, filename="score.mid"): + fd = open(filename, 'wb') + self.score.writeFile(fd) + fd.close() + class PatternWriterMIDI: - """ Writes a pattern to a MIDI file. - Requires the MIDIUtil package: - https://code.google.com/p/midiutil/ """ - - def __init__(self, numtracks = 1): - from midiutil.MidiFile import MIDIFile - - self.score = MIDIFile(numtracks) - self.track = 0 - self.channel = 0 - self.volume = 64 - - def addTrack(self, pattern, tracknumber = 0, trackname = "track", dur = 1.0): - time = 0 - # naive approach: assume every duration is 1 - # TODO: accept dicts or PDicts - try: - for note in pattern: - vdur = Pattern.value(dur) - if note is not None and vdur is not None: - self.score.addNote(tracknumber, self.channel, note, time, vdur, self.volume) - time += vdur - else: - time += vdur - except StopIteration: - # a StopIteration exception means that an input pattern has been exhausted. - # catch it and treat the track as completed. - pass - - def addTimeline(self, timeline): - # TODO: translate entire timeline into MIDI - # difficulties: need to handle degree/transpose params - # need to handle channels properly, and reset numtracks - pass - - def writeFile(self, filename = "score.mid"): - fd = open(filename, 'wb') - self.score.writeFile(fd) - fd.close() + """ Writes a pattern to a MIDI file. + Requires the MIDIUtil package: + https://code.google.com/p/midiutil/ """ + + def __init__(self, numtracks=1): + from midiutil.MidiFile import MIDIFile + + self.score = MIDIFile(numtracks) + self.track = 0 + self.channel = 0 + self.volume = 64 + + def addTrack(self, pattern, tracknumber=0, trackname="track", dur=1.0): + time = 0 + # naive approach: assume every duration is 1 + # TODO: accept dicts or PDicts + try: + for note in pattern: + vdur = Pattern.value(dur) + if note is not None and vdur is not None: + self.score.addNote( + tracknumber, + self.channel, + note, + time, + vdur, + self.volume) + time += vdur + else: + time += vdur + except StopIteration: + # a StopIteration exception means that an input pattern has been exhausted. + # catch it and treat the track as completed. + pass + + def addTimeline(self, timeline): + # TODO: translate entire timeline into MIDI + # difficulties: need to handle degree/transpose params + # need to handle channels properly, and reset numtracks + pass + + def writeFile(self, filename="score.mid"): + fd = open(filename, 'wb') + self.score.writeFile(fd) + fd.close() diff --git a/isobar/io/osc.py b/isobar/io/osc.py index 123e8cd..5b5fc61 100644 --- a/isobar/io/osc.py +++ b/isobar/io/osc.py @@ -8,43 +8,44 @@ MIDIIN_DEFAULT = "IAC Driver A" MIDIOUT_DEFAULT = "IAC Driver A" + class OSCOut: - """ OSCOut: Wraps MIDI messages in OSC. - /note [ note, velocity, channel ] - /control [ control, value, channel ] """ - def __init__(self, host = "localhost", port = 7000): - self.osc = OSCClient() - self.osc.connect((host, port)) + """ OSCOut: Wraps MIDI messages in OSC. + /note [ note, velocity, channel ] + /control [ control, value, channel ] """ - def tick(self, ticklen): - pass + def __init__(self, host="localhost", port=7000): + self.osc = OSCClient() + self.osc.connect((host, port)) - def noteOn(self, note = 60, velocity = 64, channel = 0): - msg = OSCMessage("/note") - msg.extend([ note, velocity, channel ]) - self.osc.send(msg) + def tick(self, ticklen): + pass - def noteOff(self, note = 60, channel = 0): - msg = OSCMessage("/note") - msg.extend([ note, 0, channel ]) - self.osc.send(msg) + def noteOn(self, note=60, velocity=64, channel=0): + msg = OSCMessage("/note") + msg.extend([note, velocity, channel]) + self.osc.send(msg) - def allNotesOff(self, channel = 0): - for n in range(128): - self.noteOff(n, channel) + def noteOff(self, note=60, channel=0): + msg = OSCMessage("/note") + msg.extend([note, 0, channel]) + self.osc.send(msg) - def control(self, control, value, channel = 0): - msg = OSCMessage("/control") - msg.extend([ control, value, channel ]) - self.osc.send(msg) + def allNotesOff(self, channel=0): + for n in range(128): + self.noteOff(n, channel) - def __destroy__(self): - self.osc.close() + def control(self, control, value, channel=0): + msg = OSCMessage("/control") + msg.extend([control, value, channel]) + self.osc.send(msg) - def send(self, address, params = None): - msg = OSCMessage(address) - msg.extend(params) - print "osc: %s (%s)" % (address, params) - self.osc.send(msg) + def __destroy__(self): + self.osc.close() + def send(self, address, params=None): + msg = OSCMessage(address) + msg.extend(params) + print "osc: %s (%s)" % (address, params) + self.osc.send(msg) diff --git a/isobar/io/socketio.py b/isobar/io/socketio.py index ef65a60..227981a 100644 --- a/isobar/io/socketio.py +++ b/isobar/io/socketio.py @@ -4,39 +4,40 @@ from isobar.note import * + class SocketIOOut: - """ SocketIOOut: Support for sending note on/off events via websockets. - Two types of event are sent at the moment: - note [ index, velocity, channel ] : The MIDI note number depressed. - For note-off, velocity is zero. - control [ index, value, channel ] : A MIDI control value - """ + """ SocketIOOut: Support for sending note on/off events via websockets. + Two types of event are sent at the moment: - def __init__(self, host = "localhost", port = 9000): - self.socket = socketIO_client.SocketIO(host, port) + note [ index, velocity, channel ] : The MIDI note number depressed. + For note-off, velocity is zero. + control [ index, value, channel ] : A MIDI control value + """ - def tick(self, ticklen): - pass + def __init__(self, host="localhost", port=9000): + self.socket = socketIO_client.SocketIO(host, port) - def event(self, event): - # import pprint - # pprint.pprint(event) - self.socket.emit("event", event) + def tick(self, ticklen): + pass - def noteOn(self, note = 60, velocity = 64, channel = 0): - self.socket.emit("note", note, velocity, channel) + def event(self, event): + # import pprint + # pprint.pprint(event) + self.socket.emit("event", event) - def noteOff(self, note = 60, channel = 0): - self.socket.emit("note", note, 0, channel) + def noteOn(self, note=60, velocity=64, channel=0): + self.socket.emit("note", note, velocity, channel) - def allNotesOff(self, channel = 0): - for n in range(128): - self.noteOff(n, channel) + def noteOff(self, note=60, channel=0): + self.socket.emit("note", note, 0, channel) - def control(self, control, value, channel = 0): - self.socket.emit("control", control, value, channel) + def allNotesOff(self, channel=0): + for n in range(128): + self.noteOff(n, channel) - def __destroy__(self): - self.socket.close() + def control(self, control, value, channel=0): + self.socket.emit("control", control, value, channel) + def __destroy__(self): + self.socket.close() diff --git a/isobar/key.py b/isobar/key.py index 9ca26b6..1b34156 100644 --- a/isobar/key.py +++ b/isobar/key.py @@ -4,119 +4,122 @@ import random + class Key: - """ Represents a harmonic structure, containing a tonic and scale. - """ - - def __init__(self, tonic = 0, scale = Scale.major): - if type(tonic) == str: - tonic = nametomidi(tonic) - if type(scale) == str: - scale = Scale.byname(scale) - - self.tonic = tonic - self.scale = scale - - def __eq__(self, other): - return self.tonic == other.tonic and self.scale == other.scale - - def __str__(self): - return "key: %s %s" % (miditopitch(self.tonic), self.scale.name) - - def __repr__(self): - return 'Key(%s, "%s")' % (self.tonic, self.scale.name) - - def get(self, degree): - """ Returns the th semitone within this key. """ - if degree is None: - return None - - semitone = self.scale[degree] - return semitone + self.tonic - - def __getitem__(self, degree): - return self.get(degree) - - def __contains__(self, semitone): - return (semitone % self.scale.octave_size) in self.semitones - - @property - def semitones(self): - semitones = map(lambda n: (n + self.tonic) % self.scale.octave_size, self.scale.semitones) - semitones.sort() - return semitones - - def nearest_note(self, note): - if note in self: - return note - else: - octave, pitch = divmod(note, self.scale.octave_size) - nearest_semi = None - nearest_dist = None - for semi in self.semitones: - dist = abs(semi - pitch) - if nearest_dist is None or dist < nearest_dist: - nearest_semi = semi - nearest_dist = dist - return (octave * self.scale.octave_size) + nearest_semi - - def voiceleading(self, other): - """ Returns the most parsimonious voice leading between this key - and , as a list of N tuples (semiA, semiB) where N is the - maximal length of (this, other), and semiA and semiB are members - of each. May not be bijective. """ - - if len(self.semitones) > len(other.semitones): - semisA = self.semitones - semisB = other.semitones - else: - semisA = other.semitones - semisB = self.semitones - semisB = list(reversed(semisB)) - - leading = [] - for semiA in semisA: - distances = [] - for semiB in semisB: - distance = abs(semiA - semiB) - if distance > self.scale.octave_size / 2: - distance = self.scale.octave_size - distance - distances.append(distance) - index = distances.index(min(distances)) - leading.append((semiA, semisB[index])) - - return leading - - def distance(self, other): - leading = self.voiceleading(other) - distance = sum(map(lambda (a, b): abs(a - b), leading)) - return distance - - def fadeto(self, other, level): - """ level between 0..1 """ - semitones_a = self.semitones() - semitones_b = other.semitones() - semitones_shared = filter(lambda n: n in semitones_a, semitones_b) - semitones_a_only = filter(lambda n: n not in semitones_b, semitones_a) - semitones_b_only = filter(lambda n: n not in semitones_a, semitones_b) - - if level < 0.5: - # scale from 1..0 - level = 1.0 - (level * 2) - count_from_a = int(round(level * len(semitones_a_only))) - return semitones_shared + semitones_a_only[0:count_from_a] - else: - # scale from 0..1 - level = 2 * (level - 0.5) - count_from_b = int(round(level * len(semitones_b_only))) - return semitones_shared + semitones_b_only[0:count_from_b] - - @staticmethod - def random(): - t = random.randint(0, 11) - s = Scale.random() - return Key(t, s) - - @staticmethod - def all(): - return [ Key(note, scale) for note in Note.all() for scale in Scale.all() ] + + """ Represents a harmonic structure, containing a tonic and scale. + """ + + def __init__(self, tonic=0, scale=Scale.major): + if isinstance(tonic, str): + tonic = nametomidi(tonic) + if isinstance(scale, str): + scale = Scale.byname(scale) + + self.tonic = tonic + self.scale = scale + + def __eq__(self, other): + return self.tonic == other.tonic and self.scale == other.scale + + def __str__(self): + return "key: %s %s" % (miditopitch(self.tonic), self.scale.name) + + def __repr__(self): + return 'Key(%s, "%s")' % (self.tonic, self.scale.name) + + def get(self, degree): + """ Returns the th semitone within this key. """ + if degree is None: + return None + + semitone = self.scale[degree] + return semitone + self.tonic + + def __getitem__(self, degree): + return self.get(degree) + + def __contains__(self, semitone): + return (semitone % self.scale.octave_size) in self.semitones + + @property + def semitones(self): + semitones = sorted(map(lambda n: (n + self.tonic) % + self.scale.octave_size, self.scale.semitones)) + return semitones + + def nearest_note(self, note): + if note in self: + return note + else: + octave, pitch = divmod(note, self.scale.octave_size) + nearest_semi = None + nearest_dist = None + for semi in self.semitones: + dist = abs(semi - pitch) + if nearest_dist is None or dist < nearest_dist: + nearest_semi = semi + nearest_dist = dist + return (octave * self.scale.octave_size) + nearest_semi + + def voiceleading(self, other): + """ Returns the most parsimonious voice leading between this key + and , as a list of N tuples (semiA, semiB) where N is the + maximal length of (this, other), and semiA and semiB are members + of each. May not be bijective. """ + + if len(self.semitones) > len(other.semitones): + semisA = self.semitones + semisB = other.semitones + else: + semisA = other.semitones + semisB = self.semitones + semisB = list(reversed(semisB)) + + leading = [] + for semiA in semisA: + distances = [] + for semiB in semisB: + distance = abs(semiA - semiB) + if distance > self.scale.octave_size / 2: + distance = self.scale.octave_size - distance + distances.append(distance) + index = distances.index(min(distances)) + leading.append((semiA, semisB[index])) + + return leading + + def distance(self, other): + leading = self.voiceleading(other) + distance = sum(map(lambda a_b: abs(a_b[0] - a_b[1]), leading)) + return distance + + def fadeto(self, other, level): + """ level between 0..1 """ + semitones_a = self.semitones() + semitones_b = other.semitones() + semitones_shared = filter(lambda n: n in semitones_a, semitones_b) + semitones_a_only = filter(lambda n: n not in semitones_b, semitones_a) + semitones_b_only = filter(lambda n: n not in semitones_a, semitones_b) + + if level < 0.5: + # scale from 1..0 + level = 1.0 - (level * 2) + count_from_a = int(round(level * len(semitones_a_only))) + return semitones_shared + semitones_a_only[0:count_from_a] + else: + # scale from 0..1 + level = 2 * (level - 0.5) + count_from_b = int(round(level * len(semitones_b_only))) + return semitones_shared + semitones_b_only[0:count_from_b] + + @staticmethod + def random(): + t = random.randint(0, 11) + s = Scale.random() + return Key(t, s) + + @staticmethod + def all(): + return [Key(note, scale) for note in Note.all() + for scale in Scale.all()] diff --git a/isobar/note.py b/isobar/note.py index d612569..c99137b 100644 --- a/isobar/note.py +++ b/isobar/note.py @@ -1,24 +1,23 @@ -import math - from isobar.util import * + class Note(object): - names = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ] + names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] - def __init__(self, midinote = 60, velocity = 64, length = 1): - self.midinote = midinote - self.velocity = velocity - self.length = length + def __init__(self, midinote=60, velocity=64, length=1): + self.midinote = midinote + self.velocity = velocity + self.length = length - def __str__(self): - if self.velocity == 0: - return "rest" + def __str__(self): + if self.velocity == 0: + return "rest" - return miditoname(self.midinote) + return miditoname(self.midinote) - @staticmethod - def all(): - return Note.names + @staticmethod + def all(): + return Note.names Note.rest = Note(0, 0, 0) diff --git a/isobar/pattern/automate.py b/isobar/pattern/automate.py index d2f46ad..19667ea 100644 --- a/isobar/pattern/automate.py +++ b/isobar/pattern/automate.py @@ -2,25 +2,28 @@ import math + class PAutomate(Pattern): - pass + pass + class PASine(PAutomate): - def __init__(self, length = 1, amp = 0.5): - self.length = length - self.amp = amp - self.pos = 0.0 - - def tick(self, ticklen): - pos_norm = self.pos / self.length - rv = math.sin(2.0 * math.pi * pos_norm) * self.amp - #------------------------------------------------------------------------ - # normalize to [0, 1] - #------------------------------------------------------------------------ - rv = 0.5 * rv + 0.5 - - self.pos += ticklen - if self.pos > self.length: - self.pos -= self.length - - return rv + + def __init__(self, length=1, amp=0.5): + self.length = length + self.amp = amp + self.pos = 0.0 + + def tick(self, ticklen): + pos_norm = self.pos / self.length + rv = math.sin(2.0 * math.pi * pos_norm) * self.amp + #---------------------------------------------------------------------- + # normalize to [0, 1] + #---------------------------------------------------------------------- + rv = 0.5 * rv + 0.5 + + self.pos += ticklen + if self.pos > self.length: + self.pos -= self.length + + return rv diff --git a/isobar/pattern/chance.py b/isobar/pattern/chance.py index 15d7a82..1dad2e5 100644 --- a/isobar/pattern/chance.py +++ b/isobar/pattern/chance.py @@ -5,318 +5,355 @@ from isobar.pattern.core import * from isobar.util import * + class PWhite(Pattern): - """ PWhite: White noise between and . - If values are given as floats, output values are also floats < max. - If values are ints, output values are ints <= max (as random.randint) - - >>> PWhite(0, 10).nextn(16) - [8, 10, 8, 1, 7, 3, 1, 9, 9, 3, 2, 10, 7, 5, 10, 4] - - >>> PWhite(0.0, 10.0).nextn(16) - [3.6747936220022082, 0.61313530428271923, 9.1515368696591555, ... 6.2963694390145974 ] - """ - def __init__(self, min, max = None): - self.min = min - self.max = max - - # also support a 1-argument case: PWhite(max) - if self.max is None: - self.max = min - self.min = -self.max - - def next(self): - min = self.value(self.min) - max = self.value(self.max) - - if type(min) == float: - return random.uniform(min, max) - else: - return random.randint(min, max) + + """ PWhite: White noise between and . + If values are given as floats, output values are also floats < max. + If values are ints, output values are ints <= max (as random.randint) + + >>> PWhite(0, 10).nextn(16) + [8, 10, 8, 1, 7, 3, 1, 9, 9, 3, 2, 10, 7, 5, 10, 4] + + >>> PWhite(0.0, 10.0).nextn(16) + [3.6747936220022082, 0.61313530428271923, 9.1515368696591555, ... 6.2963694390145974 ] + """ + + def __init__(self, min, max=None): + self.min = min + self.max = max + + # also support a 1-argument case: PWhite(max) + if self.max is None: + self.max = min + self.min = -self.max + + def next(self): + min = self.value(self.min) + max = self.value(self.max) + + if isinstance(min, float): + return random.uniform(min, max) + else: + return random.randint(min, max) + class PBrown(Pattern): - """ PBrown: Brownian noise, beginning at , step +/-. - Set to False to prevent consecutive repeats. - """ - def __init__(self, value = 0, step = 0.1, min = -sys.maxint, max = sys.maxint, repeats = True, length = sys.maxint): - self.init = value - self.value = value - self.step = step - self.min = min - self.max = max - self.length = length - self.repeats = repeats - self.pos = 0 - - def reset(self): - self.value = self.init - self.pos = 0 - - Pattern.reset(self) - - def next(self): - # pull out modulatable values - vstep = Pattern.value(self.step) - vmin = Pattern.value(self.min) - vmax = Pattern.value(self.max) - vrepeats = Pattern.value(self.repeats) - - if self.pos >= self.length: - raise StopIteration - rv = self.value - self.pos += 1 - if type(vstep) == float: - self.value += random.uniform(-vstep, vstep) - else: - # select new offset without repeats - steps = range(-vstep, vstep + 1) - if not vrepeats: - steps.remove(0) - self.value += random.choice(steps) - self.value = min(max(self.value, vmin), vmax) - - return rv + + """ PBrown: Brownian noise, beginning at , step +/-. + Set to False to prevent consecutive repeats. + """ + + def __init__( + self, + value=0, + step=0.1, + min=-sys.maxsize, + max=sys.maxsize, + repeats=True, + length=sys.maxsize): + self.init = value + self.value = value + self.step = step + self.min = min + self.max = max + self.length = length + self.repeats = repeats + self.pos = 0 + + def reset(self): + self.value = self.init + self.pos = 0 + + Pattern.reset(self) + + def next(self): + # pull out modulatable values + vstep = Pattern.value(self.step) + vmin = Pattern.value(self.min) + vmax = Pattern.value(self.max) + vrepeats = Pattern.value(self.repeats) + + if self.pos >= self.length: + raise StopIteration + rv = self.value + self.pos += 1 + if isinstance(vstep, float): + self.value += random.uniform(-vstep, vstep) + else: + # select new offset without repeats + steps = range(-vstep, vstep + 1) + if not vrepeats: + steps.remove(0) + self.value += random.choice(steps) + self.value = min(max(self.value, vmin), vmax) + + return rv class PWalk(Pattern): - """ PWalk: Random walk around list. - Jumps between and steps inclusive. - >>> PWalk([ 0, 2, 5, 8, 11 ], min = 1, max = 2).nextn(16) - [8, 11, 0, 8, 0, 11, 2, 11, 2, 0, 5, 8, 11, 8, 5, 8] - """ - def __init__(self, values = [], min = 1, max = 1): - self.values = values - self.min = min - self.max = max - self.pos = 0 + """ PWalk: Random walk around list. + Jumps between and steps inclusive. + + >>> PWalk([ 0, 2, 5, 8, 11 ], min = 1, max = 2).nextn(16) + [8, 11, 0, 8, 0, 11, 2, 11, 2, 0, 5, 8, 11, 8, 5, 8] + """ + + def __init__(self, values=[], min=1, max=1): + self.values = values + self.min = min + self.max = max + self.pos = 0 - def next(self): - vvalues = Pattern.value(self.values) - vmin = Pattern.value(self.min) - vmax = Pattern.value(self.max) + def next(self): + vvalues = Pattern.value(self.values) + vmin = Pattern.value(self.min) + vmax = Pattern.value(self.max) - move = random.randint(vmin, vmax) - move = 0 - move if random.uniform(0, 1) < 0.5 else move - self.pos += move + move = random.randint(vmin, vmax) + move = 0 - move if random.uniform(0, 1) < 0.5 else move + self.pos += move - while self.pos < 0: - self.pos += len(vvalues) - while self.pos >= len(vvalues): - self.pos -= len(vvalues) + while self.pos < 0: + self.pos += len(vvalues) + while self.pos >= len(vvalues): + self.pos -= len(vvalues) - return vvalues[self.pos] + return vvalues[self.pos] class PChoice(Pattern): - """ PChoice: Random selection from - >>> p = PChoice([ 0, 1, 10, 11 ]) - >>> p.nextn(16) - [11, 1, 0, 10, 1, 11, 1, 0, 11, 1, 11, 1, 1, 11, 11, 1] - """ - def __init__(self, values = []): - self.values = values + """ PChoice: Random selection from + + >>> p = PChoice([ 0, 1, 10, 11 ]) + >>> p.nextn(16) + [11, 1, 0, 10, 1, 11, 1, 0, 11, 1, 11, 1, 1, 11, 11, 1] + """ + + def __init__(self, values=[]): + self.values = values + + def next(self): + return self.values[random.randint(0, len(self.values) - 1)] - def next(self): - return self.values[random.randint(0, len(self.values) - 1)] class PWChoice(Pattern): - """ PWChoice: Random selection from , weighted by . - and must be the same length, but not - necessarily normalised. - >>> p = PWChoice([ 1, 11, 111 ], [ 8, 2, 1 ]) - >>> p.nextn(16) - [111, 1, 1, 111, 1, 1, 1, 1, 1, 1, 1, 11, 1, 1, 1, 1] - """ - def __init__(self, values = [], weights = []): - self.values = values - self.weights = weights + """ PWChoice: Random selection from , weighted by . + and must be the same length, but not + necessarily normalised. + + >>> p = PWChoice([ 1, 11, 111 ], [ 8, 2, 1 ]) + >>> p.nextn(16) + [111, 1, 1, 111, 1, 1, 1, 1, 1, 1, 1, 11, 1, 1, 1, 1] + """ + + def __init__(self, values=[], weights=[]): + self.values = values + self.weights = weights + + def next(self): + return wnchoice(self.values, self.weights) - def next(self): - return wnchoice(self.values, self.weights) class PShuffle(Pattern): - """ PShuffle: Shuffled list. - - >>> p = PShuffle([ 1, 2, 3 ]) - >>> p.nextn(16) - [1, 3, 2, 3, 2, 1, 2, 3, 1, 2, 3, 1, 1, 2, 3, 1] - """ - def __init__(self, values = [], repeats = sys.maxint): - self.values = copy.copy(values) - self.repeats = repeats - - self.pos = 0 - self.rcount = 1 - random.shuffle(self.values) - - def reset(self): - self.pos = 0 - self.rcount = 0 - random.shuffle(self.values) - - Pattern.reset(self) - - def next(self): - values = self.value(self.values) - repeats = self.value(self.repeats) - - if self.pos >= len(values): - if self.rcount >= repeats: - raise StopIteration - random.shuffle(self.values) - self.rcount += 1 - self.pos = 0 - - rv = values[self.pos] - self.pos += 1 - return rv + + """ PShuffle: Shuffled list. + + >>> p = PShuffle([ 1, 2, 3 ]) + >>> p.nextn(16) + [1, 3, 2, 3, 2, 1, 2, 3, 1, 2, 3, 1, 1, 2, 3, 1] + """ + + def __init__(self, values=[], repeats=sys.maxsize): + self.values = copy.copy(values) + self.repeats = repeats + + self.pos = 0 + self.rcount = 1 + random.shuffle(self.values) + + def reset(self): + self.pos = 0 + self.rcount = 0 + random.shuffle(self.values) + + Pattern.reset(self) + + def next(self): + values = self.value(self.values) + repeats = self.value(self.repeats) + + if self.pos >= len(values): + if self.rcount >= repeats: + raise StopIteration + random.shuffle(self.values) + self.rcount += 1 + self.pos = 0 + + rv = values[self.pos] + self.pos += 1 + return rv + class PShuffleEvery(Pattern): - """ PShuffleEvery: Every steps, take values from and reorder. - - >>> p = PShuffleEvery(PSeries(0, 1), 4) - >>> p.nextn(16) - """ - def __init__(self, pattern, every = 4): - self.pattern = pattern - self.every = every - self.values = [] - self.pos = 0 - - def begin(self): - kevery = Pattern.value(self.every) - self.values = self.pattern.nextn(kevery) - random.shuffle(self.values) - self.pos = 0 - - def reset(self): - # TODO: clarify the semantics of "reset" (which should trickle down to children) - # vs the method called each time a new set of values is needed -- should - # this be a flag to reset? - self.begin() - Pattern.reset(self) - - def next(self): - if self.pos >= len(self.values): - self.begin() - - rv = self.values[self.pos] - self.pos += 1 - return rv + + """ PShuffleEvery: Every steps, take values from and reorder. + + >>> p = PShuffleEvery(PSeries(0, 1), 4) + >>> p.nextn(16) + """ + + def __init__(self, pattern, every=4): + self.pattern = pattern + self.every = every + self.values = [] + self.pos = 0 + + def begin(self): + kevery = Pattern.value(self.every) + self.values = self.pattern.nextn(kevery) + random.shuffle(self.values) + self.pos = 0 + + def reset(self): + # TODO: clarify the semantics of "reset" (which should trickle down to children) + # vs the method called each time a new set of values is needed -- should + # this be a flag to reset? + self.begin() + Pattern.reset(self) + + def next(self): + if self.pos >= len(self.values): + self.begin() + + rv = self.values[self.pos] + self.pos += 1 + return rv + class PSelfIndex(Pattern): - def __init__(self, count = 6): - self.pos = 0 - self.values = range(count) - random.shuffle(self.values) - print "init values: %s" % self.values - - def next(self): - if self.pos >= len(self.values): - # re-index values - self.pos = 0 - self.reindex() - - rv = self.values[self.pos] - self.pos += 1 - - return rv - - def reindex(self): - values_new = [] - for n in range(len(self.values)): - values_new.append(self.values[self.values[n]]) - print "new ordering: %s" % values_new - self.values = values_new + + def __init__(self, count=6): + self.pos = 0 + self.values = range(count) + random.shuffle(self.values) + print "init values: %s" % self.values + + def next(self): + if self.pos >= len(self.values): + # re-index values + self.pos = 0 + self.reindex() + + rv = self.values[self.pos] + self.pos += 1 + + return rv + + def reindex(self): + values_new = [] + for n in range(len(self.values)): + values_new.append(self.values[self.values[n]]) + print "new ordering: %s" % values_new + self.values = values_new + class PSkip(Pattern): - """ PSkip: Skip events with some probability, 1 - . - Set to False to skip events regularly. - """ - def __init__(self, pattern, play, random = True): - self.pattern = pattern - self.play = play - self.random = random - self.pos = 0.0 - - def next(self): - play = self.value(self.play) - if self.random: - if random.uniform(0, 1) < self.play: - return self.pattern.next() - else: - self.pos += play - if self.pos >= 1: - self.pos -= 1 - return self.pattern.next() - return None + + """ PSkip: Skip events with some probability, 1 - . + Set to False to skip events regularly. + """ + + def __init__(self, pattern, play, random=True): + self.pattern = pattern + self.play = play + self.random = random + self.pos = 0.0 + + def next(self): + play = self.value(self.play) + if self.random: + if random.uniform(0, 1) < self.play: + return self.pattern.next() + else: + self.pos += play + if self.pos >= 1: + self.pos -= 1 + return self.pattern.next() + return None + class PFlipFlop(Pattern): - """ PFlipFlop: flip a binary bit with some probability. - is initial value (0 or 1) - is chance of switching from 0->1 - is chance of switching from 1->0 - - >>> p = PFlipFlop(0, 0.9, 0.5) - >>> p.nextn(16) - [1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1] - """ - def __init__(self, initial = 0, p_on = 0.5, p_off = 0.5): - self.value = initial - self.p_on = p_on - self.p_off = p_off - - def next(self): - self.value = Pattern.value(self.value) - self.p_on = Pattern.value(self.p_on) - self.p_off = Pattern.value(self.p_off) - - if self.value == 0: - if random.uniform(0, 1) < self.p_on: - self.value = 1 - else: - if random.uniform(0, 1) < self.p_off: - self.value = 0 - - return self.value + + """ PFlipFlop: flip a binary bit with some probability. + is initial value (0 or 1) + is chance of switching from 0->1 + is chance of switching from 1->0 + + >>> p = PFlipFlop(0, 0.9, 0.5) + >>> p.nextn(16) + [1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1] + """ + + def __init__(self, initial=0, p_on=0.5, p_off=0.5): + self.value = initial + self.p_on = p_on + self.p_off = p_off + + def next(self): + self.value = Pattern.value(self.value) + self.p_on = Pattern.value(self.p_on) + self.p_off = Pattern.value(self.p_off) + + if self.value == 0: + if random.uniform(0, 1) < self.p_on: + self.value = 1 + else: + if random.uniform(0, 1) < self.p_off: + self.value = 0 + + return self.value + class PSwitchOne(Pattern): - """ PSwitchOne: Capture input values; repeat, switching two adjacent values times. - """ - def __init__(self, pattern, length = 4, switches = 1): - self.pattern = pattern - self.length = length - self.switches = switches - - self.reset() - - def reset(self): - self.values = [] - self.pos = 0 - - # recursively reset my pattern fields - Pattern.reset(self) - - def next(self): - length = self.value(self.length) - switches = self.value(self.switches) - - if len(self.values) < self.length: - value = self.pattern.next() - self.values.append(value) - self.pos += 1 - return value - - if self.pos >= len(self.values): - index = random.randint(0, len(self.values)) - 1 - indexP = (index + 1) % len(self.values) - self.values[index], self.values[indexP] = self.values[indexP], self.values[index] - self.pos = 0 - - rv = self.values[self.pos] - self.pos += 1 - return rv + """ PSwitchOne: Capture input values; repeat, switching two adjacent values times. + """ + + def __init__(self, pattern, length=4, switches=1): + self.pattern = pattern + self.length = length + self.switches = switches + + self.reset() + + def reset(self): + self.values = [] + self.pos = 0 + + # recursively reset my pattern fields + Pattern.reset(self) + + def next(self): + length = self.value(self.length) + switches = self.value(self.switches) + + if len(self.values) < self.length: + value = self.pattern.next() + self.values.append(value) + self.pos += 1 + return value + + if self.pos >= len(self.values): + index = random.randint(0, len(self.values)) - 1 + indexP = (index + 1) % len(self.values) + self.values[index], self.values[ + indexP] = self.values[indexP], self.values[index] + self.pos = 0 + + rv = self.values[self.pos] + self.pos += 1 + return rv diff --git a/isobar/pattern/core.py b/isobar/pattern/core.py index 18e4901..7f420ad 100644 --- a/isobar/pattern/core.py +++ b/isobar/pattern/core.py @@ -1,6 +1,6 @@ -#------------------------------------------------------------------------------- +#------------------------------------------------------------------------- # isobar: a python library for expressing and manipulating musical patterns. -#------------------------------------------------------------------------------- +#------------------------------------------------------------------------- import sys import copy @@ -12,410 +12,430 @@ class Pattern: - """ Pattern: Abstract superclass of all pattern generators. - - Patterns are at the core of isoar. A Pattern implements the iterator - protocol, representing a sequence of values which are iteratively - returned by the next() method. A pattern may be finite, after which - point it raises an EndOfPattern exception. Call reset() to return - a pattern to its initial state. - - Patterns can be subject to standard arithmetic operators as expected. - """ - - LENGTH_MAX = 65536 - GENO_SEPARATOR = "/" - - def __init__(self): - pass - - def __str__(self): - return "pattern(%s)" % self.__class__ - - def __len__(self): - # formerly defined as len(list(self)), but list(self) seeminly relies - # on a correct __len__ to function as expected. - items = self.all() - return len(items) - - def __neg__(self): - return 0 - self - - def __add__(self, operand): - """Binary op: add two patterns""" - # operand = copy.deepcopy(operand) if isinstance(operand, pattern) else PConst(operand) - # return PAdd(copy.deepcopy(self), operand) - - # we actually want to retain references to our constituent patterns - # in case the user later changes parameters of one - operand = Pattern.pattern(operand) - return PAdd(self, operand) - - def __radd__(self, operand): - """Binary op: add two patterns""" - return self.__add__(operand) - - def __sub__(self, operand): - """Binary op: subtract two patterns""" - operand = Pattern.pattern(operand) - return PSub(self, operand) - - def __rsub__(self, operand): - """Binary op: subtract two patterns""" - operand = Pattern.pattern(operand) - return PSub(operand, self) - - def __mul__(self, operand): - """Binary op: multiply two patterns""" - operand = Pattern.pattern(operand) - return PMul(self, operand) - - def __rmul__(self, operand): - """Binary op: multiply two patterns""" - return self.__mul__(operand) - - def __div__(self, operand): - """Binary op: divide two patterns""" - operand = Pattern.pattern(operand) - return PDiv(self, operand) - - def __rdiv__(self, operand): - """Binary op: divide two patterns""" - return self.__div__(operand) - - def __mod__(self, operand): - """Modulo""" - operand = Pattern.pattern(operand) - return PMod(self, operand) - - def __rmod__(self, operand): - """Modulo (as operand)""" - operand = Pattern.pattern(operand) - return operand.__mod__(self) - - def __rpow__(self, operand): - """Power (as operand)""" - operand = Pattern.pattern(operand) - return operand.__pow__(self) - - def __pow__(self, operand): - """Power""" - operand = Pattern.pattern(operand) - return PPow(self, operand) - - def __lshift__(self, operand): - """Left bitshift""" - operand = Pattern.pattern(operand) - return PLShift(self, operand) - - def __rshift__(self, operand): - """Right bitshift""" - operand = Pattern.pattern(operand) - return PRShift(self, operand) - - def __iter__(self): - return self - - def nextn(self, count): - rv = [] - # can't do a naive [ self.next() for n in range(count) ] - # as we want to catch StopIterations. - try: - for n in range(count): - rv.append(self.next()) - except StopIteration: - pass - - return rv - - def next(self): - # default pattern should be void - raise StopIteration - - def all(self): - values = [] - try: - # do we even need a LENGTH_MAX? - # if we omit it, .all() will become an alias for list(pattern) - # - maybe not such a bad thing. - for n in xrange(Pattern.LENGTH_MAX): - value = self.next() - values.append(value) - except StopIteration: - pass - - self.reset() - return values - - def reset(self): - """ reset a finite sequence back to position 0 """ - fields = vars(self) - for name, field in fields.items(): - # print "reset: %s" % name - if isinstance(field, Pattern): - field.reset() - # look through list items and reset anything in here too - # (needed to reset items in PConcat) - elif isinstance(field, list): - for item in field: - if isinstance(item, Pattern): - item.reset() - elif isinstance(field, dict): - for item in field.values(): - if isinstance(item, Pattern): - item.reset() - - def append(self, other): - return PConcat([ self, other ]) - - @property - def timeline(self): - """ returns the timeline that i am embedded in, if any """ - stack = inspect.stack() - for frame in stack: - frameobj = frame[0] - args, _, _, value_dict = inspect.getargvalues(frameobj) - if len(args) and args[0] == 'self': - instance = value_dict.get('self', None) - classname = instance.__class__.__name__ - if classname == "Timeline": - return instance - - @staticmethod - def fromgenotype(genotype): - """ create a new object based on this genotype """ - print "genotype: %s" % genotype - parts = genotype.split(Pattern.GENO_SEPARATOR) - classname = parts[0] - arguments = parts[1:] - try: - classes = vars(isobar) - classobj = classes[classname] - instance = classobj() - fields = vars(instance) - counter = 0 - for name, field in fields.items(): - instance.__dict__[name] = eval(arguments[counter]) - print "%s - %s" % (name, arguments[counter]) - counter += 1 - except Exception, e: - print "fail: %s" % e - pass - - return instance - - def breedWith(self, other): - """ XXX: we should probably have a Genotype class that deals with all this """ - - genotypeA = self.genotype() - genotypeB = other.genotype() - genesA = genotypeA.split("/")[1:] - genesB = genotypeB.split("/")[1:] - genotype = [ genotypeA.split("/")[0] ] - for n in range(len(genesA)): - if random.uniform(0, 1) < 0.5: - genotype.append(genesA[n]) - else: - genotype.append(genesB[n]) - genotypeC = Pattern.GENO_SEPARATOR.join(genotype) - print "A %s\nB %s\n> %s" % (genotypeA, genotypeB, genotypeC) - return Pattern.fromgenotype(genotypeC) - - def genotype(self): - """ return a string representation of this pattern, suitable for breeding """ - genotype = "%s" % (self.__class__.__name__) - fields = vars(self) - - import base64 - - for name, field in fields.items(): - genotype += Pattern.GENO_SEPARATOR - - if isinstance(field, Pattern): - genotype += "(%s)" % field.genotype() - elif isinstance(field, str): - genotype += base64.b64encode(field) - else: - genotype += str(field) - - return genotype - - def copy(self): - return copy.deepcopy(self) - - @staticmethod - def value(v): - """ Resolve a pattern to its value (that is, the next item in this - pattern, recursively). - """ - return Pattern.value(v.next()) if isinstance(v, Pattern) else v - - @staticmethod - def pattern(v): - """ Patternify a value by wrapping it in PConst if necessary. """ - return v if isinstance(v, Pattern) else PConst(v) + + """ Pattern: Abstract superclass of all pattern generators. + + Patterns are at the core of isoar. A Pattern implements the iterator + protocol, representing a sequence of values which are iteratively + returned by the next() method. A pattern may be finite, after which + point it raises an EndOfPattern exception. Call reset() to return + a pattern to its initial state. + + Patterns can be subject to standard arithmetic operators as expected. + """ + + LENGTH_MAX = 65536 + GENO_SEPARATOR = "/" + + def __init__(self): + pass + + def __str__(self): + return "pattern(%s)" % self.__class__ + + def __len__(self): + # formerly defined as len(list(self)), but list(self) seeminly relies + # on a correct __len__ to function as expected. + items = self.all() + return len(items) + + def __neg__(self): + return 0 - self + + def __add__(self, operand): + """Binary op: add two patterns""" + # operand = copy.deepcopy(operand) if isinstance(operand, pattern) else PConst(operand) + # return PAdd(copy.deepcopy(self), operand) + + # we actually want to retain references to our constituent patterns + # in case the user later changes parameters of one + operand = Pattern.pattern(operand) + return PAdd(self, operand) + + def __radd__(self, operand): + """Binary op: add two patterns""" + return self.__add__(operand) + + def __sub__(self, operand): + """Binary op: subtract two patterns""" + operand = Pattern.pattern(operand) + return PSub(self, operand) + + def __rsub__(self, operand): + """Binary op: subtract two patterns""" + operand = Pattern.pattern(operand) + return PSub(operand, self) + + def __mul__(self, operand): + """Binary op: multiply two patterns""" + operand = Pattern.pattern(operand) + return PMul(self, operand) + + def __rmul__(self, operand): + """Binary op: multiply two patterns""" + return self.__mul__(operand) + + def __div__(self, operand): + """Binary op: divide two patterns""" + operand = Pattern.pattern(operand) + return PDiv(self, operand) + + def __rdiv__(self, operand): + """Binary op: divide two patterns""" + return self.__div__(operand) + + def __mod__(self, operand): + """Modulo""" + operand = Pattern.pattern(operand) + return PMod(self, operand) + + def __rmod__(self, operand): + """Modulo (as operand)""" + operand = Pattern.pattern(operand) + return operand.__mod__(self) + + def __rpow__(self, operand): + """Power (as operand)""" + operand = Pattern.pattern(operand) + return operand.__pow__(self) + + def __pow__(self, operand): + """Power""" + operand = Pattern.pattern(operand) + return PPow(self, operand) + + def __lshift__(self, operand): + """Left bitshift""" + operand = Pattern.pattern(operand) + return PLShift(self, operand) + + def __rshift__(self, operand): + """Right bitshift""" + operand = Pattern.pattern(operand) + return PRShift(self, operand) + + def __iter__(self): + return self + + def nextn(self, count): + rv = [] + # can't do a naive [ self.next() for n in range(count) ] + # as we want to catch StopIterations. + try: + for n in range(count): + rv.append(self.next()) + except StopIteration: + pass + + return rv + + def next(self): + # default pattern should be void + raise StopIteration + + def all(self): + values = [] + try: + # do we even need a LENGTH_MAX? + # if we omit it, .all() will become an alias for list(pattern) + # - maybe not such a bad thing. + for n in xrange(Pattern.LENGTH_MAX): + value = self.next() + values.append(value) + except StopIteration: + pass + + self.reset() + return values + + def reset(self): + """ reset a finite sequence back to position 0 """ + fields = vars(self) + for name, field in fields.items(): + # print "reset: %s" % name + if isinstance(field, Pattern): + field.reset() + # look through list items and reset anything in here too + # (needed to reset items in PConcat) + elif isinstance(field, list): + for item in field: + if isinstance(item, Pattern): + item.reset() + elif isinstance(field, dict): + for item in field.values(): + if isinstance(item, Pattern): + item.reset() + + def append(self, other): + return PConcat([self, other]) + + @property + def timeline(self): + """ returns the timeline that i am embedded in, if any """ + stack = inspect.stack() + for frame in stack: + frameobj = frame[0] + args, _, _, value_dict = inspect.getargvalues(frameobj) + if len(args) and args[0] == 'self': + instance = value_dict.get('self', None) + classname = instance.__class__.__name__ + if classname == "Timeline": + return instance + + @staticmethod + def fromgenotype(genotype): + """ create a new object based on this genotype """ + print "genotype: %s" % genotype + parts = genotype.split(Pattern.GENO_SEPARATOR) + classname = parts[0] + arguments = parts[1:] + try: + classes = vars(isobar) + classobj = classes[classname] + instance = classobj() + fields = vars(instance) + counter = 0 + for name, field in fields.items(): + instance.__dict__[name] = eval(arguments[counter]) + print "%s - %s" % (name, arguments[counter]) + counter += 1 + except Exception as e: + print "fail: %s" % e + pass + + return instance + + def breedWith(self, other): + """ XXX: we should probably have a Genotype class that deals with all this """ + + genotypeA = self.genotype() + genotypeB = other.genotype() + genesA = genotypeA.split("/")[1:] + genesB = genotypeB.split("/")[1:] + genotype = [genotypeA.split("/")[0]] + for n in range(len(genesA)): + if random.uniform(0, 1) < 0.5: + genotype.append(genesA[n]) + else: + genotype.append(genesB[n]) + genotypeC = Pattern.GENO_SEPARATOR.join(genotype) + print "A %s\nB %s\n> %s" % (genotypeA, genotypeB, genotypeC) + return Pattern.fromgenotype(genotypeC) + + def genotype(self): + """ return a string representation of this pattern, suitable for breeding """ + genotype = "%s" % (self.__class__.__name__) + fields = vars(self) + + import base64 + + for name, field in fields.items(): + genotype += Pattern.GENO_SEPARATOR + + if isinstance(field, Pattern): + genotype += "(%s)" % field.genotype() + elif isinstance(field, str): + genotype += base64.b64encode(field) + else: + genotype += str(field) + + return genotype + + def copy(self): + return copy.deepcopy(self) + + @staticmethod + def value(v): + """ Resolve a pattern to its value (that is, the next item in this + pattern, recursively). + """ + return Pattern.value(v.next()) if isinstance(v, Pattern) else v + + @staticmethod + def pattern(v): + """ Patternify a value by wrapping it in PConst if necessary. """ + return v if isinstance(v, Pattern) else PConst(v) + class PConst(Pattern): - """ PConst: Pattern returning a fixed value - >>> p = PConst(4) - >>> p.nextn(16) - [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] - """ - def __init__(self, constant): - self.constant = constant + """ PConst: Pattern returning a fixed value + + >>> p = PConst(4) + >>> p.nextn(16) + [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + """ + + def __init__(self, constant): + self.constant = constant - def __str__(self): - return "constant" + def __str__(self): + return "constant" + + def next(self): + return self.constant - def next(self): - return self.constant class PRef(Pattern): - """ PRef: Pattern containing a reference to another pattern - Returns the next value of the pattern contained. - Useful to change an inner pattern in real time. - """ - def __init__(self, pattern): - self.pattern = pattern - def change(self, pattern): - self.pattern = pattern + """ PRef: Pattern containing a reference to another pattern + Returns the next value of the pattern contained. + Useful to change an inner pattern in real time. + """ + + def __init__(self, pattern): + self.pattern = pattern + + def change(self, pattern): + self.pattern = pattern + + def next(self): + return self.pattern.next() - def next(self): - return self.pattern.next() class PFunc(Pattern): - def __init__(self, fn): - self.fn = fn - def next(self): - fn = Pattern.value(self.fn) - return fn() + def __init__(self, fn): + self.fn = fn + + def next(self): + fn = Pattern.value(self.fn) + return fn() + class PDict(Pattern): - """ PDict: Dict of patterns - Thanks to Dan Stowell - """ - def __init__(self, value = {}): - from isobar.pattern.sequence import PSeq - self.dict = {} + """ PDict: Dict of patterns + Thanks to Dan Stowell + """ + + def __init__(self, value={}): + from isobar.pattern.sequence import PSeq + + self.dict = {} + + if isinstance(value, dict): + self.dict = value + elif isinstance(value, list): + self.dict = {} + try: + keys = value[0].keys() + for key in keys: + self.dict[key] = PSeq([item[key] for item in value], 1) + except IndexError: + pass - if type(value) == dict: - self.dict = value - elif type(value) == list: - self.dict = {} - try: - keys = value[0].keys() - for key in keys: - self.dict[key] = PSeq([ item[key] for item in value ], 1) - except IndexError: - pass + def __getitem__(self, key): + return self.dict[key] - def __getitem__(self, key): - return self.dict[key] + def __setitem__(self, key, value): + self.dict[key] = value - def __setitem__(self, key, value): - self.dict[key] = value + def __contains__(self, key): + return key in self.dict - def __contains__(self, key): - return key in self.dict + @classmethod + def load(self, filename): + from isobar.io.midifile import MidiFileIn + from isobar.pattern.sequence import PSeq - @classmethod - def load(self, filename): - from isobar.io.midifile import MidiFileIn - from isobar.pattern.sequence import PSeq + reader = MidiFileIn() + d = reader.read(filename) + d = dict([(key, PSeq(value, 1)) for key, value in d.items()]) + return PDict(d) - reader = MidiFileIn() - d = reader.read(filename) - d = dict([ (key, PSeq(value, 1)) for key, value in d.items() ]) - return PDict(d) + def has_key(self, key): + return key in self.dict - def has_key(self, key): - return key in self.dict + def setdefault(self, key, value): + if not key in self.dict: + self.dict[key] = value - def setdefault(self, key, value): - if not key in self.dict: - self.dict[key] = value + def keys(self): + return self.dict.keys() - def keys(self): - return self.dict.keys() + def values(self): + return self.dict.values() - def values(self): - return self.dict.values() + def items(self): + return self.dict.items() - def items(self): - return self.dict.items() + def next(self): + vdict = Pattern.value(self.dict) + if not vdict: + raise StopIteration - def next(self): - vdict = Pattern.value(self.dict) - if not vdict: - raise StopIteration + # for some reason, doing a list comprehension without the surrounding square + # brackets causes an inner StopIteration to be suppressed -- we want to + # explicitly raise it. + rv = dict([(k, Pattern.value(vdict[k])) for k in vdict]) - # for some reason, doing a list comprehension without the surrounding square - # brackets causes an inner StopIteration to be suppressed -- we want to - # explicitly raise it. - rv = dict([ (k, Pattern.value(vdict[k])) for k in vdict ]) + return rv - return rv class PIndex(Pattern): - """ PIndex: Request a specified index from an array. - """ - def __init__(self, index, list): - self.index = index - self.list = list - - def next(self): - index = Pattern.value(self.index) - list = Pattern.value(self.list) - - #------------------------------------------------------------------ - # null indices denote a rest -- so return a null value. - # (same behaviour as PDegree: a degree of None returns a rest.) - #------------------------------------------------------------------ - if index is None: - return None - else: - index = int(index) - return list[index] + + """ PIndex: Request a specified index from an array. + """ + + def __init__(self, index, list): + self.index = index + self.list = list + + def next(self): + index = Pattern.value(self.index) + list = Pattern.value(self.list) + + #------------------------------------------------------------------ + # null indices denote a rest -- so return a null value. + # (same behaviour as PDegree: a degree of None returns a rest.) + #------------------------------------------------------------------ + if index is None: + return None + else: + index = int(index) + return list[index] + class PKey(Pattern): - """ PKey: Request a specified key from a dictionary. - """ - def __init__(self, key, dict): - self.key = key - self.dict = dict - def next(self): - vkey = Pattern.value(self.key) - vdict = Pattern.value(self.dict) - return vdict[vkey] + """ PKey: Request a specified key from a dictionary. + """ + + def __init__(self, key, dict): + self.key = key + self.dict = dict + + def next(self): + vkey = Pattern.value(self.key) + vdict = Pattern.value(self.dict) + return vdict[vkey] + class PConcat(Pattern): - """ PConcat: Concatenate the output of multiple sequences. - - >>> PConcat([ PSeq([ 1, 2, 3 ], 2), PSeq([ 9, 8, 7 ], 2) ]).nextn(16) - [1, 2, 3, 1, 2, 3, 9, 8, 7, 9, 8, 7] - """ - - def __init__(self, inputs): - self.inputs = inputs - self.current = self.inputs.pop(0) - - def next(self): - try: - return self.current.next() - except StopIteration: - if len(self.inputs) > 0: - self.current = self.inputs.pop(0) - # can't just blindly return the first value of current - # -- what if it is empty? - return self.next() - else: - # no more sequences left, so just return. - raise StopIteration + + """ PConcat: Concatenate the output of multiple sequences. + + >>> PConcat([ PSeq([ 1, 2, 3 ], 2), PSeq([ 9, 8, 7 ], 2) ]).nextn(16) + [1, 2, 3, 1, 2, 3, 9, 8, 7, 9, 8, 7] + """ + + def __init__(self, inputs): + self.inputs = inputs + self.current = self.inputs.pop(0) + + def next(self): + try: + return self.current.next() + except StopIteration: + if len(self.inputs) > 0: + self.current = self.inputs.pop(0) + # can't just blindly return the first value of current + # -- what if it is empty? + return self.next() + else: + # no more sequences left, so just return. + raise StopIteration #------------------------------------------------------------------ @@ -423,87 +443,111 @@ def next(self): #------------------------------------------------------------------ class PBinOp(Pattern): - def __init__(self, a, b): - self.a = a - self.b = b + + def __init__(self, a, b): + self.a = a + self.b = b + class PAdd(PBinOp): - """ PAdd: Add elements of two patterns (shorthand: patternA + patternB) """ - def __str__(self): - return "%s + %s" % (self.a, self.b) - def next(self): - a = self.a.next() - b = self.b.next() - return None if a is None or b is None else a + b - + """ PAdd: Add elements of two patterns (shorthand: patternA + patternB) """ + + def __str__(self): + return "%s + %s" % (self.a, self.b) + + def next(self): + a = self.a.next() + b = self.b.next() + return None if a is None or b is None else a + b + class PSub(PBinOp): - """ PSub: Subtract elements of two patterns (shorthand: patternA - patternB) """ - def __str__(self): - return "%s - %s" % (self.a, self.b) - def next(self): - a = self.a.next() - b = self.b.next() - return None if a is None or b is None else a - b + """ PSub: Subtract elements of two patterns (shorthand: patternA - patternB) """ + + def __str__(self): + return "%s - %s" % (self.a, self.b) + + def next(self): + a = self.a.next() + b = self.b.next() + return None if a is None or b is None else a - b + class PMul(PBinOp): - """ PMul: Multiply elements of two patterns (shorthand: patternA * patternB) """ - def __str__(self): - return "(%s) * (%s)" % (self.a, self.b) - def next(self): - a = self.a.next() - b = self.b.next() - return None if a is None or b is None else a * b + """ PMul: Multiply elements of two patterns (shorthand: patternA * patternB) """ + + def __str__(self): + return "(%s) * (%s)" % (self.a, self.b) + + def next(self): + a = self.a.next() + b = self.b.next() + return None if a is None or b is None else a * b + class PDiv(PBinOp): - """ PDiv: Divide elements of two patterns (shorthand: patternA / patternB) """ - def __str__(self): - return "(%s) / (%s)" % (self.a, self.b) - def next(self): - a = self.a.next() - b = self.b.next() - return None if a is None or b is None else a / b + """ PDiv: Divide elements of two patterns (shorthand: patternA / patternB) """ + + def __str__(self): + return "(%s) / (%s)" % (self.a, self.b) + + def next(self): + a = self.a.next() + b = self.b.next() + return None if a is None or b is None else a / b + class PMod(PBinOp): - """ PMod: Modulo elements of two patterns (shorthand: patternA % patternB) """ - def __str__(self): - return "(%s) %% (%s)" % (self.a, self.b) - def next(self): - a = self.a.next() - b = self.b.next() - return None if a is None or b is None else a % b + """ PMod: Modulo elements of two patterns (shorthand: patternA % patternB) """ + + def __str__(self): + return "(%s) %% (%s)" % (self.a, self.b) + + def next(self): + a = self.a.next() + b = self.b.next() + return None if a is None or b is None else a % b + class PPow(PBinOp): - """ PPow: One pattern to the power of another (shorthand: patternA ** patternB) """ - def __str__(self): - return "pow(%s, %s)" % (self.a, self.b) - def next(self): - a = self.a.next() - b = self.b.next() - return None if a is None or b is None else pow(a, b) + """ PPow: One pattern to the power of another (shorthand: patternA ** patternB) """ + + def __str__(self): + return "pow(%s, %s)" % (self.a, self.b) + + def next(self): + a = self.a.next() + b = self.b.next() + return None if a is None or b is None else pow(a, b) + class PLShift(PBinOp): - """ PLShift: Binary left-shift (shorthand: patternA << patternB) """ - def __str__(self): - return "(%s << %s)" % (self.a, self.b) - def next(self): - a = self.a.next() - b = self.b.next() - return None if a is None or b is None else a << b + """ PLShift: Binary left-shift (shorthand: patternA << patternB) """ + + def __str__(self): + return "(%s << %s)" % (self.a, self.b) + + def next(self): + a = self.a.next() + b = self.b.next() + return None if a is None or b is None else a << b + class PRShift(PBinOp): - """ PRShift: Binary right-shift (shorthand: patternA << patternB) """ - def __str__(self): - return "(%s >> %s)" % (self.a, self.b) - - def next(self): - a = self.a.next() - b = self.b.next() - return None if a is None or b is None else a >> b + + """ PRShift: Binary right-shift (shorthand: patternA << patternB) """ + + def __str__(self): + return "(%s >> %s)" % (self.a, self.b) + + def next(self): + a = self.a.next() + b = self.b.next() + return None if a is None or b is None else a >> b diff --git a/isobar/pattern/fade.py b/isobar/pattern/fade.py index e0bf518..a5c661b 100644 --- a/isobar/pattern/fade.py +++ b/isobar/pattern/fade.py @@ -9,127 +9,141 @@ from isobar.pattern.core import * + class PFade(Pattern): - IN = 1 - PEAK = 0 - OUT = -1 + IN = 1 + PEAK = 0 + OUT = -1 + + def __init__(self): + self.direction = 1 - def __init__(self): - self.direction = 1 + self.fadestep = 0 + self.counter = 0 + self.rcounter = 0 + self.pattern = None - self.fadestep = 0 - self.counter = 0 - self.rcounter = 0 - self.pattern = None + def __str__(self): + classname = str(p.__class__).split(".")[-1] + return "%s(%s)" % (classname, str(self.pattern)) - def __str__(self): - classname = str(p.__class__).split(".")[-1] - return "%s(%s)" % (classname, str(self.pattern)) class PFadeNotewise(PFade): - """ PFadeNotewise: Fade a pattern in/out by introducing notes at a gradual rate. """ - - def __init__(self, pattern, rate_min = 1, rate_max = 1, repeats = 1, repeats_postfade = 1): - PFade.__init__(self) - - self.notes = pattern.all() - self.on = [ False ] * len(self.notes) - self.rate_min = rate_min - self.rate_max = rate_max - self.repeats = repeats - self.repeats_postfade = repeats_postfade - - self.fadeindex = 0 - - def fade_in(self): - fade_count = random.randint(self.rate_min, self.rate_max) - for n in range(fade_count): - if self.fadeindex >= len(self.on): - return - self.on[self.fadeindex] = True - self.fadeindex += 1 - self.fadestep += 1 - - def fade_out(self): - fade_count = random.randint(self.rate_min, self.rate_max) - for n in range(fade_count): - if self.fadeindex >= len(self.on): - return - self.on[self.fadeindex] = False - self.fadeindex += 1 - self.fadestep -= 1 - - def next(self): - if self.counter >= len(self.notes): - #---------------------------------------------------------------------- - # we've reached the end of the sequence. - #---------------------------------------------------------------------- - self.rcounter += 1 - self.counter = 0 - if self.direction == PFade.IN and self.rcounter == self.repeats: - #---------------------------------------------------------------------- - # we've finished fading in -- now play the complete sequence. - #---------------------------------------------------------------------- - self.rcounter = 0 - self.fade_in() - if self.fadestep == len(self.notes): - self.direction = PFade.PEAK - elif self.direction == PFade.OUT and self.rcounter == self.repeats: - #---------------------------------------------------------------------- - # finish fading out. - #---------------------------------------------------------------------- - self.rcounter = 0 - self.fade_out() - if self.fadestep == 0: - raise StopIteration - elif self.direction == PFade.PEAK and self.rcounter == self.repeats_postfade: - #---------------------------------------------------------------------- - # start fading out. - #---------------------------------------------------------------------- - self.rcounter = 0 - self.direction = PFade.OUT - self.fadeindex = 0 - self.fade_out() - - if type(self.notes[self.counter]) == dict: - if self.on[self.counter]: - rv = self.notes[self.counter] - else: - note = self.notes[self.counter].copy() - note["note"] = None - rv = note - else: - rv = self.notes[self.counter] if self.on[self.counter] else None - - self.counter += 1 - - return rv + + """ PFadeNotewise: Fade a pattern in/out by introducing notes at a gradual rate. """ + + def __init__( + self, + pattern, + rate_min=1, + rate_max=1, + repeats=1, + repeats_postfade=1): + PFade.__init__(self) + + self.notes = pattern.all() + self.on = [False] * len(self.notes) + self.rate_min = rate_min + self.rate_max = rate_max + self.repeats = repeats + self.repeats_postfade = repeats_postfade + + self.fadeindex = 0 + + def fade_in(self): + fade_count = random.randint(self.rate_min, self.rate_max) + for n in range(fade_count): + if self.fadeindex >= len(self.on): + return + self.on[self.fadeindex] = True + self.fadeindex += 1 + self.fadestep += 1 + + def fade_out(self): + fade_count = random.randint(self.rate_min, self.rate_max) + for n in range(fade_count): + if self.fadeindex >= len(self.on): + return + self.on[self.fadeindex] = False + self.fadeindex += 1 + self.fadestep -= 1 + + def next(self): + if self.counter >= len(self.notes): + #------------------------------------------------------------------ + # we've reached the end of the sequence. + #------------------------------------------------------------------ + self.rcounter += 1 + self.counter = 0 + if self.direction == PFade.IN and self.rcounter == self.repeats: + #-------------------------------------------------------------- + # we've finished fading in -- now play the complete sequence. + #-------------------------------------------------------------- + self.rcounter = 0 + self.fade_in() + if self.fadestep == len(self.notes): + self.direction = PFade.PEAK + elif self.direction == PFade.OUT and self.rcounter == self.repeats: + #-------------------------------------------------------------- + # finish fading out. + #-------------------------------------------------------------- + self.rcounter = 0 + self.fade_out() + if self.fadestep == 0: + raise StopIteration + elif self.direction == PFade.PEAK and self.rcounter == self.repeats_postfade: + #-------------------------------------------------------------- + # start fading out. + #-------------------------------------------------------------- + self.rcounter = 0 + self.direction = PFade.OUT + self.fadeindex = 0 + self.fade_out() + + if isinstance(self.notes[self.counter], dict): + if self.on[self.counter]: + rv = self.notes[self.counter] + else: + note = self.notes[self.counter].copy() + note["note"] = None + rv = note + else: + rv = self.notes[self.counter] if self.on[self.counter] else None + + self.counter += 1 + + return rv + class PFadeNotewiseRandom(PFadeNotewise): - """ PFadeNotewise: Fade a pattern in/out by gradually introducing random notes. """ - def __init__(self, *args, **kwargs): - PFadeNotewise.__init__(self, *args, **kwargs) - self.ordering = range(len(self.notes)) - random.shuffle(self.ordering) - - def fade_in(self): - fade_count = random.randint(self.rate_min, self.rate_max) - if fade_count < 1: fade_count = 1 - for n in range(fade_count): - if self.fadeindex >= len(self.on): - return - index = self.ordering[self.fadeindex] - self.on[index] = True - self.fadeindex += 1 - self.fadestep += 1 - - def fade_out(self): - fade_count = random.randint(self.rate_min, self.rate_max) - if fade_count < 1: fade_count = 1 - for n in range(fade_count): - if self.fadeindex >= len(self.on): - return - index = self.ordering[self.fadeindex] - self.on[index] = False - self.fadeindex += 1 - self.fadestep -= 1 + + """ PFadeNotewise: Fade a pattern in/out by gradually introducing random notes. """ + + def __init__(self, *args, **kwargs): + PFadeNotewise.__init__(self, *args, **kwargs) + self.ordering = range(len(self.notes)) + random.shuffle(self.ordering) + + def fade_in(self): + fade_count = random.randint(self.rate_min, self.rate_max) + if fade_count < 1: + fade_count = 1 + for n in range(fade_count): + if self.fadeindex >= len(self.on): + return + index = self.ordering[self.fadeindex] + self.on[index] = True + self.fadeindex += 1 + self.fadestep += 1 + + def fade_out(self): + fade_count = random.randint(self.rate_min, self.rate_max) + if fade_count < 1: + fade_count = 1 + for n in range(fade_count): + if self.fadeindex >= len(self.on): + return + index = self.ordering[self.fadeindex] + self.on[index] = False + self.fadeindex += 1 + self.fadestep -= 1 diff --git a/isobar/pattern/grapher.py b/isobar/pattern/grapher.py index efef24a..82f5e75 100644 --- a/isobar/pattern/grapher.py +++ b/isobar/pattern/grapher.py @@ -2,7 +2,9 @@ from isobar.util import * import sys + class Grapher: + def graph(self, markov): gv = GvGen() items = [] @@ -17,7 +19,7 @@ def graph(self, markov): # first pass: add nodes for n, node in enumerate(markov.nodes): - if type(node) == int: + if isinstance(node, int): node = miditopitch(node) item = gv.newItem(str(node)) items.append(item) @@ -31,5 +33,4 @@ def graph(self, markov): gv.styleApply("edge", link) # gv.propertyAppend(link, "weight", edges[e]) - gv.dot(sys.stderr) diff --git a/isobar/pattern/harmony.py b/isobar/pattern/harmony.py index c985f4e..ae186c0 100644 --- a/isobar/pattern/harmony.py +++ b/isobar/pattern/harmony.py @@ -1,24 +1,28 @@ from isobar.pattern.core import * + class PFilterByKey(Pattern): - def __init__(self, input, key): - self.input = input - self.key = key - - def next(self): - note = self.input.next() - key = Pattern.value(self.key) - if note in key: - return note - else: - return None + + def __init__(self, input, key): + self.input = input + self.key = key + + def next(self): + note = self.input.next() + key = Pattern.value(self.key) + if note in key: + return note + else: + return None + class PNearest(Pattern): - def __init__(self, input, key): - self.input = input - self.key = key - - def next(self): - note = self.input.next() - key = Pattern.value(self.key) - return key.nearest_note(note) + + def __init__(self, input, key): + self.input = input + self.key = key + + def next(self): + note = self.input.next() + key = Pattern.value(self.key) + return key.nearest_note(note) diff --git a/isobar/pattern/lsystem.py b/isobar/pattern/lsystem.py index a0dce06..0946a2a 100644 --- a/isobar/pattern/lsystem.py +++ b/isobar/pattern/lsystem.py @@ -3,75 +3,80 @@ from isobar.note import * from isobar.pattern import * + class LSystem: - def __init__(self, rule = "N[-N++N]-N", seed = "N"): - self.rule = rule - self.seed = seed - self.string = seed - - self.reset() - - def iterate(self, count = 3): - if self.rule.count("[") != self.rule.count("]"): - raise ValueError, "Imbalanced brackets in rule string: %s" % self.rule - - for n in range(count): - string_new = "" - for char in self.string: - string_new = string_new + self.rule if char == "N" else string_new + char - - self.string = string_new - # print "(iter %d) string now %s" % (n, self.string) - - def next(self): - while self.pos < len(self.string): - token = self.string[self.pos] - self.pos = self.pos + 1 - - if token == 'N': - return self.state - elif token == '_': - return None - elif token == '-': - self.state -= 1 - elif token == '+': - self.state += 1 - elif token == '?': - self.state += random.choice([ -1, 1 ]) - elif token == '[': - self.stack.append(self.state) - elif token == ']': - self.state = self.stack.pop() - - raise StopIteration - - def reset(self): - self.pos = 0 - self.stack = [] - self.state = 0 + + def __init__(self, rule="N[-N++N]-N", seed="N"): + self.rule = rule + self.seed = seed + self.string = seed + + self.reset() + + def iterate(self, count=3): + if self.rule.count("[") != self.rule.count("]"): + raise ValueError( + "Imbalanced brackets in rule string: %s" % + self.rule) + + for n in range(count): + string_new = "" + for char in self.string: + string_new = string_new + \ + self.rule if char == "N" else string_new + char + + self.string = string_new + # print "(iter %d) string now %s" % (n, self.string) + + def next(self): + while self.pos < len(self.string): + token = self.string[self.pos] + self.pos = self.pos + 1 + + if token == 'N': + return self.state + elif token == '_': + return None + elif token == '-': + self.state -= 1 + elif token == '+': + self.state += 1 + elif token == '?': + self.state += random.choice([-1, 1]) + elif token == '[': + self.stack.append(self.state) + elif token == ']': + self.state = self.stack.pop() + + raise StopIteration + + def reset(self): + self.pos = 0 + self.stack = [] + self.state = 0 class PLSys(Pattern): - """ PLSys: integer sequence derived from Lindenmayer systems """ - def __init__(self, rule, depth = 3, loop = True): - self.rule = rule - self.depth = depth - self.loop = loop - self.reset() + """ PLSys: integer sequence derived from Lindenmayer systems """ - def __str__(self): - return "lsystem (%s)" % rule + def __init__(self, rule, depth=3, loop=True): + self.rule = rule + self.depth = depth + self.loop = loop + self.reset() - def reset(self): - self.lsys = LSystem(self.rule, "N") - self.lsys.iterate(self.depth) + def __str__(self): + return "lsystem (%s)" % rule - def next(self): - n = self.lsys.next() - if self.loop and n is None: - self.lsys.reset() - n = self.lsys.next() + def reset(self): + self.lsys = LSystem(self.rule, "N") + self.lsys.iterate(self.depth) - return None if n is None else n + def next(self): + n = self.lsys.next() + if self.loop and n is None: + self.lsys.reset() + n = self.lsys.next() + return None if n is None else n diff --git a/isobar/pattern/markov.py b/isobar/pattern/markov.py index 86b6850..91f96e8 100644 --- a/isobar/pattern/markov.py +++ b/isobar/pattern/markov.py @@ -1,107 +1,113 @@ from isobar.pattern import * from isobar.util import * + class PMarkov(Pattern): - """ PMarkov: First-order Markov chain. - http://pastebin.com/rNu2CSFs - TODO: Implement n-order. """ - - def __init__(self, nodes = None): - #------------------------------------------------------------------------ - # avoid using [] (mutable default arguments considered harmful) - # http://stackoverflow.com/questions/1132941/least-astonishment-in-python-the-mutable-default-argument - #------------------------------------------------------------------------ - if isinstance(nodes, list): - #------------------------------------------------------------------------ - # Learn a sequence of values by inferring probabilities - #------------------------------------------------------------------------ - learner = MarkovLearner() - for value in nodes: - learner.register(value) - self.nodes = learner.markov.nodes - elif isinstance(nodes, dict): - #------------------------------------------------------------------------ - # Take a dictionary argument with the same format as our internal nodes - # model : eg { 1 : [ 2, 3 ], 2 : [ 3 ], 3 : [ 1, 2 ] } - #------------------------------------------------------------------------ - self.nodes = nodes - else: - self.nodes = {} - - self.node = None - - def randomize(self): - """ Uses the existing set of nodes but randomizes their connections. """ - for node in self.nodes.keys(): - self.nodes[node] = [] - for other in self.nodes.keys(): - prob = random.randint(0, 10) - self.nodes[node] += [ other ] * prob - - def next(self): - #------------------------------------------------------------------------ - # Returns the next value according to our internal statistical model. - #------------------------------------------------------------------------ - if self.node is None and len(self.nodes) > 0: - self.node = random.choice(self.nodes.keys()) - else: - try: - #------------------------------------------------------------------------ - # - #------------------------------------------------------------------------ - self.node = random.choice(self.nodes[self.node]) - except IndexError: - self.node = random.choice(self.nodes.keys()) - except KeyError: - print "no such node: %s" % self.node - - if self.node is None: - print "PMarkov: got no next node :-(" - - return self.node - - @classmethod - def fromsequence(self, sequence): - learner = MarkovLearner() - for value in sequence: - learner.register(value) - return PMarkov(learner.markov) - - @classmethod - def fromscale(self, scale): - # TODO: BROKEN - semitones = scale.semitones - weights = scale.weights - return PMarkov(semitones, [ weights[:] for _ in semitones ]) + + """ PMarkov: First-order Markov chain. + http://pastebin.com/rNu2CSFs + TODO: Implement n-order. """ + + def __init__(self, nodes=None): + #---------------------------------------------------------------------- + # avoid using [] (mutable default arguments considered harmful) + # http://stackoverflow.com/questions/1132941/least-astonishment-in-python-the-mutable-default-argument + #---------------------------------------------------------------------- + if isinstance(nodes, list): + #------------------------------------------------------------------ + # Learn a sequence of values by inferring probabilities + #------------------------------------------------------------------ + learner = MarkovLearner() + for value in nodes: + learner.register(value) + self.nodes = learner.markov.nodes + elif isinstance(nodes, dict): + #------------------------------------------------------------------ + # Take a dictionary argument with the same format as our internal nodes + # model : eg { 1 : [ 2, 3 ], 2 : [ 3 ], 3 : [ 1, 2 ] } + #------------------------------------------------------------------ + self.nodes = nodes + else: + self.nodes = {} + + self.node = None + + def randomize(self): + """ Uses the existing set of nodes but randomizes their connections. """ + for node in self.nodes.keys(): + self.nodes[node] = [] + for other in self.nodes.keys(): + prob = random.randint(0, 10) + self.nodes[node] += [other] * prob + + def next(self): + #---------------------------------------------------------------------- + # Returns the next value according to our internal statistical model. + #---------------------------------------------------------------------- + if self.node is None and len(self.nodes) > 0: + self.node = random.choice(self.nodes.keys()) + else: + try: + #-------------------------------------------------------------- + # + #-------------------------------------------------------------- + self.node = random.choice(self.nodes[self.node]) + except IndexError: + self.node = random.choice(self.nodes.keys()) + except KeyError: + print "no such node: %s" % self.node + + if self.node is None: + print "PMarkov: got no next node :-(" + + return self.node + + @classmethod + def fromsequence(self, sequence): + learner = MarkovLearner() + for value in sequence: + learner.register(value) + return PMarkov(learner.markov) + + @classmethod + def fromscale(self, scale): + # TODO: BROKEN + semitones = scale.semitones + weights = scale.weights + return PMarkov(semitones, [weights[:] for _ in semitones]) + class MarkovLearner: - def __init__(self): - self.markov = PMarkov() - self.last = None - - def register(self, value): - # print "registering %s in %s (%s)" % (value, self.markov, self.markov.nodes) - if value not in self.markov.nodes: - self.markov.nodes[value] = [] - if self.last is not None: - self.markov.nodes[self.last].append(value) - self.last = value + + def __init__(self): + self.markov = PMarkov() + self.last = None + + def register(self, value): + # print "registering %s in %s (%s)" % (value, self.markov, + # self.markov.nodes) + if value not in self.markov.nodes: + self.markov.nodes[value] = [] + if self.last is not None: + self.markov.nodes[self.last].append(value) + self.last = value + class MarkovLParallel: - def __init__(self, count): - self.count = count - self.learners = map(lambda n: MarkovLearner(), range(count)) - def register(self, list): - for n in range(self.count): - print "registering %d in %s" % (list[n], self.learners[n]) - self.learners[n].register(list[n]) + def __init__(self, count): + self.count = count + self.learners = map(lambda n: MarkovLearner(), range(count)) - def normalize(self): - for learner in self.learners: - learner.markov.normalize() + def register(self, list): + for n in range(self.count): + print "registering %d in %s" % (list[n], self.learners[n]) + self.learners[n].register(list[n]) - def chains(self): - self.normalize() - return map(lambda learner: learner.markov, self.learners) + def normalize(self): + for learner in self.learners: + learner.markov.normalize() + def chains(self): + self.normalize() + return map(lambda learner: learner.markov, self.learners) diff --git a/isobar/pattern/operator.py b/isobar/pattern/operator.py index e5bb85a..9589562 100644 --- a/isobar/pattern/operator.py +++ b/isobar/pattern/operator.py @@ -8,273 +8,316 @@ from isobar.key import * from isobar.util import * + class PChanged(Pattern): - """ PChanged: Outputs a 1 if the value of a pattern has changed. """ - def __init__(self, source): - self.source = source - self.current = Pattern.value(self.source) + """ PChanged: Outputs a 1 if the value of a pattern has changed. """ + + def __init__(self, source): + self.source = source + self.current = Pattern.value(self.source) + + def next(self): + next = Pattern.value(self.source) + rv = 0 if next == self.current else 1 + self.current = next + return rv - def next(self): - next = Pattern.value(self.source) - rv = 0 if next == self.current else 1 - self.current = next - return rv class PDiff(Pattern): - """ PDiff: Outputs the difference between the current and previous values of an input pattern """ - def __init__(self, source): - self.source = source - self.current = Pattern.value(self.source) + """ PDiff: Outputs the difference between the current and previous values of an input pattern """ + + def __init__(self, source): + self.source = source + self.current = Pattern.value(self.source) + + def next(self): + next = Pattern.value(self.source) + rv = next - self.current + self.current = next + return rv - def next(self): - next = Pattern.value(self.source) - rv = next - self.current - self.current = next - return rv class PAbs(Pattern): - """ PAbs: Absolute value of """ - def __init__(self, input): - self.input = input + """ PAbs: Absolute value of """ + + def __init__(self, input): + self.input = input + + def next(self): + next = Pattern.value(self.input) + if next is not None: + return abs(next) + return next - def next(self): - next = Pattern.value(self.input) - if next is not None: - return abs(next) - return next class PNorm(Pattern): - """ PNorm: Normalise to [0..1]. - Use maximum and minimum values found in history of size . - """ - - def __init__(self, input, window_size = None): - self.input = input - self.window_size = window_size - - self.lower = None - self.upper = None - self.history = [] - - def next(self): - value = Pattern.value(self.input) - window_size = Pattern.value(self.window_size) - - if window_size: - # TODO - pass - else: - if self.lower is None: - self.lower = value - self.upper = value - else: - if value > self.upper: - self.upper = value - if value < self.lower: - self.lower = value - - if self.upper == self.lower: - rv = 0.0 - else: - rv = (value - self.lower) / (self.upper - self.lower) - - return rv; + + """ PNorm: Normalise to [0..1]. + Use maximum and minimum values found in history of size . + """ + + def __init__(self, input, window_size=None): + self.input = input + self.window_size = window_size + + self.lower = None + self.upper = None + self.history = [] + + def next(self): + value = Pattern.value(self.input) + window_size = Pattern.value(self.window_size) + + if window_size: + # TODO + pass + else: + if self.lower is None: + self.lower = value + self.upper = value + else: + if value > self.upper: + self.upper = value + if value < self.lower: + self.lower = value + + if self.upper == self.lower: + rv = 0.0 + else: + rv = (value - self.lower) / (self.upper - self.lower) + + return rv + class PCollapse(Pattern): - """ PCollapse: Skip over any rests in """ - def __init__(self, input): - self.input = input - def next(self): - rv = None - while rv == None: - rv = Pattern.value(self.input) - return rv + """ PCollapse: Skip over any rests in """ + + def __init__(self, input): + self.input = input + + def next(self): + rv = None + while rv is None: + rv = Pattern.value(self.input) + return rv + class PNoRepeats(Pattern): - """ PNoRepeats: Skip over repeated values in """ - def __init__(self, input): - self.input = input - self.value = sys.maxint - - def next(self): - rv = sys.maxint - while rv == self.value or rv == sys.maxint: - rv = Pattern.value(self.input) - self.value = rv - return rv + + """ PNoRepeats: Skip over repeated values in """ + + def __init__(self, input): + self.input = input + self.value = sys.maxsize + + def next(self): + rv = sys.maxsize + while rv == self.value or rv == sys.maxsize: + rv = Pattern.value(self.input) + self.value = rv + return rv + class PReverse(Pattern): - """ reverses a finite sequence """ - def __init__(self, input): - self.input = input - self.values = reversed(list(input)) + """ reverses a finite sequence """ + + def __init__(self, input): + self.input = input + self.values = reversed(list(input)) + + def next(self): + return self.values.next() - def next(self): - return self.values.next() class PDelay(Pattern): - """ outputs the next value of patternA after patternB ticks """ - def __init__(self, source, delay): - self.source = source - self.delay = delay - self.counter = Pattern.value(self.delay) + """ outputs the next value of patternA after patternB ticks """ + + def __init__(self, source, delay): + self.source = source + self.delay = delay + self.counter = Pattern.value(self.delay) + + def next(self): + self.counter -= 1 + if self.counter < 0: + self.counter = Pattern.value(self.delay) + return Pattern.value(self.source) - def next(self): - self.counter -= 1 - if self.counter < 0: - self.counter = Pattern.value(self.delay) - return Pattern.value(self.source) class PMap(Pattern): - """ PMap: Apply an arbitrary function to an input pattern. - Will pass any additional arguments, which can also be patterns. - >>> PMap(PSeries(), lambda value: value * value).nextn(16) - [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225] + """ PMap: Apply an arbitrary function to an input pattern. + Will pass any additional arguments, which can also be patterns. + + >>> PMap(PSeries(), lambda value: value * value).nextn(16) + [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225] + + >>> PMap(PSeries(), pow, PSeries()).nextn(16) + [1, 1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489, 10000000000, ... ] + """ - >>> PMap(PSeries(), pow, PSeries()).nextn(16) - [1, 1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489, 10000000000, ... ] - """ + def __init__(self, input, operator, *args, **kwargs): + self.input = input + self.operator = operator + self.args = args + self.kwargs = kwargs - def __init__(self, input, operator, *args, **kwargs): - self.input = input - self.operator = operator - self.args = args - self.kwargs = kwargs + def next(self): + args = [Pattern.value(value) for value in self.args] + kwargs = dict((key, Pattern.value(value)) + for key, value in self.kwargs.items()) + value = self.input.next() + rv = self.operator(value, *args, **kwargs) + return rv - def next(self): - args = [ Pattern.value(value) for value in self.args ] - kwargs = dict((key, Pattern.value(value)) for key, value in self.kwargs.items()) - value = self.input.next() - rv = self.operator(value, *args, **kwargs) - return rv class PMapEnumerated(PMap): - """ PMapEnumerated: Apply arbitrary function to input, passing a counter. - - >>> PMapEnumerated(PSeq([ 1, 11, 111 ]), lambda n, value: n * value).nextn(16) - [0, 11, 222, 3, 44, 555, 6, 77, 888, 9, 110, 1221, 12, 143, 1554, 15] - """ - - def __init__(self, *args): - PMap.__init__(self, *args) - self.counter = 0 - - def next(self): - args = [ Pattern.value(value) for value in self.args ] - kwargs = dict((key, Pattern.value(value)) for key, value in self.kwargs.items()) - value = self.input.next() - rv = self.operator(self.counter, value, *args, **kwargs) - self.counter += 1 - return rv - + + """ PMapEnumerated: Apply arbitrary function to input, passing a counter. + + >>> PMapEnumerated(PSeq([ 1, 11, 111 ]), lambda n, value: n * value).nextn(16) + [0, 11, 222, 3, 44, 555, 6, 77, 888, 9, 110, 1221, 12, 143, 1554, 15] + """ + + def __init__(self, *args): + PMap.__init__(self, *args) + self.counter = 0 + + def next(self): + args = [Pattern.value(value) for value in self.args] + kwargs = dict((key, Pattern.value(value)) + for key, value in self.kwargs.items()) + value = self.input.next() + rv = self.operator(self.counter, value, *args, **kwargs) + self.counter += 1 + return rv + + class PLinLin(PMap): - """ PLinLin: Map from linear range [a,b] to linear range [c,d]. - >>> p = PLinLin(PWhite(), 0, 1, -50, 50) - >>> p.nextn(16) - [-34.434991496625955, -33.38823791706497, 42.153457333940267, 16.692545937573783, ... -48.850511242044604 ] - """ + """ PLinLin: Map from linear range [a,b] to linear range [c,d]. - def linlin(self, value, from_min = 0, from_max = 1, to_min = 0, to_max = 1): - norm = float(value - from_min) / (from_max - from_min) - return norm * float(to_max - to_min) + to_min + >>> p = PLinLin(PWhite(), 0, 1, -50, 50) + >>> p.nextn(16) + [-34.434991496625955, -33.38823791706497, 42.153457333940267, 16.692545937573783, ... -48.850511242044604 ] + """ + + def linlin(self, value, from_min=0, from_max=1, to_min=0, to_max=1): + norm = float(value - from_min) / (from_max - from_min) + return norm * float(to_max - to_min) + to_min + + def __init__(self, input, *args): + PMap.__init__(self, input, self.linlin, *args) - def __init__(self, input, *args): - PMap.__init__(self, input, self.linlin, *args) class PLinExp(PMap): - """ PLinExp: Map from linear range [a,b] to exponential range [c,d]. - >>> p = PLinExp(PWhite(), 0, 1, 40, 20000) - >>> p.nextn(16) - """ + """ PLinExp: Map from linear range [a,b] to exponential range [c,d]. + + >>> p = PLinExp(PWhite(), 0, 1, 40, 20000) + >>> p.nextn(16) + """ - def linexp(self, value, from_min = 0, from_max = 1, to_min = 1, to_max = 10): - if value < from_min: return to_min - if value > from_max: return to_max - return ((to_max / to_min) ** ((value - from_min) / (from_max - from_min))) * to_min; + def linexp(self, value, from_min=0, from_max=1, to_min=1, to_max=10): + if value < from_min: + return to_min + if value > from_max: + return to_max + return ( + (to_max / to_min) ** ((value - from_min) / (from_max - from_min))) * to_min + + def __init__(self, input, *args): + PMap.__init__(self, input, self.linexp, *args) - def __init__(self, input, *args): - PMap.__init__(self, input, self.linexp, *args) class PRound(PMap): - """ PRound: Round to N decimal places. - >>> p = PLinExp(PWhite(), 0, 1, 40, 20000) - >>> p.nextn(16) - """ - def __init__(self, input, *args): - PMap.__init__(self, input, round, *args) + """ PRound: Round to N decimal places. + + >>> p = PLinExp(PWhite(), 0, 1, 40, 20000) + >>> p.nextn(16) + """ + + def __init__(self, input, *args): + PMap.__init__(self, input, round, *args) + class PIndexOf(Pattern): - """ PIndexOf: Find index of items from in - - >>> p = PIndexOf([ chr(ord("a") + n) for n in range(26) ], PSeq("isobar")) - >>> p.nextn(16) - >>> [8, 18, 14, 1, 0, 17, 8, 18, 14, 1, 0, 17, 8, 18, 14, 1] - """ - def __init__(self, list, item): - self.list = list - self.item = item - - def next(self): - list = Pattern.value(self.list) - item = Pattern.value(self.item) - if list is None or item is None or item not in list: - return None - return list.index(item) + + """ PIndexOf: Find index of items from in + + >>> p = PIndexOf([ chr(ord("a") + n) for n in range(26) ], PSeq("isobar")) + >>> p.nextn(16) + >>> [8, 18, 14, 1, 0, 17, 8, 18, 14, 1, 0, 17, 8, 18, 14, 1] + """ + + def __init__(self, list, item): + self.list = list + self.item = item + + def next(self): + list = Pattern.value(self.list) + item = Pattern.value(self.item) + if list is None or item is None or item not in list: + return None + return list.index(item) + class PPad(Pattern): - """ PPad: Pad with rests until it reaches length . - """ - def __init__(self, pattern, length): - self.pattern = pattern - self.length = length - self.count = 0 + """ PPad: Pad with rests until it reaches length . + """ - def next(self): - try: - rv = self.pattern.next() - except: - if self.count >= self.length: - raise StopIteration - rv = None + def __init__(self, pattern, length): + self.pattern = pattern + self.length = length + self.count = 0 + + def next(self): + try: + rv = self.pattern.next() + except: + if self.count >= self.length: + raise StopIteration + rv = None + + self.count += 1 + return rv - self.count += 1 - return rv class PPadToMultiple(Pattern): - """ PPadToMultiple: Pad with rests until its length is divisible by . - Enforces a minimum padding of . - - Useful to create patterns which occupy a whole number of bars. - """ - - def __init__(self, pattern, multiple, minimum_pad = 0): - self.pattern = pattern - self.multiple = multiple - self.minimum_pad = minimum_pad - self.count = 0 - self.padcount = 0 - self.terminated = False - - def next(self): - try: - rv = self.pattern.next() - except: - if self.padcount >= self.minimum_pad and (self.count % self.multiple == 0): - raise StopIteration - else: - rv = None - self.padcount += 1 - - self.count += 1 - return rv + + """ PPadToMultiple: Pad with rests until its length is divisible by . + Enforces a minimum padding of . + + Useful to create patterns which occupy a whole number of bars. + """ + + def __init__(self, pattern, multiple, minimum_pad=0): + self.pattern = pattern + self.multiple = multiple + self.minimum_pad = minimum_pad + self.count = 0 + self.padcount = 0 + self.terminated = False + + def next(self): + try: + rv = self.pattern.next() + except: + if self.padcount >= self.minimum_pad and ( + self.count % + self.multiple == 0): + raise StopIteration + else: + rv = None + self.padcount += 1 + + self.count += 1 + return rv diff --git a/isobar/pattern/oscillator.py b/isobar/pattern/oscillator.py index 4cb9349..12ed4e8 100644 --- a/isobar/pattern/oscillator.py +++ b/isobar/pattern/oscillator.py @@ -6,33 +6,34 @@ from isobar.pattern.core import * + class PTri(Pattern): - """ PTri: Generates a triangle waveform of period . - >>> p = PTri(10) - >>> p.nextn(10) - [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 0.8, 0.6, 0.4, 0.2] - """ + """ PTri: Generates a triangle waveform of period . - def __init__(self, length = 10): - self.length = length - self.reset() + >>> p = PTri(10) + >>> p.nextn(10) + [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 0.8, 0.6, 0.4, 0.2] + """ - def reset(self): - self.phase = 0.0 + def __init__(self, length=10): + self.length = length + self.reset() - def next(self): - length = Pattern.value(self.length) + def reset(self): + self.phase = 0.0 - norm_phase = float(self.phase) / length - if norm_phase < 0.5: - rv = norm_phase * 2.0 - else: - rv = 1.0 - (norm_phase - 0.5) * 2.0 + def next(self): + length = Pattern.value(self.length) - self.phase += 1 - if self.phase > length: - self.phase -= length + norm_phase = float(self.phase) / length + if norm_phase < 0.5: + rv = norm_phase * 2.0 + else: + rv = 1.0 - (norm_phase - 0.5) * 2.0 - return rv + self.phase += 1 + if self.phase > length: + self.phase -= length + return rv diff --git a/isobar/pattern/sequence.py b/isobar/pattern/sequence.py index 3dafae0..6e5612a 100644 --- a/isobar/pattern/sequence.py +++ b/isobar/pattern/sequence.py @@ -9,669 +9,726 @@ from isobar.key import * from isobar.util import * from isobar.chord import * +from functools import reduce + class PSeq(Pattern): - """ PSeq: Sequence of values based on an array - Takes an input list, and repeats the items in this list. - >>> p = PSeq([ 1, 2, 3, 5 ]) - >>> p.nextn(10) - [1, 2, 3, 5, 1, 2, 3, 5, 1, 2, 3, 5, 1, 2, 3, 5] - """ + """ PSeq: Sequence of values based on an array + Takes an input list, and repeats the items in this list. + + >>> p = PSeq([ 1, 2, 3, 5 ]) + >>> p.nextn(10) + [1, 2, 3, 5, 1, 2, 3, 5, 1, 2, 3, 5, 1, 2, 3, 5] + """ + + def __init__(self, list=[], repeats=sys.maxsize): + #---------------------------------------------------------------------- + # take a copy of the list to avoid changing the original + #---------------------------------------------------------------------- + assert hasattr(list, "__getitem__"), "PSeq must take a list argument" + self.list = copy.copy(list) + self.repeats = repeats - def __init__(self, list = [], repeats = sys.maxint): - #------------------------------------------------------------------------ - # take a copy of the list to avoid changing the original - #------------------------------------------------------------------------ - assert hasattr(list, "__getitem__"), "PSeq must take a list argument" - self.list = copy.copy(list) - self.repeats = repeats + self.reset() - self.reset() + def reset(self): + self.rcount = 0 + self.pos = 0 - def reset(self): - self.rcount = 0 - self.pos = 0 + def next(self): + if len(self.list) == 0 or self.rcount >= self.repeats: + raise StopIteration - def next(self): - if len(self.list) == 0 or self.rcount >= self.repeats: - raise StopIteration + # support for pattern arguments + list = self.value(self.list) + repeats = self.value(self.repeats) - # support for pattern arguments - list = self.value(self.list) - repeats = self.value(self.repeats) + rv = Pattern.value(list[self.pos]) + self.pos += 1 + if self.pos >= len(list): + self.pos = 0 + self.rcount += 1 - rv = Pattern.value(list[self.pos]) - self.pos += 1 - if self.pos >= len(list): - self.pos = 0 - self.rcount += 1 + return rv - return rv class PSeries(Pattern): - """ PSeries: Arithmetic series, beginning at , increment by - - >>> p = PSeries(3, 9) - >>> p.nextn(16) - [3, 12, 21, 30, 39, 48, 57, 66, 75, 84, 93, 102, 111, 120, 129, 138] - """ - - def __init__(self, start = 0, step = 1, length = sys.maxint): - self.start = start - self.value = start - self.step = step - self.length = length - self.count = 0 - - def reset(self): - self.value = self.start - self.count = 0 - - Pattern.reset(self) - - def next(self): - if self.count >= self.length: - # return None - raise StopIteration - step = Pattern.value(self.step) - n = self.value - self.value += step - self.count += 1 - return n + + """ PSeries: Arithmetic series, beginning at , increment by + + >>> p = PSeries(3, 9) + >>> p.nextn(16) + [3, 12, 21, 30, 39, 48, 57, 66, 75, 84, 93, 102, 111, 120, 129, 138] + """ + + def __init__(self, start=0, step=1, length=sys.maxsize): + self.start = start + self.value = start + self.step = step + self.length = length + self.count = 0 + + def reset(self): + self.value = self.start + self.count = 0 + + Pattern.reset(self) + + def next(self): + if self.count >= self.length: + # return None + raise StopIteration + step = Pattern.value(self.step) + n = self.value + self.value += step + self.count += 1 + return n + class PRange(Pattern): - """ PRange: Similar to PSeries, but specify a max/step value. - - >>> p = PRange(0, 20, 2) - >>> p.nextn(16) - [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] - """ - - def __init__(self, start = 0, end = 128, step = 1): - self.start = start - self.end = end - self.step = step - self.reset() - - def reset(self): - self.value = self.start - - Pattern.reset(self) - - def next(self): - step = Pattern.value(self.step) - if step > 0 and self.value >= self.end: - raise StopIteration - elif step < 0 and self.value <= self.end: - raise StopIteration - rv = self.value - self.value += step - return rv + + """ PRange: Similar to PSeries, but specify a max/step value. + + >>> p = PRange(0, 20, 2) + >>> p.nextn(16) + [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] + """ + + def __init__(self, start=0, end=128, step=1): + self.start = start + self.end = end + self.step = step + self.reset() + + def reset(self): + self.value = self.start + + Pattern.reset(self) + + def next(self): + step = Pattern.value(self.step) + if step > 0 and self.value >= self.end: + raise StopIteration + elif step < 0 and self.value <= self.end: + raise StopIteration + rv = self.value + self.value += step + return rv + class PGeom(Pattern): - """ PGeom: Geometric series, beginning at , multiplied by - >>> p = PGeom(1, 2) - >>> p.nextn(16) - [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768] - """ + """ PGeom: Geometric series, beginning at , multiplied by + + >>> p = PGeom(1, 2) + >>> p.nextn(16) + [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768] + """ - def __init__(self, start = 1, multiply = 2, length = sys.maxint): - self.start = start - self.value = start - self.multiply = multiply - self.length = length - self.count = 0 + def __init__(self, start=1, multiply=2, length=sys.maxsize): + self.start = start + self.value = start + self.multiply = multiply + self.length = length + self.count = 0 - def reset(self): - self.value = self.start - self.count = 0 + def reset(self): + self.value = self.start + self.count = 0 - Pattern.reset(self) + Pattern.reset(self) - def next(self): - if self.count >= self.length: - raise StopIteration + def next(self): + if self.count >= self.length: + raise StopIteration - multiply = Pattern.value(self.multiply) + multiply = Pattern.value(self.multiply) + + rv = self.value + self.value *= multiply + self.count += 1 + return rv - rv = self.value - self.value *= multiply - self.count += 1 - return rv class PLoop(Pattern): - """ PLoop: Repeats a finite for repeats. - Useful for pattern generators which don't natively loop. - - Input must be finite or results may vary. - - >>> p = PLoop(PSeq([ 1, 4, 9 ], 1)) - >>> p.nextn(16) - [1, 4, 9, 1, 4, 9, 1, 4, 9, 1, 4, 9, 1, 4, 9, 1] - """ - - def __init__(self, pattern, count = sys.maxint, bang = False): - self.pattern = pattern - self.count = count - self.bang = bang - self.reset() - - def reset(self): - self.pos = 0 - self.rpos = 1 - self.read_all = False - self.values = [] - - Pattern.reset(self) - - def next(self): - # print "%d, %d, %d" % (self.rebang, self.pos, self.rpos) - if self.bang and self.pos >= len(self.values) and self.rpos >= self.count: - self.reset() - self.pattern.bang() - - if not self.read_all: - # print "reading all" - try: - rv = self.pattern.next() - self.values.append(rv) - except StopIteration: - self.read_all = True - - if self.read_all and self.pos >= len(self.values): - if self.rpos >= self.count: - raise StopIteration - else: - self.rpos += 1 - self.pos = 0 - - # print "pos = %d, value len = %d" % (self.pos, len(self.values)) - rv = self.values[self.pos] - self.pos += 1 - return rv + + """ PLoop: Repeats a finite for repeats. + Useful for pattern generators which don't natively loop. + + Input must be finite or results may vary. + + >>> p = PLoop(PSeq([ 1, 4, 9 ], 1)) + >>> p.nextn(16) + [1, 4, 9, 1, 4, 9, 1, 4, 9, 1, 4, 9, 1, 4, 9, 1] + """ + + def __init__(self, pattern, count=sys.maxsize, bang=False): + self.pattern = pattern + self.count = count + self.bang = bang + self.reset() + + def reset(self): + self.pos = 0 + self.rpos = 1 + self.read_all = False + self.values = [] + + Pattern.reset(self) + + def next(self): + # print "%d, %d, %d" % (self.rebang, self.pos, self.rpos) + if self.bang and self.pos >= len( + self.values) and self.rpos >= self.count: + self.reset() + self.pattern.bang() + + if not self.read_all: + # print "reading all" + try: + rv = self.pattern.next() + self.values.append(rv) + except StopIteration: + self.read_all = True + + if self.read_all and self.pos >= len(self.values): + if self.rpos >= self.count: + raise StopIteration + else: + self.rpos += 1 + self.pos = 0 + + # print "pos = %d, value len = %d" % (self.pos, len(self.values)) + rv = self.values[self.pos] + self.pos += 1 + return rv + class PConcat(Pattern): - """ PConcat: Concatenate the output of multiple finite sequences - - >>> PConcat([ PSeq([ 1, 2, 3], 2), PSeq([ 9, 8, 7 ], 2) ]).nextn(16) - [1, 4, 9, 4, 1, 4, 9, 4, 1, 4, 9, 4, 1, 4, 9, 4] - """ - - def __init__(self, inputs): - self.inputs = inputs - self.current = inputs.pop(0) - - def next(self): - try: - return self.current.next() - except StopIteration: - if len(self.inputs) > 0: - self.current = self.inputs.pop(0) - # can't just blindly return the first value of current - # -- what if it is empty? - return self.next() - else: - # no more sequences left, so just return. - raise StopIteration + + """ PConcat: Concatenate the output of multiple finite sequences + + >>> PConcat([ PSeq([ 1, 2, 3], 2), PSeq([ 9, 8, 7 ], 2) ]).nextn(16) + [1, 4, 9, 4, 1, 4, 9, 4, 1, 4, 9, 4, 1, 4, 9, 4] + """ + + def __init__(self, inputs): + self.inputs = inputs + self.current = inputs.pop(0) + + def next(self): + try: + return self.current.next() + except StopIteration: + if len(self.inputs) > 0: + self.current = self.inputs.pop(0) + # can't just blindly return the first value of current + # -- what if it is empty? + return self.next() + else: + # no more sequences left, so just return. + raise StopIteration + class PPingPong(Pattern): - """ PPingPong: Ping-pong input pattern back and forth N times. - - >>> p = PPingPong(PSeq([ 1, 4, 9 ], 1), 10) - >>> p.nextn(16) - [1, 4, 9, 4, 1, 4, 9, 4, 1, 4, 9, 4, 1, 4, 9, 4] - """ - - def __init__(self, pattern, count = 1): - self.pattern = pattern - self.count = count - self.reset() - - def reset(self): - self.pattern.reset() - self.values = self.pattern.all() - self.pos = 0 - self.dir = 1 - self.rpos = 0 - - def next(self): - if self.pos == 0 and self.rpos >= self.count: - raise StopIteration - - rv = self.values[self.pos] - self.pos += self.dir - if self.pos == len(self.values) - 1: - self.dir = -1 - elif self.pos == 0: - self.dir = 1 - self.rpos += 1 - - return rv + + """ PPingPong: Ping-pong input pattern back and forth N times. + + >>> p = PPingPong(PSeq([ 1, 4, 9 ], 1), 10) + >>> p.nextn(16) + [1, 4, 9, 4, 1, 4, 9, 4, 1, 4, 9, 4, 1, 4, 9, 4] + """ + + def __init__(self, pattern, count=1): + self.pattern = pattern + self.count = count + self.reset() + + def reset(self): + self.pattern.reset() + self.values = self.pattern.all() + self.pos = 0 + self.dir = 1 + self.rpos = 0 + + def next(self): + if self.pos == 0 and self.rpos >= self.count: + raise StopIteration + + rv = self.values[self.pos] + self.pos += self.dir + if self.pos == len(self.values) - 1: + self.dir = -1 + elif self.pos == 0: + self.dir = 1 + self.rpos += 1 + + return rv + class PCreep(Pattern): - """ PCreep: Loop -note segment, progressing notes after repeats. - - >>> p = PCreep(PSeries(), 3, 1, 2) - >>> p.nextn(16) - [0, 1, 2, 0, 1, 2, 1, 2, 3, 1, 2, 3, 2, 3, 4, 2] - """ - def __init__(self, pattern, length = 4, creep = 1, count = 1, prob = 1): - self.pattern = pattern - self.length = length - self.creep = creep - self.count = count - self.prob = prob - - self.buffer = [] - self.pos = 0 - self.rcount = 1 - while len(self.buffer) < length: - self.buffer.append(pattern.next()) - - def next(self): - pos = Pattern.value(self.pos) - length = Pattern.value(self.length) - creep = Pattern.value(self.creep) - count = Pattern.value(self.count) - prob = Pattern.value(self.prob) - - while len(self.buffer) < length: - self.buffer.append(self.pattern.next()) - while len(self.buffer) > length: - self.buffer.pop(0) - - if self.pos >= len(self.buffer): - repeat = random.uniform(0, 1) < prob - - if self.rcount >= count or not repeat: - #------------------------------------------------------------------------ - # finished creeping, pull some more data from our buffer - #------------------------------------------------------------------------ - for n in range(creep): - self.buffer.pop(0) - self.buffer.append(self.pattern.next()) - self.rcount = 1 - else: - #------------------------------------------------------------------------ - # finished the Nth repeat but, still more repeats to do - #------------------------------------------------------------------------ - self.rcount += 1 - - #------------------------------------------------------------------------ - # reset to the start of our buffer - #------------------------------------------------------------------------ - if not repeat: - self.pos -= 1 - else: - self.pos = 0 - - self.pos += 1 - return self.buffer[self.pos - 1] + + """ PCreep: Loop -note segment, progressing notes after repeats. + + >>> p = PCreep(PSeries(), 3, 1, 2) + >>> p.nextn(16) + [0, 1, 2, 0, 1, 2, 1, 2, 3, 1, 2, 3, 2, 3, 4, 2] + """ + + def __init__(self, pattern, length=4, creep=1, count=1, prob=1): + self.pattern = pattern + self.length = length + self.creep = creep + self.count = count + self.prob = prob + + self.buffer = [] + self.pos = 0 + self.rcount = 1 + while len(self.buffer) < length: + self.buffer.append(pattern.next()) + + def next(self): + pos = Pattern.value(self.pos) + length = Pattern.value(self.length) + creep = Pattern.value(self.creep) + count = Pattern.value(self.count) + prob = Pattern.value(self.prob) + + while len(self.buffer) < length: + self.buffer.append(self.pattern.next()) + while len(self.buffer) > length: + self.buffer.pop(0) + + if self.pos >= len(self.buffer): + repeat = random.uniform(0, 1) < prob + + if self.rcount >= count or not repeat: + #-------------------------------------------------------------- + # finished creeping, pull some more data from our buffer + #-------------------------------------------------------------- + for n in range(creep): + self.buffer.pop(0) + self.buffer.append(self.pattern.next()) + self.rcount = 1 + else: + #-------------------------------------------------------------- + # finished the Nth repeat but, still more repeats to do + #-------------------------------------------------------------- + self.rcount += 1 + + #------------------------------------------------------------------ + # reset to the start of our buffer + #------------------------------------------------------------------ + if not repeat: + self.pos -= 1 + else: + self.pos = 0 + + self.pos += 1 + return self.buffer[self.pos - 1] + class PStutter(Pattern): - """ PStutter: Play each note of times. - Is really a more convenient way to do: - - PCreep(pattern, 1, 1, count) - - >>> p = PStutter(PSeries(), 2) - >>> p.nextn(16) - [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7] - """ - - def __init__(self, pattern, count = 2): - self.pattern = pattern - self.count = count - self.count_current = Pattern.value(count) - self.pos = self.count_current - self.value = 0 - - def next(self): - if self.pos >= self.count_current: - self.count_current = Pattern.value(self.count) - self.value = self.pattern.next() - self.pos = 0 - self.pos += 1 - return self.value + + """ PStutter: Play each note of times. + Is really a more convenient way to do: + + PCreep(pattern, 1, 1, count) + + >>> p = PStutter(PSeries(), 2) + >>> p.nextn(16) + [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7] + """ + + def __init__(self, pattern, count=2): + self.pattern = pattern + self.count = count + self.count_current = Pattern.value(count) + self.pos = self.count_current + self.value = 0 + + def next(self): + if self.pos >= self.count_current: + self.count_current = Pattern.value(self.count) + self.value = self.pattern.next() + self.pos = 0 + self.pos += 1 + return self.value + class PWrap(Pattern): - """ PWrap: Wrap input note values within , . - - >>> p = PWrap(PSeries(5, 3), 0, 10) - >>> p.nextn(16) - [5, 8, 1, 4, 7, 0, 3, 6, 9, 2, 5, 8, 1, 4, 7, 0] - """ - - def __init__(self, pattern, min = 40, max = 80): - self.pattern = pattern - self.min = min - self.max = max - - def next(self): - value = self.pattern.next() - while value < self.min: - value += self.max - self.min - while value >= self.max: - value -= self.max - self.min - return value + + """ PWrap: Wrap input note values within , . + + >>> p = PWrap(PSeries(5, 3), 0, 10) + >>> p.nextn(16) + [5, 8, 1, 4, 7, 0, 3, 6, 9, 2, 5, 8, 1, 4, 7, 0] + """ + + def __init__(self, pattern, min=40, max=80): + self.pattern = pattern + self.min = min + self.max = max + + def next(self): + value = self.pattern.next() + while value < self.min: + value += self.max - self.min + while value >= self.max: + value -= self.max - self.min + return value + class PPermut(Pattern): - """ PPermut: Generate every permutation of input items. - - >>> p = PPermut(PSeq([ 1, 11, 111, 1111 ]), 4) - >>> p.nextn(16) - [1, 11, 111, 1111, 1, 11, 1111, 111, 1, 111, 11, 1111, 1, 111, 1111, 11] - """ - - def __init__(self, input, count = 8): - self.input = input - self.count = count - self.pos = sys.maxint - self.permindex = sys.maxint - self.permutations = [] - - def reset(self): - self.pos = sys.maxint - self.permindex = sys.maxint - self.permutations = [] - - Pattern.reset(self) - - def next(self): - if self.permindex > len(self.permutations): - n = 0 - values = [] - while n < self.count: - try: - v = self.input.next() - except StopIteration: - break - - values.append(v) - n += 1 - - self.permutations = list(itertools.permutations(values)) - self.permindex = 0 - self.pos = 0 - elif self.pos >= len(self.permutations[0]): - self.permindex = self.permindex + 1 - self.pos = 0 - - if self.permindex >= len(self.permutations): - return None - - rv = self.permutations[self.permindex][self.pos] - self.pos += 1 - return rv + + """ PPermut: Generate every permutation of input items. + + >>> p = PPermut(PSeq([ 1, 11, 111, 1111 ]), 4) + >>> p.nextn(16) + [1, 11, 111, 1111, 1, 11, 1111, 111, 1, 111, 11, 1111, 1, 111, 1111, 11] + """ + + def __init__(self, input, count=8): + self.input = input + self.count = count + self.pos = sys.maxsize + self.permindex = sys.maxsize + self.permutations = [] + + def reset(self): + self.pos = sys.maxsize + self.permindex = sys.maxsize + self.permutations = [] + + Pattern.reset(self) + + def next(self): + if self.permindex > len(self.permutations): + n = 0 + values = [] + while n < self.count: + try: + v = self.input.next() + except StopIteration: + break + + values.append(v) + n += 1 + + self.permutations = list(itertools.permutations(values)) + self.permindex = 0 + self.pos = 0 + elif self.pos >= len(self.permutations[0]): + self.permindex = self.permindex + 1 + self.pos = 0 + + if self.permindex >= len(self.permutations): + return None + + rv = self.permutations[self.permindex][self.pos] + self.pos += 1 + return rv + class PDegree(Pattern): - """ PDegree: Map scale index to MIDI notes in . - >>> p = PDegree(PSeries(0, 1), Scale.major) - >>> p.nextn(16) - [0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23, 24, 26] - """ - def __init__(self, degree, scale = Scale.major): - self.degree = degree - self.scale = scale + """ PDegree: Map scale index to MIDI notes in . + + >>> p = PDegree(PSeries(0, 1), Scale.major) + >>> p.nextn(16) + [0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23, 24, 26] + """ + + def __init__(self, degree, scale=Scale.major): + self.degree = degree + self.scale = scale - def next(self): - degree = Pattern.value(self.degree) - scale = Pattern.value(self.scale) - if degree is None: - return None + def next(self): + degree = Pattern.value(self.degree) + scale = Pattern.value(self.scale) + if degree is None: + return None + + return scale[degree] - return scale[degree] class PSubsequence(Pattern): - """ PSubsequence: Returns a finite subsequence of an input pattern. - >>> p = PSubsequence(PSer - >>> p.nextn(16) - [0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23, 24, 26] - """ - def __init__(self, pattern, offset, length): - self.pattern = pattern - self.offset = offset - self.length = length - self.pos = 0 - self.values = [] + """ PSubsequence: Returns a finite subsequence of an input pattern. + + >>> p = PSubsequence(PSer + >>> p.nextn(16) + [0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23, 24, 26] + """ + + def __init__(self, pattern, offset, length): + self.pattern = pattern + self.offset = offset + self.length = length + self.pos = 0 + self.values = [] - def reset(self): - self.pos = 0 + def reset(self): + self.pos = 0 - Pattern.reset(self) + Pattern.reset(self) - def next(self): - offset = Pattern.value(self.offset) - length = Pattern.value(self.length) + def next(self): + offset = Pattern.value(self.offset) + length = Pattern.value(self.length) - # print "length is %d, pos %d" % (length, self.pos) - if self.pos >= length: - raise StopIteration + # print "length is %d, pos %d" % (length, self.pos) + if self.pos >= length: + raise StopIteration - while len(self.values) <= self.pos + offset: - self.values.append(self.pattern.next()) + while len(self.values) <= self.pos + offset: + self.values.append(self.pattern.next()) - rv = self.values[offset + self.pos] - self.pos += 1 + rv = self.values[offset + self.pos] + self.pos += 1 + + return rv - return rv class PImpulse(Pattern): - """ PImpulse: Outputs a 1 every events, otherwise 0. - - >>> p = PImpulse(4) - >>> p.nextn(16) - [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0] - """ - def __init__(self, period): - self.period = period - self.pos = period - - def reset(self): - self.pos = 0 - - def next(self): - period = self.value(self.period) - - if self.pos >= period - 1: - rv = 1 - self.pos = 0 - else: - rv = 0 - self.pos += 1 - - return rv + + """ PImpulse: Outputs a 1 every events, otherwise 0. + + >>> p = PImpulse(4) + >>> p.nextn(16) + [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0] + """ + + def __init__(self, period): + self.period = period + self.pos = period + + def reset(self): + self.pos = 0 + + def next(self): + period = self.value(self.period) + + if self.pos >= period - 1: + rv = 1 + self.pos = 0 + else: + rv = 0 + self.pos += 1 + + return rv + class PReset(Pattern): - """ PReset: Resets each time it receives a zero-crossing from - - - >>> p = PReset(PSeries(0, 1), PImpulse(4)) - >>> p.nextn(16) - [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] - """ - def __init__(self, pattern, trigger): - self.value = 0 - self.pattern = pattern - self.trigger = trigger - - def reset(self): - self.value = 0 - - def next(self): - value = self.trigger.next() - if value > 0 and self.value <= 0: - self.pattern.reset() - self.value = value - elif value <= 0 and self.value > 0: - self.value = value - - return self.pattern.next() - + + """ PReset: Resets each time it receives a zero-crossing from + + + >>> p = PReset(PSeries(0, 1), PImpulse(4)) + >>> p.nextn(16) + [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + """ + + def __init__(self, pattern, trigger): + self.value = 0 + self.pattern = pattern + self.trigger = trigger + + def reset(self): + self.value = 0 + + def next(self): + value = self.trigger.next() + if value > 0 and self.value <= 0: + self.pattern.reset() + self.value = value + elif value <= 0 and self.value > 0: + self.value = value + + return self.pattern.next() + + class PCounter(Pattern): - """ PCounter: Increments a counter by 1 for each zero-crossing in . - - >>> p = PCounter(PImpulse(4)) - >>> p.nextn(16) - [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] - """ - def __init__(self, trigger): - self.trigger = trigger - self.value = 0 - self.count = 0 - - def next(self): - value = self.trigger.next() - if value > 0 and self.value <= 0: - self.count += 1 - self.value = value - elif value <= 0 and self.value > 0: - self.value = value - - return self.count + + """ PCounter: Increments a counter by 1 for each zero-crossing in . + + >>> p = PCounter(PImpulse(4)) + >>> p.nextn(16) + [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] + """ + + def __init__(self, trigger): + self.trigger = trigger + self.value = 0 + self.count = 0 + + def next(self): + value = self.trigger.next() + if value > 0 and self.value <= 0: + self.count += 1 + self.value = value + elif value <= 0 and self.value > 0: + self.value = value + + return self.count + class PArp(Pattern): - """ PArp: Arpeggiator. - - can be one of: - PArp.UP - PArp.DOWN - PArp.UPDOWN - PArp.CONVERGE - PArp.DIVERGE - PArp.RANDOM - - >>> p = PLoop(PArp(Chord.major, PArp.CONVERGE)) - >>> p.nextn(16) - [0, 12, 4, 7, 0, 12, 4, 7, 0, 12, 4, 7, 0, 12, 4, 7] - """ - UP = 0 - DOWN = 1 - CONVERGE = 2 - DIVERGE = 2 - RANDOM = 3 - - def __init__(self, chord = Chord.major, type = UP): - self.chord = chord - self.type = type - self.pos = 0 - self.offsets = [] - - try: - #------------------------------------------------------------------------ - # prefer to specify a chord (or Key) - #------------------------------------------------------------------------ - self.notes = self.chord.semitones - except: - #------------------------------------------------------------------------ - # can alternatively specify a list of notes - #------------------------------------------------------------------------ - self.notes = self.chord - - if type == PArp.UP: - self.offsets = range(len(self.notes)) - elif type == PArp.DOWN: - self.offsets = list(reversed(range(len(self.notes)))) - elif type == PArp.CONVERGE: - self.offsets = [ (n / 2) if (n % 2 == 0) else (0 - (n + 1) / 2) for n in xrange(len(self.notes)) ] - elif type == PArp.DIVERGE: - self.offsets = [ (n / 2) if (n % 2 == 0) else (0 - (n + 1) / 2) for n in xrange(len(self.notes)) ] - self.offsets = list(reversed(self.offsets)) - elif type == PArp.RANDOM: - self.offsets = range(len(self.notes)) - random.shuffle(self.offsets) - - def next(self): - type = self.value(self.type) - pos = self.value(self.pos) - - if pos < len(self.offsets): - offset = self.offsets[pos] - rv = self.notes[offset] - self.pos = pos + 1 - return rv - else: - raise StopIteration + + """ PArp: Arpeggiator. + + can be one of: + PArp.UP + PArp.DOWN + PArp.UPDOWN + PArp.CONVERGE + PArp.DIVERGE + PArp.RANDOM + + >>> p = PLoop(PArp(Chord.major, PArp.CONVERGE)) + >>> p.nextn(16) + [0, 12, 4, 7, 0, 12, 4, 7, 0, 12, 4, 7, 0, 12, 4, 7] + """ + UP = 0 + DOWN = 1 + CONVERGE = 2 + DIVERGE = 2 + RANDOM = 3 + + def __init__(self, chord=Chord.major, type=UP): + self.chord = chord + self.type = type + self.pos = 0 + self.offsets = [] + + try: + #------------------------------------------------------------------ + # prefer to specify a chord (or Key) + #------------------------------------------------------------------ + self.notes = self.chord.semitones + except: + #------------------------------------------------------------------ + # can alternatively specify a list of notes + #------------------------------------------------------------------ + self.notes = self.chord + + if type == PArp.UP: + self.offsets = range(len(self.notes)) + elif type == PArp.DOWN: + self.offsets = list(reversed(range(len(self.notes)))) + elif type == PArp.CONVERGE: + self.offsets = [(n / + 2) if (n % + 2 == 0) else (0 - + (n + + 1) / + 2) for n in xrange(len(self.notes))] + elif type == PArp.DIVERGE: + self.offsets = [(n / + 2) if (n % + 2 == 0) else (0 - + (n + + 1) / + 2) for n in xrange(len(self.notes))] + self.offsets = list(reversed(self.offsets)) + elif type == PArp.RANDOM: + self.offsets = range(len(self.notes)) + random.shuffle(self.offsets) + + def next(self): + type = self.value(self.type) + pos = self.value(self.pos) + + if pos < len(self.offsets): + offset = self.offsets[pos] + rv = self.notes[offset] + self.pos = pos + 1 + return rv + else: + raise StopIteration + class PEuclidean(Pattern): - """ PEuclidean: Generate Euclidean rhythms. - Effectively tries to space events out evenly over beats. - Events returned are either 1 or None (rest) - - >>> p = PEuclidean(8, 5) - >>> p.nextn(8) - [1, None, 1, 1, None, 1, 1, None] - """ - def __init__(self, length, mod, phase = 0): - self.length = length - self.mod = mod - self.sequence = [] - self.pos = phase - - def next(self): - length = self.value(self.length) - mod = self.value(self.mod) - sequence = self.euclidean(length, mod) - if self.pos >= len(sequence): - self.pos = 0 - rv = sequence[self.pos] - self.pos += 1 - return rv - - def split_remainder(self, seq): - last = None - a = [] - b = [] - for value in seq: - if last is None or value == last: - a.append(value) - last = value - else: - b.append(value) - return (a, b) - - def interleave(self, a, b): - if len(a) < len(b): - return [ a[n] + b[n] for n in range(len(a)) ] + b[len(a) - len(b):] - elif len(b) < len(a): - return [ a[n] + b[n] for n in range(len(b)) ] + a[len(b) - len(a):] - else: - return [ a[n] + b[n] for n in range(len(b)) ] - - def euclidean(self, length, mod): - seqs = [ (1,) ] * mod + [ (None,) ] * (length - mod) - seqs, remainder = self.split_remainder(seqs) - while True: - if len(remainder) <= 1: - break - seqs = self.interleave(seqs, remainder) - seqs, remainder = self.split_remainder(seqs) - - return reduce(lambda a, b: a + b, seqs + remainder) + + """ PEuclidean: Generate Euclidean rhythms. + Effectively tries to space events out evenly over beats. + Events returned are either 1 or None (rest) + + >>> p = PEuclidean(8, 5) + >>> p.nextn(8) + [1, None, 1, 1, None, 1, 1, None] + """ + + def __init__(self, length, mod, phase=0): + self.length = length + self.mod = mod + self.sequence = [] + self.pos = phase + + def next(self): + length = self.value(self.length) + mod = self.value(self.mod) + sequence = self.euclidean(length, mod) + if self.pos >= len(sequence): + self.pos = 0 + rv = sequence[self.pos] + self.pos += 1 + return rv + + def split_remainder(self, seq): + last = None + a = [] + b = [] + for value in seq: + if last is None or value == last: + a.append(value) + last = value + else: + b.append(value) + return (a, b) + + def interleave(self, a, b): + if len(a) < len(b): + return [a[n] + b[n] for n in range(len(a))] + b[len(a) - len(b):] + elif len(b) < len(a): + return [a[n] + b[n] for n in range(len(b))] + a[len(b) - len(a):] + else: + return [a[n] + b[n] for n in range(len(b))] + + def euclidean(self, length, mod): + seqs = [(1,)] * mod + [(None,)] * (length - mod) + seqs, remainder = self.split_remainder(seqs) + while True: + if len(remainder) <= 1: + break + seqs = self.interleave(seqs, remainder) + seqs, remainder = self.split_remainder(seqs) + + return reduce(lambda a, b: a + b, seqs + remainder) + class PDecisionPoint(Pattern): - """ PDecisionPoint: Each time its pattern is exhausted, requests a new pattern by calling . - - >>> - >>> - """ - def __init__(self, fn): - self.fn = fn - self.pattern = self.fn() - - def next(self): - try: - return self.pattern.next() - except StopIteration: - self.pattern = self.fn() - # if not self.pattern: - # causes this to break horribly in the PStaticSeq case - # -- seems to try to evaluate self.pattern as a list - if self.pattern is None: - return None - return self.next() + """ PDecisionPoint: Each time its pattern is exhausted, requests a new pattern by calling . + + >>> + >>> + """ + + def __init__(self, fn): + self.fn = fn + self.pattern = self.fn() + + def next(self): + try: + return self.pattern.next() + except StopIteration: + self.pattern = self.fn() + # if not self.pattern: + # causes this to break horribly in the PStaticSeq case + # -- seems to try to evaluate self.pattern as a list + if self.pattern is None: + return None + return self.next() diff --git a/isobar/pattern/static.py b/isobar/pattern/static.py index 720ecd9..a85b4bd 100644 --- a/isobar/pattern/static.py +++ b/isobar/pattern/static.py @@ -3,132 +3,139 @@ import inspect import math + class PStaticViaOSC (Pattern): - initialised = False - - def __init__(self, default = 0, address = "/value", port = 9900): - if not PStaticViaOSC.initialised: - import osc + initialised = False + + def __init__(self, default=0, address="/value", port=9900): + if not PStaticViaOSC.initialised: + import osc + + osc.init() + osc.listen(port=port) - osc.init() - osc.listen(port = port) + self.value = default + self.address = address + osc.bind(self.recv, address) - self.value = default - self.address = address - osc.bind(self.recv, address) + def recv(self, msg, source=None): + address = msg[0] + signature = msg[1][1:] + print "(%s) %s" % (address, signature) + self.value = msg[2] - def recv(self, msg, source = None): - address = msg[0] - signature = msg[1][1:] - print "(%s) %s" % (address, signature) - self.value = msg[2] + def next(self): + return self.value - def next(self): - return self.value class PStaticTimeline (Pattern): - """ PStaticTimeline: Returns the position (in beats) of the current timeline. """ - def __init__(self, timeline = None): - self.given_timeline = timeline + """ PStaticTimeline: Returns the position (in beats) of the current timeline. """ + + def __init__(self, timeline=None): + self.given_timeline = timeline + + def next(self): + beats = self.get_beats() + return round(beats, 5) - def next(self): - beats = self.get_beats() - return round(beats, 5) + def get_beats(self): + #---------------------------------------------------------------------- + # using the specified timeline (if given) or the currently-embedded + # timeline (otherwise), return the current position in beats. + #---------------------------------------------------------------------- + timeline = self.given_timeline if self.given_timeline else self.timeline + if timeline: + return timeline.beats - def get_beats(self): - #------------------------------------------------------------------------ - # using the specified timeline (if given) or the currently-embedded - # timeline (otherwise), return the current position in beats. - #------------------------------------------------------------------------ - timeline = self.given_timeline if self.given_timeline else self.timeline - if timeline: - return timeline.beats + return 0 - return 0 class PStaticGlobal(Pattern): - """ PStaticGlobal: Static global value identified by a string, with OSC listener """ - dict = {} - listening = False - - def __init__(self, name, value = None): - self.name = name - if value is not None: - PStaticGlobal.set(name, value) - - def next(self): - name = Pattern.value(self.name) - value = PStaticGlobal.dict[name] - return Pattern.value(value) - - # BROKEN: not sure why - @classmethod - def _get(self, key): - value = PStaticGlobal.dict[key] - return Pattern.value(value) - - @classmethod - def set(self, key, value): - PStaticGlobal.dict[key] = value - - @classmethod - def listen(self, prefix = "/global", port = 9900): - if not PStaticGlobal.listening: - import threading - import OSC - - server = OSC.OSCServer(("localhost", port)) - server.addMsgHandler(prefix, self.recv) - self.thread = threading.Thread(target = server.serve_forever) - self.thread.setDaemon(True) - self.thread.start() - - PStaticGlobal.listening = True - - self.prefix = prefix - - @classmethod - def recv(self, addr, tags, data, client_address): - # print "GOT MSG" - key = data[0] - value = data[1] - print "(%s) %s = %.1f" % (addr, key, value) - PStaticGlobal.set(key, value) + + """ PStaticGlobal: Static global value identified by a string, with OSC listener """ + dict = {} + listening = False + + def __init__(self, name, value=None): + self.name = name + if value is not None: + PStaticGlobal.set(name, value) + + def next(self): + name = Pattern.value(self.name) + value = PStaticGlobal.dict[name] + return Pattern.value(value) + + # BROKEN: not sure why + @classmethod + def _get(self, key): + value = PStaticGlobal.dict[key] + return Pattern.value(value) + + @classmethod + def set(self, key, value): + PStaticGlobal.dict[key] = value + + @classmethod + def listen(self, prefix="/global", port=9900): + if not PStaticGlobal.listening: + import threading + import OSC + + server = OSC.OSCServer(("localhost", port)) + server.addMsgHandler(prefix, self.recv) + self.thread = threading.Thread(target=server.serve_forever) + self.thread.setDaemon(True) + self.thread.start() + + PStaticGlobal.listening = True + + self.prefix = prefix + + @classmethod + def recv(self, addr, tags, data, client_address): + # print "GOT MSG" + key = data[0] + value = data[1] + print "(%s) %s = %.1f" % (addr, key, value) + PStaticGlobal.set(key, value) + class PStaticTimelineSine(PStaticTimeline): - def __init__(self, period): - PStaticTimeline.__init__(self) - self.period = period - def next(self): - period = Pattern.value(self.period) - # self.phase += math.pi * 2.0 / period + def __init__(self, period): + PStaticTimeline.__init__(self) + self.period = period + + def next(self): + period = Pattern.value(self.period) + # self.phase += math.pi * 2.0 / period - beats = self.get_beats() - rv = math.sin(2 * math.pi * beats / self.period) - return rv + beats = self.get_beats() + rv = math.sin(2 * math.pi * beats / self.period) + return rv class PStaticSeq(Pattern): - def __init__(self, sequence, duration): - #------------------------------------------------------------------------ - # take a copy of the list to avoid changing the original - #------------------------------------------------------------------------ - self.sequence = copy.copy(sequence) - self.duration = duration - self.start = None - - def next(self): - timeline = self.timeline - if self.start is None: - self.start = round(timeline.beats, 5) - - now = round(timeline.beats, 5) - if now - self.start >= self.duration: - self.sequence.pop(0) - self.start = now - if len(self.sequence) == 0: - raise StopIteration - return self.sequence[0] + def __init__(self, sequence, duration): + #---------------------------------------------------------------------- + # take a copy of the list to avoid changing the original + #---------------------------------------------------------------------- + self.sequence = copy.copy(sequence) + self.duration = duration + self.start = None + + def next(self): + timeline = self.timeline + if self.start is None: + self.start = round(timeline.beats, 5) + + now = round(timeline.beats, 5) + if now - self.start >= self.duration: + self.sequence.pop(0) + self.start = now + if len(self.sequence) == 0: + raise StopIteration + return self.sequence[0] diff --git a/isobar/pattern/warp.py b/isobar/pattern/warp.py index e1a5e3c..519240d 100644 --- a/isobar/pattern/warp.py +++ b/isobar/pattern/warp.py @@ -3,104 +3,113 @@ import math + class PWarp(Pattern): - pass + pass + class PWInterpolate(PWarp): - """ PWInterpolate: Requests a new target warp value from every beats - and applies linear interpolation to ramp between values. - To select a new target warp value every 8 beats, between [-0.5, 0.5]: + """ PWInterpolate: Requests a new target warp value from every beats + and applies linear interpolation to ramp between values. + + To select a new target warp value every 8 beats, between [-0.5, 0.5]: + + >>> p = PWInterpolate(PWhite(-0.5, 0.5), 8) + """ - >>> p = PWInterpolate(PWhite(-0.5, 0.5), 8) - """ - def __init__(self, pattern, length = 1): - self.length = length - self.pattern = pattern - self.pos = self.length - self.value = 0.0 + def __init__(self, pattern, length=1): + self.length = length + self.pattern = pattern + self.pos = self.length + self.value = 0.0 - def next(self): - rv = self.value + def next(self): + rv = self.value - #------------------------------------------------------------------------ - # keep ticking until we have reached our period (length, in beats) - #------------------------------------------------------------------------ - self.pos += 1.0 / TICKS_PER_BEAT - if self.pos >= self.length: - self.pos = 0 - self.target = self.pattern.next() + #---------------------------------------------------------------------- + # keep ticking until we have reached our period (length, in beats) + #---------------------------------------------------------------------- + self.pos += 1.0 / TICKS_PER_BEAT + if self.pos >= self.length: + self.pos = 0 + self.target = self.pattern.next() - #------------------------------------------------------------------------ - # in case our length parameter is also a pattern: obtain a scalar value. - # dv is used for linear interpolation until the next target reached. - #------------------------------------------------------------------------ - length = Pattern.value(self.length) - self.dv = (self.target - self.value) / (TICKS_PER_BEAT * length) + #------------------------------------------------------------------ + # in case our length parameter is also a pattern: obtain a scalar value. + # dv is used for linear interpolation until the next target reached. + #------------------------------------------------------------------ + length = Pattern.value(self.length) + self.dv = (self.target - self.value) / (TICKS_PER_BEAT * length) - self.value = self.value + self.dv + self.value = self.value + self.dv + + return rv - return rv class PWSine(PWarp): - """ PWSine: Sinosoidal warp, period beats, amplitude +/-. - >>> p = PWAmp(8, 0.5) - """ - def __init__(self, length = 1, amp = 0.5): - self.length = length - self.amp = amp - self.pos = 0.0 + """ PWSine: Sinosoidal warp, period beats, amplitude +/-. + + >>> p = PWAmp(8, 0.5) + """ + + def __init__(self, length=1, amp=0.5): + self.length = length + self.amp = amp + self.pos = 0.0 + + def next(self): + self.pos += 1.0 / TICKS_PER_BEAT + if self.pos > self.length: + self.pos -= self.length - def next(self): - self.pos += 1.0 / TICKS_PER_BEAT - if self.pos > self.length: - self.pos -= self.length + # normalize to [0, 1] + pos_norm = self.pos / self.length + warp = math.sin(2.0 * math.pi * pos_norm) + warp = warp * self.amp - # normalize to [0, 1] - pos_norm = self.pos / self.length - warp = math.sin(2.0 * math.pi * pos_norm) - warp = warp * self.amp + return warp - return warp class PWRallantando(PWarp): - """ PWRallantando: Exponential deceleration to times the current tempo over beats. - - >>> p = PWRallantando(8, 0.5) - """ - def __init__(self, length = 1, amp = 0.5): - self.length = length - self.amp = amp - self.pos = 0.0 - self.value = 1.0 - self.length_cur = -1.0 - self.dv = None - - def next(self): - rv = self.value - - #------------------------------------------------------------------------ - # need to round or we might succumb to the dreaded python rounding - # error (eg 0.99999 < 0 when multiplying 1/24.0 by 24) - #------------------------------------------------------------------------ - if round(self.pos, 8) >= round(self.length_cur, 8): - self.value = 1.0 - rv = 1.0 - self.pos = 0 - self.length_cur = Pattern.value(self.length) - amp_cur = Pattern.value(self.amp) - rate_start = 1.0 - rate_end = 1.0 + amp_cur - steps = TICKS_PER_BEAT * self.length_cur - self.dv = math.exp(math.log(rate_end / rate_start) / steps) - - self.pos += 1.0 / TICKS_PER_BEAT - self.value = self.value * self.dv - #------------------------------------------------------------------------ - # subtract - #------------------------------------------------------------------------ - rv = math.log(rv, 2) - print "warp: %f" % rv - return rv + """ PWRallantando: Exponential deceleration to times the current tempo over beats. + + >>> p = PWRallantando(8, 0.5) + """ + + def __init__(self, length=1, amp=0.5): + self.length = length + self.amp = amp + self.pos = 0.0 + self.value = 1.0 + self.length_cur = -1.0 + self.dv = None + + def next(self): + rv = self.value + + #---------------------------------------------------------------------- + # need to round or we might succumb to the dreaded python rounding + # error (eg 0.99999 < 0 when multiplying 1/24.0 by 24) + #---------------------------------------------------------------------- + if round(self.pos, 8) >= round(self.length_cur, 8): + self.value = 1.0 + rv = 1.0 + self.pos = 0 + self.length_cur = Pattern.value(self.length) + amp_cur = Pattern.value(self.amp) + rate_start = 1.0 + rate_end = 1.0 + amp_cur + steps = TICKS_PER_BEAT * self.length_cur + self.dv = math.exp(math.log(rate_end / rate_start) / steps) + + self.pos += 1.0 / TICKS_PER_BEAT + self.value = self.value * self.dv + #---------------------------------------------------------------------- + # subtract + #---------------------------------------------------------------------- + rv = math.log(rv, 2) + print "warp: %f" % rv + return rv diff --git a/isobar/scale.py b/isobar/scale.py index 1774b53..f96e25e 100644 --- a/isobar/scale.py +++ b/isobar/scale.py @@ -1,154 +1,191 @@ import random from util import * + class Scale(object): - dict = { } - - def __init__(self, semitones = [ 0, 2, 4, 5, 7, 9, 11 ], name = "unnamed scale", octave_size = 12): - self.semitones = semitones - """ For polymorphism with WeightedScale -- assume all notes equally weighted. """ - self.weights = [ 1.0 / len(self.semitones) for _ in range(len(self.semitones)) ] - self.name = name - self.octave_size = octave_size - if not Scale.dict.has_key(name): - Scale.dict[name] = self - - def __str__(self): - return "%s %s" % (self.name, self.semitones) - - def __getitem__(self, key): - return self.get(key) - - def __eq__(self, other): - return self.semitones == other.semitones and self.octave_size == other.octave_size - - def __contains__(self, semitone): - return (semitone % self.scale.octave_size) in self.semitones - - def get(self, n): - """ Retrieve the n'th degree of this scale. """ - if n is None: - return None - - octave = int(n / len(self.semitones)) - degree = n % len(self.semitones) - note = (self.octave_size * octave) + self.semitones[degree] - return note - - def copy(self): - other = Scale(self.semitones, self.name) - return other - - def change(self): - """ Exchange two random elements of this scale. """ - i = random.randint(0, len(self.semitones) - 1) - j = random.randint(0, len(self.semitones) - 1) - if i <> j: - tmp = self.semitones[i] - self.semitones[i] = self.semitones[j] - self.semitones[j] = tmp - return self - - def shuffle(self): - random.shuffle(self.semitones) - return self - - def indexOf(self, note): - """ Return the index of the given note within this scale. """ - octave = int(note / self.octave_size) - index = octave * len(self.semitones) - note -= octave * self.octave_size - degree = 0 - - while note > self.semitones[degree] and degree < len(self.semitones) - 1: - degree += 1 - - index += degree - return index - - @staticmethod - def fromnotes(notes, name = "unnamed scale", octave_size = 12): - notes = [ note % octave_size for note in notes ] - notes = dict((k, k) for k in notes).keys() - notes = sorted(notes) - scale = Scale(notes, name = name, octave_size = octave_size) - return scale - - @staticmethod - def all(): - return Scale.dict.values() - - @staticmethod - def byname(name): - return Scale.dict[name] - - @staticmethod - def random(): - key = random.choice(Scale.dict.keys()) - return Scale.dict[key] - -Scale.chromatic = Scale([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ], "chromatic") -Scale.major = Scale([ 0, 2, 4, 5, 7, 9, 11 ], "major") -Scale.maj7 = Scale([ 0, 2, 4, 5, 7, 9, 10 ], "maj7") ## XXX? -Scale.minor = Scale([ 0, 2, 3, 5, 7, 8, 11 ], "minor") -Scale.pureminor = Scale([ 0, 3, 7 ], "pureminor") -Scale.puremajor = Scale([ 0, 4, 7 ], "puremajor") -Scale.minorPenta = Scale([ 0, 3, 5, 7, 10 ], "minorPenta") -Scale.majorPenta = Scale([ 0, 2, 4, 7, 9 ], "majorPenta") -Scale.ritusen = Scale([ 0, 2, 5, 7, 9 ], "ritusen") -Scale.pelog = Scale([ 0, 1, 3, 7, 8 ], "pelog") -Scale.augmented = Scale([ 0, 3, 4, 7, 8, 11 ], "augmented") -Scale.augmented2 = Scale([ 0, 1, 4, 5, 8, 9 ], "augmented 2") -Scale.wholetone = Scale([ 0, 2, 4, 6, 8, 10 ], "wholetone") - -Scale.ionian = Scale([ 0, 2, 4, 5, 7, 9, 11 ], "ionian") -Scale.dorian = Scale([ 0, 2, 3, 5, 7, 9, 10 ], "dorian") -Scale.phrygian = Scale([ 0, 1, 3, 5, 7, 8, 10 ], "phrygian") -Scale.lydian = Scale([ 0, 2, 4, 6, 7, 9, 11 ], "lydian") -Scale.mixolydian = Scale([ 0, 2, 4, 5, 7, 9, 10 ], "mixolydian") -Scale.aeolian = Scale([ 0, 2, 3, 5, 7, 8, 10 ], "aeolian") -Scale.locrian = Scale([ 0, 1, 3, 5, 6, 8, 10 ], "locrian") -Scale.fourths = Scale([ 0, 2, 5, 7 ], "fourths") - -#------------------------------------------------------------------------ -# Use major scale as a global default. This can be overriden by user. -#------------------------------------------------------------------------ -Scale.default = Scale.major + dict = {} + + def __init__( + self, + semitones=[ + 0, + 2, + 4, + 5, + 7, + 9, + 11], + name="unnamed scale", + octave_size=12): + self.semitones = semitones + """ For polymorphism with WeightedScale -- assume all notes equally weighted. """ + self.weights = [1.0 / len(self.semitones) + for _ in range(len(self.semitones))] + self.name = name + self.octave_size = octave_size + if name not in Scale.dict: + Scale.dict[name] = self + + def __str__(self): + return "%s %s" % (self.name, self.semitones) + + def __getitem__(self, key): + return self.get(key) + + def __eq__(self, other): + return self.semitones == other.semitones and self.octave_size == other.octave_size + + def __contains__(self, semitone): + return (semitone % self.scale.octave_size) in self.semitones + + def get(self, n): + """ Retrieve the n'th degree of this scale. """ + if n is None: + return None + + octave = int(n / len(self.semitones)) + degree = n % len(self.semitones) + note = (self.octave_size * octave) + self.semitones[degree] + return note + + def copy(self): + other = Scale(self.semitones, self.name) + return other + + def change(self): + """ Exchange two random elements of this scale. """ + i = random.randint(0, len(self.semitones) - 1) + j = random.randint(0, len(self.semitones) - 1) + if i != j: + tmp = self.semitones[i] + self.semitones[i] = self.semitones[j] + self.semitones[j] = tmp + return self + + def shuffle(self): + random.shuffle(self.semitones) + return self + + def indexOf(self, note): + """ Return the index of the given note within this scale. """ + octave = int(note / self.octave_size) + index = octave * len(self.semitones) + note -= octave * self.octave_size + degree = 0 + + while note > self.semitones[ + degree] and degree < len(self.semitones) - 1: + degree += 1 + + index += degree + return index + + @staticmethod + def fromnotes(notes, name="unnamed scale", octave_size=12): + notes = [note % octave_size for note in notes] + notes = dict((k, k) for k in notes).keys() + notes = sorted(notes) + scale = Scale(notes, name=name, octave_size=octave_size) + return scale + + @staticmethod + def all(): + return Scale.dict.values() + + @staticmethod + def byname(name): + return Scale.dict[name] + + @staticmethod + def random(): + key = random.choice(Scale.dict.keys()) + return Scale.dict[key] + +Scale.chromatic = Scale([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "chromatic") +Scale.major = Scale([0, 2, 4, 5, 7, 9, 11], "major") +Scale.maj7 = Scale([0, 2, 4, 5, 7, 9, 10], "maj7") # XXX? +Scale.minor = Scale([0, 2, 3, 5, 7, 8, 11], "minor") +Scale.pureminor = Scale([0, 3, 7], "pureminor") +Scale.puremajor = Scale([0, 4, 7], "puremajor") +Scale.minorPenta = Scale([0, 3, 5, 7, 10], "minorPenta") +Scale.majorPenta = Scale([0, 2, 4, 7, 9], "majorPenta") +Scale.ritusen = Scale([0, 2, 5, 7, 9], "ritusen") +Scale.pelog = Scale([0, 1, 3, 7, 8], "pelog") +Scale.augmented = Scale([0, 3, 4, 7, 8, 11], "augmented") +Scale.augmented2 = Scale([0, 1, 4, 5, 8, 9], "augmented 2") +Scale.wholetone = Scale([0, 2, 4, 6, 8, 10], "wholetone") + +Scale.ionian = Scale([0, 2, 4, 5, 7, 9, 11], "ionian") +Scale.dorian = Scale([0, 2, 3, 5, 7, 9, 10], "dorian") +Scale.phrygian = Scale([0, 1, 3, 5, 7, 8, 10], "phrygian") +Scale.lydian = Scale([0, 2, 4, 6, 7, 9, 11], "lydian") +Scale.mixolydian = Scale([0, 2, 4, 5, 7, 9, 10], "mixolydian") +Scale.aeolian = Scale([0, 2, 3, 5, 7, 8, 10], "aeolian") +Scale.locrian = Scale([0, 1, 3, 5, 6, 8, 10], "locrian") +Scale.fourths = Scale([0, 2, 5, 7], "fourths") + +''' +Use major scale as a global default. This can be overriden by user. +''' + +Scale.default = Scale.major + class WeightedScale(Scale): - def __init__(self, semitones = [ 0, 2, 4, 5, 7, 9, 11 ], weights = [ 1/7.0 ] * 7, name = "major", octave_size = 12): - Scale.__init__(self, semitones, name = name, octave_size = octave_size) - self.weights = weights - if not Scale.dict.has_key(name): - Scale.dict[name] = self - - def __str__(self): - return "%s %s weights = %s" % (self.name, self.semitones, self.weights) - - @staticmethod - def fromnotes(notes, name = "unnamed scale", octave_size = 12): - note_sequence = [ note % octave_size for note in notes ] - notes_dict = {} - for note in note_sequence: - if not note in notes_dict: - notes_dict[note] = 0 - notes_dict[note] += 1.0 / len(note_sequence) - - notes_unique = dict((k, k) for k in note_sequence).keys() - notes_unique = sorted(notes_unique) - weights = [] - for note in notes_unique: - weights.append(notes_dict[note]) - - scale = WeightedScale(notes_unique, weights, name = name, octave_size = octave_size) - return scale - - @staticmethod - def fromorder(notes, name = "unnamed scale", octave_size = 12): - notes = [ note % octave_size for note in notes ] - weights = [ len(notes) - n for n in range(len(notes)) ] - weights = normalize(weights) - - scale = WeightedScale(notes, weights, name = name, octave_size = octave_size) - return scale - + + def __init__( + self, + semitones=[ + 0, + 2, + 4, + 5, + 7, + 9, + 11], + weights=[ + 1 / 7.0] * 7, + name="major", + octave_size=12): + Scale.__init__(self, semitones, name=name, octave_size=octave_size) + self.weights = weights + if name not in Scale.dict: + Scale.dict[name] = self + + def __str__(self): + return "%s %s weights = %s" % (self.name, self.semitones, self.weights) + + @staticmethod + def fromnotes(notes, name="unnamed scale", octave_size=12): + note_sequence = [note % octave_size for note in notes] + notes_dict = {} + for note in note_sequence: + if note not in notes_dict: + notes_dict[note] = 0 + notes_dict[note] += 1.0 / len(note_sequence) + + notes_unique = dict((k, k) for k in note_sequence).keys() + notes_unique = sorted(notes_unique) + weights = [] + for note in notes_unique: + weights.append(notes_dict[note]) + + scale = WeightedScale( + notes_unique, + weights, + name=name, + octave_size=octave_size) + return scale + + @staticmethod + def fromorder(notes, name="unnamed scale", octave_size=12): + notes = [note % octave_size for note in notes] + weights = [len(notes) - n for n in range(len(notes))] + weights = normalize(weights) + + scale = WeightedScale( + notes, + weights, + name=name, + octave_size=octave_size) + return scale diff --git a/isobar/timeline.py b/isobar/timeline.py index 583dc34..5571213 100644 --- a/isobar/timeline.py +++ b/isobar/timeline.py @@ -11,477 +11,522 @@ TICKS_PER_BEAT = 24 + class Timeline(object): - CLOCK_INTERNAL = 0 - CLOCK_EXTERNAL = 1 - - def __init__(self, bpm = 120, device = None, debug = None): - """expect to receive one tick per beat, generate events at 120bpm""" - self.ticklen = 1.0 / TICKS_PER_BEAT - self.beats = 0 - self.devices = [ device ] if device else [] - self.channels = [] - self.automators = [] - self.max_channels = 0 - - self.bpm = None - self.clock = None - self.clocksource = None - self.thread = None - - self.stop_when_done = False - - if debug is not None: - isobar.debug = debug - - self.events = [] - - if hasattr(bpm, "clocktarget"): - bpm.clocktarget = self - self.clocksource = bpm - self.clockmode = self.CLOCK_EXTERNAL - else: - self.bpm = bpm - #------------------------------------------------------------------------ - # Create clock with a tick-size of 1/24th of a beat. - #------------------------------------------------------------------------ - self.clock = Clock(60.0 / (self.bpm * TICKS_PER_BEAT)) - self.clockmode = self.CLOCK_INTERNAL - - def tick(self): - # if round(self.beats, 5) % 1 == 0: - # print "tick (%d active channels, %d pending events)" % (len(self.channels), len(self.events)) - - #------------------------------------------------------------------------ - # copy self.events because removing from it whilst using it = bad idea. - # perform events before channels are executed because an event might - # include scheduling a quantized channel, which should then be - # immediately evaluated. - #------------------------------------------------------------------------ - for event in self.events[:]: - #------------------------------------------------------------------------ - # the only event we currently get in a Timeline are add_channel events - # -- which have a raw function associated with them. - # - # round needed because we can sometimes end up with beats = 3.99999999... - # http://docs.python.org/tutorial/floatingpoint.html - #------------------------------------------------------------------------ - if round(event["time"], 8) <= round(self.beats, 8): - event["fn"]() - self.events.remove(event) - - #------------------------------------------------------------------------ - # some devices (ie, MidiFileOut) require being told to tick - #------------------------------------------------------------------------ - for device in self.devices: - device.tick(self.ticklen) - - #------------------------------------------------------------------------ - # copy self.channels because removing from it whilst using it = bad idea - #------------------------------------------------------------------------ - for channel in self.channels[:]: - channel.tick(self.ticklen) - if channel.finished: - self.channels.remove(channel) - - if self.stop_when_done and len(self.channels) == 0: - raise StopIteration - - #------------------------------------------------------------------------ - # TODO: should automator and channel inherit from a common superclass? - # one is continuous, one is discrete. - #------------------------------------------------------------------------ - for automator in self.automators[:]: - automator.tick(self.ticklen) - if automator.finished: - self.automators.remove(automator) - - self.beats += self.ticklen - - def dump(self): - """ Output a summary of this Timeline object - """ - print "Timeline (clock: %s)" % ("external" if self.clockmode == self.CLOCK_EXTERNAL else "%sbpm" % self.bpm) - - print " - %d devices" % len(self.devices) - for device in self.devices: - print " - %s" % device - - print " - %d channels" % len(self.channels) - for channel in self.channels: - print" - %s" % channel - - def reset_to_beat(self): - self.beats = round(self.beats) - for channel in self.channels: - channel.reset_to_beat() - - def reset(self): - # print "tl reset!" - self.beats = -.0001 - # XXX probably shouldn't have to do this - should channels read the tl ticks value? - for channel in self.channels: - channel.reset() - - def background(self): - """ Run this Timeline in a background thread. """ - self.thread = thread.start_new_thread(self.run, ()) - - def run(self, high_priority = True): - """ Create clock with 64th-beat granularity. - By default, attempts to run as a high-priority thread - (though requires being run as root to re-nice the process) """ - try: - import os - os.nice(-20) - print "Timeline: Running as high-priority thread" - except: - pass - - try: - #------------------------------------------------------------------------ - # Start the clock. This might internal (eg a Clock object, running on - # an independent thread), or external (eg a MIDI clock). - #------------------------------------------------------------------------ - if self.clockmode == self.CLOCK_INTERNAL: - self.clock.run(self) - else: - self.clocksource.run() - - except StopIteration: - #------------------------------------------------------------------------ - # This will be hit if every Pattern in a timeline is exhausted. - #------------------------------------------------------------------------ - print "Timeline finished" - - except Exception, e: - print " *** Exception in background Timeline thread: %s" % e - traceback.print_exc(file = sys.stdout) - - def warp(self, warper): - self.clock.warp(warper) - - def unwarp(self, warper): - self.clock.warp(warper) - - def set_output(self, device): - self.devices.clear() - self.add_output(device) - - def add_output(self, device): - self.devices.append(device) - - def sched(self, event, quantize = 0, delay = 0, count = 0, device = None): - if not device: - if not self.devices: - isobar.log("Adding default MIDI output") - self.add_output(isobar.io.MidiOut()) - device = self.devices[0] - - if self.max_channels and len(self.channels) >= self.max_channels: - print "*** timeline: refusing to schedule channel (hit limit of %d)" % self.max_channels - return - - # hmm - why do we need to copy this? - # c = channel(copy.deepcopy(dict)) - # c = Channel(copy.copy(dict)) - def addchan(): - #---------------------------------------------------------------------- - # this isn't exactly the best way to determine whether a device is - # an automator or event generator. should we have separate calls? - #---------------------------------------------------------------------- - if type(event) == dict and event.has_key("control") and False: - pass - else: - c = Channel(event, count) - c.timeline = self - c.device = device - self.channels.append(c) - - if quantize or delay: - if quantize: - schedtime = quantize * math.ceil(float(self.beats + delay) / quantize) - else: - schedtime = self.beats + delay - self.events.append({ 'time' : schedtime, 'fn' : addchan }) - else: - addchan() + CLOCK_INTERNAL = 0 + CLOCK_EXTERNAL = 1 + + def __init__(self, bpm=120, device=None, debug=None): + """expect to receive one tick per beat, generate events at 120bpm""" + self.ticklen = 1.0 / TICKS_PER_BEAT + self.beats = 0 + self.devices = [device] if device else [] + self.channels = [] + self.automators = [] + self.max_channels = 0 + + self.bpm = None + self.clock = None + self.clocksource = None + self.thread = None + + self.stop_when_done = False + + if debug is not None: + isobar.debug = debug + + self.events = [] + + if hasattr(bpm, "clocktarget"): + bpm.clocktarget = self + self.clocksource = bpm + self.clockmode = self.CLOCK_EXTERNAL + else: + self.bpm = bpm + ''' + # Create clock with a tick-size of 1/24th of a beat. + ''' + self.clock = Clock(60.0 / (self.bpm * TICKS_PER_BEAT)) + self.clockmode = self.CLOCK_INTERNAL + + def tick(self): + # if round(self.beats, 5) % 1 == 0: + # print "tick (%d active channels, %d pending events)" % + # (len(self.channels), len(self.events)) + ''' + # copy self.events because removing from it whilst using it = bad idea. + # perform events before channels are executed because an event might + # include scheduling a quantized channel, which should then be + # immediately evaluated. + ''' + for event in self.events[:]: + ''' + # the only event we currently get in a Timeline are add_channel events + # -- which have a raw function associated with them. + # + # round needed because we can sometimes end up with beats = 3.99999999... + # http://docs.python.org/tutorial/floatingpoint.html + ''' + if round(event["time"], 8) <= round(self.beats, 8): + event["fn"]() + self.events.remove(event) + + ''' + # some devices (ie, MidiFileOut) require being told to tick + ''' + for device in self.devices: + device.tick(self.ticklen) + + ''' + # copy self.channels because removing from it whilst using it = bad idea + ''' + for channel in self.channels[:]: + channel.tick(self.ticklen) + if channel.finished: + self.channels.remove(channel) + + if self.stop_when_done and len(self.channels) == 0: + raise StopIteration + + ''' + # TODO: should automator and channel inherit from a common superclass? + # one is continuous, one is discrete. + ''' + for automator in self.automators[:]: + automator.tick(self.ticklen) + if automator.finished: + self.automators.remove(automator) + + self.beats += self.ticklen + + def dump(self): + """ Output a summary of this Timeline object + """ + print "Timeline (clock: %s)" % ("external" if self.clockmode == self.CLOCK_EXTERNAL else "%sbpm" % self.bpm) + + print " - %d devices" % len(self.devices) + for device in self.devices: + print " - %s" % device + + print " - %d channels" % len(self.channels) + for channel in self.channels: + print" - %s" % channel + + def reset_to_beat(self): + self.beats = round(self.beats) + for channel in self.channels: + channel.reset_to_beat() + + def reset(self): + # print "tl reset!" + self.beats = -.0001 + # XXX probably shouldn't have to do this - should channels read the tl + # ticks value? + for channel in self.channels: + channel.reset() + + def background(self): + """ Run this Timeline in a background thread. """ + self.thread = thread.start_new_thread(self.run, ()) + + def run(self, high_priority=True): + """ Create clock with 64th-beat granularity. + By default, attempts to run as a high-priority thread + (though requires being run as root to re-nice the process) """ + try: + import os + os.nice(-20) + print "Timeline: Running as high-priority thread" + except: + pass + + try: + ''' + Start the clock. This might internal (eg a Clock object, running on + an independent thread), or external (eg a MIDI clock). + ''' + if self.clockmode == self.CLOCK_INTERNAL: + self.clock.run(self) + else: + self.clocksource.run() + + except StopIteration: + ''' + # This will be hit if every Pattern in a timeline is exhausted. + ''' + print "Timeline finished" + + except Exception as e: + print " *** Exception in background Timeline thread: %s" % e + traceback.print_exc(file=sys.stdout) + + def warp(self, warper): + self.clock.warp(warper) + + def unwarp(self, warper): + self.clock.warp(warper) + + def set_output(self, device): + self.devices.clear() + self.add_output(device) + + def add_output(self, device): + self.devices.append(device) + + def sched(self, event, quantize=0, delay=0, count=0, device=None): + if not device: + if not self.devices: + isobar.log("Adding default MIDI output") + self.add_output(isobar.io.MidiOut()) + device = self.devices[0] + if self.max_channels and len(self.channels) >= self.max_channels: + print "*** timeline: refusing to schedule channel (hit limit of %d)" % self.max_channels + return + + # hmm - why do we need to copy this? + # c = channel(copy.deepcopy(dict)) + # c = Channel(copy.copy(dict)) + def addchan(): + #------------------------------------------------------------------ + # this isn't exactly the best way to determine whether a device is + # an automator or event generator. should we have separate calls? + #------------------------------------------------------------------ + if isinstance(event, dict) and "control" in event and False: + pass + else: + c = Channel(event, count) + c.timeline = self + c.device = device + self.channels.append(c) + + if quantize or delay: + if quantize: + schedtime = quantize * \ + math.ceil(float(self.beats + delay) / quantize) + else: + schedtime = self.beats + delay + self.events.append({'time': schedtime, 'fn': addchan}) + else: + addchan() + class AutomatorChannel: - def __init__(self, dict = {}): - dict.setdefault('value', 0.5) - dict.setdefault('control', 0) - dict.setdefault('channel', 0) - for k, value in dict.iteritems(): - if not isinstance(value, Pattern): - dict[k] = PAConst(value) + def __init__(self, dict={}): + dict.setdefault('value', 0.5) + dict.setdefault('control', 0) + dict.setdefault('channel', 0) + + for k, value in dict.iteritems(): + if not isinstance(value, Pattern): + dict[k] = PAConst(value) - self.dick = dict + self.dick = dict + + def tick(self, ticklen): + pass - def tick(self, ticklen): - pass class Channel: - def __init__(self, events = {}, count = 0): - #---------------------------------------------------------------------- - # evaluate in case we have a pattern which gives us an event - # eg: PSeq([ { "note" : 20, "dur" : 0.5 }, { "note" : 50, "dur" : PWhite(0, 2) } ]) - # - # is this ever even necessary? - #---------------------------------------------------------------------- - # self.events = Pattern.pattern(events) - self.events = events - - self.next() - - self.timeline = None - self.pos = 0 - self.dur_now = 0 - self.phase_now = self.event["phase"].next() - self.next_note = 0 - - self.noteOffs = [] - self.finished = False - self.count_max = count - self.count_now = 0 - - def __str__(self): - return "Channel(pos = %d, note = %s, dur = %s, dur_now = %d, channel = %s, control = %s)[count = %d/%d])" % (self.pos, self.event["note"], self.event["dur"], self.dur_now, self.event["channel"], self.event["control"] if "control" in self.event else "-", self.count_now, self.count_max) - - def next(self): - #---------------------------------------------------------------------- - # event is a dictionary of patterns. anything which is not a pattern - # (eg, constant values) are turned into PConsts. - #---------------------------------------------------------------------- - event = Pattern.value(self.events) - - event.setdefault('note', 60) - event.setdefault('transpose', 0) - event.setdefault('dur', 1) - event.setdefault('amp', 64) - event.setdefault('channel', 0) - event.setdefault('omit', 0) - event.setdefault('gate', 1.0) - event.setdefault('phase', 0.0) - event.setdefault('octave', 0) - - if event.has_key('key'): - pass - elif event.has_key('scale'): - event['key'] = Key(0, event['scale']) - else: - event['key'] = Key(0, Scale.default) - - #---------------------------------------------------------------------- - # this does the job of turning constant values into (PConst) patterns. - #---------------------------------------------------------------------- - for key, value in event.items(): - event[key] = Pattern.pattern(value) - - self.event = event - - def tick(self, time): - #---------------------------------------------------------------------- - # process noteOffs before we play the next note, else a repeated note - # with gate = 1.0 will immediately be cancelled. - #---------------------------------------------------------------------- - self.processNoteOffs() - - try: - if round(self.pos, 8) >= round(self.next_note + self.phase_now, 8): - self.dur_now = self.event['dur'].next() - self.phase_now = self.event['phase'].next() - - self.play() - - self.next_note += self.dur_now - - self.next() - - self.count_now += 1 - if self.count_max and self.count_now >= self.count_max: - raise StopIteration - except StopIteration: - self.finished = True - - self.pos += time - - def reset_to_beat(self): - self.pos = round(self.pos) - - def reset(self): - self.pos = 0 - self.dur_now = 0 - self.next_note = 0 - - def play(self): - values = {} - for key, pattern in self.event.items(): - # TODO: HACK!! to prevent stepping through dur twice (see 'tick' above') - if key == "dur": - value = self.dur_now - elif key == "phase": - value = self.phase_now - else: - value = pattern.next() - values[key] = value - - #------------------------------------------------------------------------ - # print: Prints a value each time this event is triggered. - #------------------------------------------------------------------------ - if "print" in values: - print values["print"] - return - - #------------------------------------------------------------------------ - # action: Carry out an action each time this event is triggered - #------------------------------------------------------------------------ - if "action" in values: - try: - if "object" in values: - object = values["object"] - values["action"](object) - else: - values["action"]() - except Exception, e: - print "Exception when handling scheduled action: %s" % e - import traceback - traceback.print_exc() - pass - - return - - #------------------------------------------------------------------------ - # control: Send a control value - #------------------------------------------------------------------------ - if "control" in values: - value = values["value"] - channel = values["channel"] - isobar.log("control (channel %d, control %d, value %d)", values["channel"], values["control"], values["value"]) - self.device.control(values["control"], values["value"], values["channel"]) - return - - #------------------------------------------------------------------------ + + def __init__(self, events={}, count=0): + #---------------------------------------------------------------------- + # evaluate in case we have a pattern which gives us an event + # eg: PSeq([ { "note" : 20, "dur" : 0.5 }, { "note" : 50, "dur" : PWhite(0, 2) } ]) + # + # is this ever even necessary? + #---------------------------------------------------------------------- + # self.events = Pattern.pattern(events) + self.events = events + + self.next() + + self.timeline = None + self.pos = 0 + self.dur_now = 0 + self.phase_now = self.event["phase"].next() + self.next_note = 0 + + self.noteOffs = [] + self.finished = False + self.count_max = count + self.count_now = 0 + + def __str__(self): + return "Channel(pos = %d, note = %s, dur = %s, dur_now = %d, channel = %s, control = %s)[count = %d/%d])" % (self.pos, self.event["note"], self.event[ + "dur"], self.dur_now, self.event["channel"], self.event["control"] if "control" in self.event else "-", self.count_now, self.count_max) + + def next(self): + #---------------------------------------------------------------------- + # event is a dictionary of patterns. anything which is not a pattern + # (eg, constant values) are turned into PConsts. + #---------------------------------------------------------------------- + event = Pattern.value(self.events) + + event.setdefault('note', 60) + event.setdefault('transpose', 0) + event.setdefault('dur', 1) + event.setdefault('amp', 64) + event.setdefault('channel', 0) + event.setdefault('omit', 0) + event.setdefault('gate', 1.0) + event.setdefault('phase', 0.0) + event.setdefault('octave', 0) + + if 'key' in event: + pass + elif 'scale' in event: + event['key'] = Key(0, event['scale']) + else: + event['key'] = Key(0, Scale.default) + + #---------------------------------------------------------------------- + # this does the job of turning constant values into (PConst) patterns. + #---------------------------------------------------------------------- + for key, value in event.items(): + event[key] = Pattern.pattern(value) + + self.event = event + + def tick(self, time): + #---------------------------------------------------------------------- + # process noteOffs before we play the next note, else a repeated note + # with gate = 1.0 will immediately be cancelled. + #---------------------------------------------------------------------- + self.processNoteOffs() + + try: + if round(self.pos, 8) >= round(self.next_note + self.phase_now, 8): + self.dur_now = self.event['dur'].next() + self.phase_now = self.event['phase'].next() + + self.play() + + self.next_note += self.dur_now + + self.next() + + self.count_now += 1 + if self.count_max and self.count_now >= self.count_max: + raise StopIteration + except StopIteration: + self.finished = True + + self.pos += time + + def reset_to_beat(self): + self.pos = round(self.pos) + + def reset(self): + self.pos = 0 + self.dur_now = 0 + self.next_note = 0 + + def play(self): + values = {} + for key, pattern in self.event.items(): + # TODO: HACK!! to prevent stepping through dur twice (see 'tick' + # above') + if key == "dur": + value = self.dur_now + elif key == "phase": + value = self.phase_now + else: + value = pattern.next() + values[key] = value + + ''' + print: Prints a value each time this event is triggered. + ''' + if "print" in values: + print values["print"] + return + + ''' + action: Carry out an action each time this event is triggered + ''' + if "action" in values: + try: + if "object" in values: + object = values["object"] + values["action"](object) + else: + values["action"]() + except Exception as e: + print "Exception when handling scheduled action: %s" % e + import traceback + traceback.print_exc() + pass + + return + + ''' + control: Send a control value + ''' + if "control" in values: + value = values["value"] + channel = values["channel"] + isobar.log( + "control (channel %d, control %d, value %d)", + values["channel"], + values["control"], + values["value"]) + self.device.control( + values["control"], + values["value"], + values["channel"]) + return + + ''' # address: Send a value to an OSC endpoint - #------------------------------------------------------------------------ - if "address" in values: - self.device.send(values["address"], values["params"]) - return - - #------------------------------------------------------------------------ - # note/degree/etc: Send a MIDI note - #------------------------------------------------------------------------ - note = None - - if "degree" in values: - degree = values["degree"] - key = values["key"] - octave = values["octave"] - if not degree is None: - #---------------------------------------------------------------------- - # handle lists of notes (eg chords). - # TODO: create a class which allows for scalars and arrays to handle - # addition transparently - #---------------------------------------------------------------------- - try: - values["note"] = [ key[n] + (octave * 12) for n in degree ] - except: - values["note"] = key[degree] + (octave * 12) - - #---------------------------------------------------------------------- - # For cases in which we want to introduce a rest, simply set our 'amp' - # value to zero. This means that we can still send rest events to - # devices which receive all generic events (useful to display rests - # when rendering a score). - #---------------------------------------------------------------------- - if random.uniform(0, 1) < values['omit']: - values["note"] = None - - if values["note"] is None: - # print "(rest)" - # return - values["note"] = 0 - values["amp"] = 0 - else: - #---------------------------------------------------------------------- - # handle lists of notes (eg chords). - # TODO: create a class which allows for scalars and arrays to handle - # addition transparently. - # - # the below does not allow for values["transpose"] to be an array, - # for example. - #---------------------------------------------------------------------- - try: - values["note"] = [ note + values["transpose"] for note in values["note"] ] - except: - values["note"] += values["transpose"] - - - #---------------------------------------------------------------------- - # event: Certain devices (eg Socket IO) handle generic events, - # rather than noteOn/noteOff. (Should probably have to - # register for this behaviour rather than happening magically...) - #---------------------------------------------------------------------- - if hasattr(self.device, "event") and callable(getattr(self.device, "event")): - d = copy.copy(values) - for key, value in d.items(): - #------------------------------------------------------------------------ - # turn non-builtin objects into their string representations. - # we don't want to call repr() on numbers as it turns them into strings, - # which we don't want to happen in our resultant JSON. - # TODO: there absolutely must be a way to do this for all objects which are - # non-builtins... ie, who are "class" instances rather than "type". - # - # we could check dir(__builtins__), but for some reason, __builtins__ is - # different here than it is outside of a module!? - # - # instead, go with the lame option of listing "primitive" types. - #------------------------------------------------------------------------ - if type(value) not in (int, float, bool, str, list, dict, tuple): - name = type(value).__name__ - value = repr(value) - d[key] = value - - self.device.event(d) - return - - #---------------------------------------------------------------------- - # noteOn: Standard (MIDI) type of device - #---------------------------------------------------------------------- - if values["amp"] > 0: - # TODO: pythonic duck-typing approach might be better - # TODO: doesn't handle arrays of amp, channel values, etc - notes = values["note"] if hasattr(values["note"], '__iter__') else [ values["note"] ] - - #---------------------------------------------------------------------- - # Allow for arrays of amp, gate etc, to handle chords properly. - # Caveat: Things will go horribly wrong for an array of amp/gate values - # shorter than the number of notes. - #---------------------------------------------------------------------- - for index, note in enumerate(notes): - amp = values["amp"][index] if isinstance(values["amp"], list) else values["amp"] - channel = values["channel"][index] if isinstance(values["channel"], list) else values["channel"] - gate = values["gate"][index] if isinstance(values["gate"], list) else values["gate"] - - isobar.log("note on (channel %d, note %d, velocity %d)", channel, note, amp); - self.device.noteOn(note, amp, channel) - - note_dur = self.dur_now * gate - self.schedNoteOff(self.next_note + note_dur + self.phase_now, note, channel) - - def schedNoteOff(self, time, note, channel): - self.noteOffs.append([ time, note, channel ]) - - def processNoteOffs(self): - for n, note in enumerate(self.noteOffs): - # TODO: create a Note object to represent these noteOff events - if note[0] <= self.pos: - index = note[1] - channel = note[2] - isobar.log("note off (channel %d, note %d)", channel, index); - self.device.noteOff(index, channel) - self.noteOffs.pop(n) + ''' + if "address" in values: + self.device.send(values["address"], values["params"]) + return + + ''' + # note/degree/etc: Send a MIDI note + ''' + note = None + + if "degree" in values: + degree = values["degree"] + key = values["key"] + octave = values["octave"] + if degree is not None: + ''' + handle lists of notes (eg chords). + TODO: create a class which allows for scalars and arrays to + handle addition transparently + ''' + try: + values["note"] = [key[n] + (octave * 12) for n in degree] + except: + values["note"] = key[degree] + (octave * 12) + + #---------------------------------------------------------------------- + # For cases in which we want to introduce a rest, simply set our 'amp' + # value to zero. This means that we can still send rest events to + # devices which receive all generic events (useful to display rests + # when rendering a score). + #---------------------------------------------------------------------- + if random.uniform(0, 1) < values['omit']: + values["note"] = None + + if values["note"] is None: + # print "(rest)" + # return + values["note"] = 0 + values["amp"] = 0 + else: + #------------------------------------------------------------------ + # handle lists of notes (eg chords). + # TODO: create a class which allows for scalars and arrays to handle + # addition transparently. + # + # the below does not allow for values["transpose"] to be an array, + # for example. + #------------------------------------------------------------------ + try: + values["note"] = [note + values["transpose"] + for note in values["note"]] + except: + values["note"] += values["transpose"] + + #---------------------------------------------------------------------- + # event: Certain devices (eg Socket IO) handle generic events, + # rather than noteOn/noteOff. (Should probably have to + # register for this behaviour rather than happening magically...) + #---------------------------------------------------------------------- + if hasattr( + self.device, + "event") and callable( + getattr( + self.device, + "event")): + d = copy.copy(values) + for key, value in d.items(): + ''' + # turn non-builtin objects into their string representations. + # we don't want to call repr() on numbers as it turns them into strings, + # which we don't want to happen in our resultant JSON. + # TODO: there absolutely must be a way to do this for all objects which are + # non-builtins... ie, who are "class" instances rather than "type". + # + # we could check dir(__builtins__), but for some reason, __builtins__ is + # different here than it is outside of a module!? + # + # instead, go with the lame option of listing "primitive" types. + ''' + if type(value) not in ( + int, + float, + bool, + str, + list, + dict, + tuple): + name = type(value).__name__ + value = repr(value) + d[key] = value + + self.device.event(d) + return + + #---------------------------------------------------------------------- + # noteOn: Standard (MIDI) type of device + #---------------------------------------------------------------------- + if values["amp"] > 0: + # TODO: pythonic duck-typing approach might be better + # TODO: doesn't handle arrays of amp, channel values, etc + notes = values["note"] if hasattr( + values["note"], + '__iter__') else [ + values["note"]] + + #------------------------------------------------------------------ + # Allow for arrays of amp, gate etc, to handle chords properly. + # Caveat: Things will go horribly wrong for an array of amp/gate values + # shorter than the number of notes. + #------------------------------------------------------------------ + for index, note in enumerate(notes): + amp = values["amp"][index] if isinstance( + values["amp"], + list) else values["amp"] + channel = values["channel"][index] if isinstance( + values["channel"], + list) else values["channel"] + gate = values["gate"][index] if isinstance( + values["gate"], + list) else values["gate"] + + isobar.log( + "note on (channel %d, note %d, velocity %d)", + channel, + note, + amp) + self.device.noteOn(note, amp, channel) + + note_dur = self.dur_now * gate + self.schedNoteOff( + self.next_note + + note_dur + + self.phase_now, + note, + channel) + + def schedNoteOff(self, time, note, channel): + self.noteOffs.append([time, note, channel]) + + def processNoteOffs(self): + for n, note in enumerate(self.noteOffs): + # TODO: create a Note object to represent these noteOff events + if note[0] <= self.pos: + index = note[1] + channel = note[2] + isobar.log("note off (channel %d, note %d)", channel, index) + self.device.noteOff(index, channel) + self.noteOffs.pop(n) #---------------------------------------------------------------------- # a clock is relied upon to generate accurate tick() events every @@ -492,44 +537,45 @@ def processNoteOffs(self): # as per MIDI #---------------------------------------------------------------------- + class Clock: - def __init__(self, ticksize = 1.0/24): - self.ticksize_orig = ticksize - self.ticksize = ticksize - self.warpers = [] - self.accelerate = 1.0 - - def run(self, timeline): - clock0 = clock1 = time.time() * self.accelerate - try: - #------------------------------------------------------------------------ - # allow a tick to elapse before we call tick() for the first time - # to keep Warp patterns in sync - #------------------------------------------------------------------------ - while True: - if clock1 - clock0 >= self.ticksize: - # time for a tick - timeline.tick() - clock0 += self.ticksize - self.ticksize = self.ticksize_orig - for warper in self.warpers: - warp = warper.next() - #------------------------------------------------------------------------ - # map [-1..1] to [0.5, 2] - # - so -1 doubles our tempo, +1 halves it - #------------------------------------------------------------------------ - warp = pow(2, warp) - self.ticksize *= warp - - time.sleep(0.002) - clock1 = time.time() * self.accelerate - except KeyboardInterrupt: - print "interrupt caught, exiting" - return - - def warp(self, warper): - self.warpers.append(warper) - - def unwarp(self, warper): - self.warpers.remove(warper) + def __init__(self, ticksize=1.0 / 24): + self.ticksize_orig = ticksize + self.ticksize = ticksize + self.warpers = [] + self.accelerate = 1.0 + + def run(self, timeline): + clock0 = clock1 = time.time() * self.accelerate + try: + ''' + # allow a tick to elapse before we call tick() for the first time + # to keep Warp patterns in sync + ''' + while True: + if clock1 - clock0 >= self.ticksize: + # time for a tick + timeline.tick() + clock0 += self.ticksize + self.ticksize = self.ticksize_orig + for warper in self.warpers: + warp = warper.next() + ''' + # map [-1..1] to [0.5, 2] + # - so -1 doubles our tempo, +1 halves it + ''' + warp = pow(2, warp) + self.ticksize *= warp + + time.sleep(0.002) + clock1 = time.time() * self.accelerate + except KeyboardInterrupt: + print "interrupt caught, exiting" + return + + def warp(self, warper): + self.warpers.append(warper) + + def unwarp(self, warper): + self.warpers.remove(warper) diff --git a/isobar/util.py b/isobar/util.py index 2a7998e..6d412f7 100644 --- a/isobar/util.py +++ b/isobar/util.py @@ -4,105 +4,119 @@ import sys note_names = [ - [ "C" ], - [ "C#", "Db" ], - [ "D" ], - [ "D#", "Eb" ], - [ "E" ], - [ "F" ], - [ "F#", "Gb" ], - [ "G" ], - [ "G#", "Ab" ], - [ "A" ], - [ "A#", "Bb" ], - [ "B" ] + ["C"], + ["C#", "Db"], + ["D"], + ["D#", "Eb"], + ["E"], + ["F"], + ["F#", "Gb"], + ["G"], + ["G#", "Ab"], + ["A"], + ["A#", "Bb"], + ["B"] ] isobar.debug = False + def log(message, *args): - if isobar.debug: - message = message % args - sys.stderr.write("[isobar] %s\n" % message) + if isobar.debug: + message = message % args + sys.stderr.write("[isobar] %s\n" % message) + def normalize(array): - """ Normalise an array to sum to 1.0. """ - if sum(array) == 0: - return array - return map(lambda n: float(n) / sum(array), array) + """ Normalise an array to sum to 1.0. """ + if sum(array) == 0: + return array + return map(lambda n: float(n) / sum(array), array) + def windex(weights): - """ Return a random index based on a list of weights, from 0..(len(weights) - 1). - Assumes that weights is already normalised. """ - n = random.uniform(0, 1) - for i in range(len(weights)): - if n < weights[i]: - return i - n = n - weights[i] + """ Return a random index based on a list of weights, from 0..(len(weights) - 1). + Assumes that weights is already normalised. """ + n = random.uniform(0, 1) + for i in range(len(weights)): + if n < weights[i]: + return i + n = n - weights[i] + def wnindex(weights): - """ Returns a random index based on a list of weights. - Normalises list of weights before executing. """ - wnorm = normalize(weights) - return windex(wnorm) + """ Returns a random index based on a list of weights. + Normalises list of weights before executing. """ + wnorm = normalize(weights) + return windex(wnorm) + def wchoice(array, weights): - """ Performs a weighted choice from a list of values (assumes pre-normalised weights) """ - index = windex(weights) - return array[index] + """ Performs a weighted choice from a list of values (assumes pre-normalised weights) """ + index = windex(weights) + return array[index] + def wnchoice(array, weights): - """ Performs a weighted choice from a list of values - (does not assume pre-normalised weights). """ - index = wnindex(weights) - return array[index] + """ Performs a weighted choice from a list of values + (does not assume pre-normalised weights). """ + index = wnindex(weights) + return array[index] + def nametomidi(name): - """ Maps a MIDI note name (D3, C#6) to a value. - Assumes that middle C is C4. """ - if name[-1].isdigit(): - octave = int(name[-1]) - name = name[:-1] - else: - octave = 0 - - try: - index = note_names.index(filter(lambda nameset: name in nameset, note_names)[0]) - return octave * 12 + index - except: - return None + """ Maps a MIDI note name (D3, C#6) to a value. + Assumes that middle C is C4. """ + if name[-1].isdigit(): + octave = int(name[-1]) + name = name[:-1] + else: + octave = 0 + + try: + index = note_names.index( + filter( + lambda nameset: name in nameset, + note_names)[0]) + return octave * 12 + index + except: + return None + def miditopitch(note): - """ Maps a MIDI note index to a note name (independent of octave) - miditopitch(0) -> "C" - miditopitch(1) -> "C#" """ - degree = int(note) % len(note_names) - return note_names[degree][0] + """ Maps a MIDI note index to a note name (independent of octave) + miditopitch(0) -> "C" + miditopitch(1) -> "C#" """ + degree = int(note) % len(note_names) + return note_names[degree][0] + def miditoname(note): - """ Maps a MIDI note index to a note name. """ - degree = int(note) % len(note_names) - octave = int(note / len(note_names)) - 1 - str = "%s%d" % (note_names[degree][0], octave) - frac = math.modf(note)[0] - if frac > 0: - str = (str + " + %2f" % frac) + """ Maps a MIDI note index to a note name. """ + degree = int(note) % len(note_names) + octave = int(note / len(note_names)) - 1 + str = "%s%d" % (note_names[degree][0], octave) + frac = math.modf(note)[0] + if frac > 0: + str = (str + " + %2f" % frac) - return str + return str -def bipolar_diverge(maximum): - """ returns [0, 1, -1, ...., maximum, -maximum ] """ - sequence = list(sum(zip(range(maximum + 1), range(0, -maximum - 1, -1)), ())) - sequence.pop(0) - return sequence - -def filter_tone_row(source, target, bend_limit = 7): - """ filters the notes in by the permitted notes in . - returns a tuple ( acceptable, pitch_bend) """ - bends = bipolar_diverge(bend_limit) - for bend in bends: - if all(((note + bend) % 12) in target for note in source): - return (True, bend) - return (False, 0) +def bipolar_diverge(maximum): + """ returns [0, 1, -1, ...., maximum, -maximum ] """ + sequence = list( + sum(zip(range(maximum + 1), range(0, -maximum - 1, -1)), ())) + sequence.pop(0) + return sequence + + +def filter_tone_row(source, target, bend_limit=7): + """ filters the notes in by the permitted notes in . + returns a tuple ( acceptable, pitch_bend) """ + bends = bipolar_diverge(bend_limit) + for bend in bends: + if all(((note + bend) % 12) in target for note in source): + return (True, bend) + return (False, 0)