diff --git a/.gitignore b/.gitignore index 2ac6cc1..8ffee6a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ recordings dev refs NOTES.md -*backup.zip \ No newline at end of file +*backup.zip +.idea diff --git a/braid/notation.py b/braid/notation.py index fd726db..90b748e 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -1,12 +1,18 @@ -from random import randint, choice, random, shuffle +from random import randint, choice, random, shuffle, uniform +from collections import deque +from bisect import bisect_left +from .signal import amp_bias, calc_pos -class Scale(list): - - """Allows for specifying scales by degree, up to one octave below and two octaves above""" - """Any number of scale steps is supported, but for MAJ: """ - """ -1, -2, -3, -4, -5, -6, -7, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14""" - def __init__(self, *args): +class Scale(list): + """Allows for specifying scales by degree, up to 1 octave below and octaves_above above (default 2)""" + """Set constrain=True to octave shift out-of-range degrees into range, preserving pitch class, else ScaleError""" + """Any number of scale steps is supported, but default for MAJ: """ + """ -1, -2, -3, -4, -5, -6, -7, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14""" + + def __init__(self, *args, constrain=False, octaves_above=2): + self.constrain = constrain + self.octaves_above = octaves_above super(Scale, self).__init__(*args) def __getitem__(self, degree): @@ -19,8 +25,14 @@ def __getitem__(self, degree): octave_shift = 0 if degree == R: degree = list.__getitem__(self, randint(0, len(self) - 1)) - if degree > len(self) * 2 or degree < 0 - len(self): - raise ScaleError(degree) + if self.constrain: + while degree > self._upper_bound(): + degree = degree - len(self) + while degree < 0 - len(self): + degree = degree + len(self) + else: + if degree > self._upper_bound() or degree < 0 - len(self): + raise ScaleError(degree) if degree < 0: degree = abs(degree) octave_shift -= 12 @@ -33,6 +45,9 @@ def __getitem__(self, degree): return float(semitone) return semitone + def _upper_bound(self): + return len(self) * self.octaves_above + def rotate(self, steps): l = list(self) scale = l[steps:] + l[:steps] @@ -40,6 +55,16 @@ def rotate(self, steps): scale = [(degree + 12) if degree < 0 else degree for degree in scale] return Scale(scale) + def quantize(self, interval): + """Quantize a semitone interval to the scale, negative and positive intervals are accepted without bounds""" + """Intervals not in the scale are shifted up in pitch to the nearest interval in the scale""" + """i.e. for MAJ, 1 returns 2, 3 returns 4, -2 returns -1, -4 returns -3, etc...""" + if interval in self: + return interval + octave_shift = (interval // 12) * 12 + interval = interval - octave_shift + degree = bisect_left(list(self), interval) + 1 + return self[degree] + octave_shift class ScaleError(Exception): @@ -185,45 +210,94 @@ def __str__(self): MYX = DOM = ION.rotate(4) -AOL = ION.rotate(5) +AOL = NMI = ION.rotate(5) LOC = ION.rotate(6) -MIN = Scale([0, 2, 3, 5, 7, 8, 11]) +MIN = HMI = Scale([0, 2, 3, 5, 7, 8, 11]) # Harmonic Minor + +MMI = Scale([0, 2, 3, 5, 7, 9, 11]) # Melodic Minor -MMI = Scale([0, 2, 4, 5, 6, 9, 11]) +BLU = BMI = Scale([0, 3, 5, 6, 7, 10]) # Blues Minor -BLU = Scale([0, 3, 5, 6, 7, 10]) +BMA = Scale([0, 3, 4, 7, 9, 10]) # Blues major (From midipal/BitT source code) PEN = Scale([0, 2, 5, 7, 10]) +PMA = Scale([0, 2, 4, 7, 9]) # Pentatonic major (From midipal/BitT source code) + +PMI = Scale([0, 3, 5, 7, 10]) # Pentatonic minor (From midipal/BitT source code) + +# world + +FLK = Scale([0, 1, 3, 4, 5, 7, 8, 10]) # Folk (From midipal/BitT source code) + +JPN = Scale([0, 1, 5, 7, 8]) # Japanese (From midipal/BitT source code) + +GYP = Scale([0, 2, 3, 6, 7, 8, 11]) # Gypsy (From MI Braids source code) + +ARB = Scale([0, 1, 4, 5, 7, 8, 11]) # Arabian (From MI Braids source code) + +FLM = Scale([0, 1, 4, 5, 7, 8, 10]) # Flamenco (From MI Braids source code) + +# other + +WHL = Scale([0, 2, 4, 6, 8, 10]) # Whole tone (From midipal/BitT source code) + # chromatic CHR = Scale([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) - # gamelan +GML = Scale([0, 1, 3, 7, 8]) # Gamelan (From midipal/BitT source code) + SDR = Scale([0, 2, 5, 7, 9]) PLG = Scale([0, 1, 3, 6, 7, 8, 10]) - # personal JAM = Scale([0, 2, 3, 5, 6, 7, 10, 11]) - # stepwise drums DRM = Scale([0, 2, 7, 14, 6, 10, 3, 39, 31, 13]) # -R = 'R' # random -Z = 'REST' # rest +R = 'R' # random +Z = 'REST' # rest def g(note): # create grace note from named step return float(note) + + +def v(note, v_scale=None): + # create a note with scaled velocity from named (or unnamed) step + # v_scale can be a single float value (the part before the decimal is ignored) + # or it can be a tuple of (lo, hi) specifying a range for random scaling, e.g. v(C4, (.2, .9)) + # else randomly generate v_scale between 0.17 and 0.999 + if type(v_scale) == float: + v_scale = v_scale % 1 # ignore any part before the decimal + elif type(v_scale) == tuple and len(v_scale): + hi = 0.999 if len(v_scale) < 2 else v_scale[1] % 1 # allow specifying only lo, e.g. v(C, (.5,)) + v_scale = uniform(v_scale[0] % 1, hi) + else: + v_scale = uniform(0.17, 0.999) + return int(note) + v_scale + + +def s(signal, a=1, r=1, p=0, b=0, v_scale=None): + # Use signals to dynamically generate note pitches and velocities with base phase derived from the thread + def f(t): + pos = calc_pos(t._base_phase, r, p) + n = signal(pos) + if v_scale is not None: + vs = v_scale(pos) if callable(v_scale) else v_scale + n = v(n, v_scale=vs) + return amp_bias(n, a, b, pos) + + return f diff --git a/braid/pattern.py b/braid/pattern.py index 7631052..9f1f6f0 100644 --- a/braid/pattern.py +++ b/braid/pattern.py @@ -3,16 +3,25 @@ from . import num_args from .signal import ease_in, ease_out - +class Q(collections.deque): + """Q is a wrapper around collections.deque for making rotating note 'queues' + Set drunk=True to randomize the rotation direction, else always rotate right. + e.g. Q([1, 2, 3]) returns 1 on the first cycle, then 2 on the next, then 3, then 1, etc... + """ + def __init__(self, iterable, drunk=False): + self.drunk = drunk + super(Q, self).__init__(iterable) class Pattern(list): """ Pattern is just a list (of whatever) that can be specified in compacted form - ... with the addition of the Markov expansion of tuples on calling resolve + ... with the addition of the Markov expansion of tuples and rotating Qs on calling resolve ... and some blending functions + Set drunk = True to treat all Qs as if they are drunk = True, else defer to each Q's drunk property """ - def __init__(self, value=[0]): + def __init__(self, value=[0], drunk=False): + self.drunk = drunk list.__init__(self, value) def resolve(self): @@ -23,8 +32,17 @@ def _subresolve(self, pattern): """Resolve a subbranch of the pattern""" steps = [] for step in pattern: - while type(step) == tuple: - step = choice(step) + while type(step) == tuple or type(step) == Q: + if type(step) == tuple: + step = choice(step) + else: + coin = choice([0, 1]) + if coin and (self.drunk or step.drunk): + step.rotate(1) + else: + step.rotate(-1) + step = step[-1] + if type(step) == list: step = self._subresolve(step) steps.append(step) @@ -57,11 +75,18 @@ def _get_divs(self, pattern): def __repr__(self): return "P%s" % list.__repr__(self) + def notes(self): + """return a list of all values in pattern that are not 0 or REST""" + return [n for n in self if n != 0 and n != 'REST'] + def replace(self, value, target): list.__init__(self, [target if step == value else value for step in self]) def rotate(self, steps=1): - list.__init__(self, self[steps:] + self[:steps]) + # steps > 0 = right rotation, steps < 0 = left rotation + if steps: + steps = -(steps % len(self)) + list.__init__(self, self[steps:] + self[:steps]) def blend(self, pattern_2, balance=0.5): l = blend(self, pattern_2, balance) @@ -75,6 +100,22 @@ def xor(self, pattern_2): l = xor(self, pattern_2) list.__init__(self, l) + def invert(self, off_note=0, note_list=None): + """replace all occurrences of off_note with consecutive values from note_list (default = self.notes())""" + """and replace all occurrences of NOT off_note with off_note""" + """e.g. [1, 0, 2, 0, 3] becomes [0, 1, 0, 2, 0]""" + if note_list is None: + note_list = self.notes() + inverted_pattern = [] + i = 0 + for n in self: + if n == off_note: + inverted_pattern.append(note_list[i % len(note_list)]) + i += 1 + else: + inverted_pattern.append(off_note) + list.__init__(self, inverted_pattern) + def prep(pattern_1, pattern_2): if type(pattern_1) is not Pattern: @@ -145,7 +186,7 @@ def lcm(a, b): return a * b // gcd -def euc(steps, pulses, note=1): +def euc(steps, pulses, rotation=0, invert=False, note=1, off_note=0, note_list=None, off_note_list=None): steps = int(steps) pulses = int(pulses) if pulses > steps: @@ -168,7 +209,7 @@ def euc(steps, pulses, note=1): def build(level): if level == -1: - pattern.append(0) + pattern.append(off_note) elif level == -2: pattern.append(note) else: @@ -176,8 +217,29 @@ def build(level): build(level - 1) if remainders[level] != 0: build(level - 2) - + build(level) i = pattern.index(note) pattern = pattern[i:] + pattern[0:i] - return pattern + + if rotation: + pattern = Pattern(pattern) + pattern.rotate(rotation) + pattern = list(pattern) + if note_list is None: + note_list = [note] + if off_note_list is None: + off_note_list = [off_note] + pulse = off_note if invert else note + final_pattern = [] + i = j = 0 + for n in pattern: + if n == pulse: + final_pattern.append(note_list[i % len(note_list)]) + i += 1 + else: + final_pattern.append(off_note_list[j % len(off_note_list)]) + j += 1 + + return final_pattern + diff --git a/braid/signal.py b/braid/signal.py index 951b860..1208a9b 100644 --- a/braid/signal.py +++ b/braid/signal.py @@ -1,5 +1,25 @@ import time, math, __main__ -from random import random +from random import random, triangular, uniform + + +def calc_pos(pos, rate, phase): + pos = clamp(pos) + pos = pos * rate(pos) if callable(rate) else pos * rate + return (pos + phase(pos)) % 1.0 if callable(phase) else (pos + phase) % 1.0 + + +def amp_bias(value, amp, bias, pos=None): + p = value if pos is None else pos + amp = amp(p) if callable(amp) else amp + bias = bias(p) if callable(bias) else bias + return value * amp + bias + + +def pos2rad(pos): + pos = clamp(pos) + degrees = pos * 360 + return math.radians(degrees) + def clamp(pos): if pos > 1.0: @@ -9,58 +29,143 @@ def clamp(pos): else: return pos -def linear(): + +def const(value, mod=False, a=1, r=1, p=0, b=0): def f(pos): - pos = clamp(pos) - return pos + if mod: + pos = calc_pos(pos, r, p) + v = value(pos) if callable(value) else value + return amp_bias(v, a, b, pos) + else: + return value + + return f + + +def noise(l=0, h=1, a=1, r=1, p=0, b=0, m=None): + def f(pos): + pos = calc_pos(pos, r, p) + lo = l(pos) if callable(l) else l + hi = h(pos) if callable(h) else h + if m is not None: + mode = m(pos) if callable(m) else m + return amp_bias(triangular(lo, hi, mode), a, b, pos) + else: + return amp_bias(uniform(lo, hi), a, b, pos) + + return f + + +def linear(a=1, r=1, p=0, b=0): + def f(pos): + pos = calc_pos(pos, r, p) + return amp_bias(pos, a, b) + + return f + + +def inverse_linear(a=1, r=1, p=0, b=0): + def f(pos): + pos = calc_pos(1 - pos, r, p) + return amp_bias(pos, a, b) + + return f + + +def triangle(s=.5, a=1, r=1, p=0, b=0): + def f(pos): + pos = calc_pos(pos, r, p) + sym = s(pos) if callable(s) else s + if pos < sym: + value = pos * 1 / sym + else: + value = 1 - ((pos - sym) * (1 / (1 - sym))) + return amp_bias(value, a, b, pos) + return f -def ease_in(exp=2): + +def pulse(w=.5, a=1, r=1, p=0, b=0): def f(pos): - pos = clamp(pos) - return pos**exp + pos = calc_pos(pos, r, p) + width = w(pos) if callable(w) else w + if pos < width: + return amp_bias(0.0, a, b, pos) + else: + return amp_bias(1.0, a, b, pos) + return f - -def ease_out(exp=3): + + +def ease_in(exp=2, a=1, r=1, p=0, b=0): def f(pos): - pos = clamp(pos) - return (pos - 1)**exp + 1 + pos = calc_pos(pos, r, p) + e = exp(pos) if callable(exp) else exp + return amp_bias(pos ** e, a, b, pos) + return f -def ease_in_out(exp=3): - def f (pos): - pos = clamp(pos) - pos *= 2 - if pos < 1: - return 0.5 * pos**exp - pos -= 2 - return 0.5 * (pos**exp + 2) + +def ease_out(exp=3, a=1, r=1, p=0, b=0): + def f(pos): + pos = calc_pos(pos, r, p) + e = exp(pos) if callable(exp) else exp + return amp_bias(((pos - 1) ** e + 1), a, b, pos) + return f - -def ease_out_in(exp=3): + + +def ease_in_out(exp=3, a=1, r=1, p=0, b=0): + def f(pos): + pos = calc_pos(pos, r, p) + value = pos * 2 + e = exp(pos) if callable(exp) else exp + if value < 1: + return amp_bias(0.5 * value ** e, a, b, pos) + value -= 2 + return amp_bias(0.5 * (value ** e + 2), a, b, pos) + + return f + + +def ease_out_in(exp=3, a=1, r=1, p=0, b=0): def f(pos): - pos = clamp(pos) - pos *= 2 - pos = pos - 1 - if pos < 2: - return 0.5 * pos**exp + 0.5 + pos = calc_pos(pos, r, p) + value = pos * 2 - 1 + e = exp(pos) if callable(exp) else exp + if value < 2: + return amp_bias(0.5 * value ** e + 0.5, a, b, pos) else: - return 1.0 - (0.5 * pos**exp + 0.5) + return amp_bias(1.0 - (0.5 * value ** e + 0.5), a, b, pos) + return f + +def sine(a=1, r=1, p=0, b=0): + def f(pos): + pos = calc_pos(pos, r, p) + return amp_bias(math.sin(pos2rad(pos)) * 0.5 + 0.5, a, b, pos) + + return f + + def normalize(signal): min_value = min(signal) max_value = max(signal) - return [(v - min_value) / (max_value - min_value) for v in signal] + return [(v - min_value) / (max_value - min_value) for v in signal] + def timeseries(timeseries): timeseries = normalize(timeseries) + def f(pos): indexf = pos * (len(timeseries) - 1) pos = indexf % 1.0 value = (timeseries[math.floor(indexf)] * (1.0 - pos)) + (timeseries[math.ceil(indexf)] * pos) return value - return f + + return f + def breakpoints(*breakpoints): """ eg: @@ -77,9 +182,11 @@ def breakpoints(*breakpoints): domain = max(breakpoints, key=lambda bp: bp[0])[0] - min_x min_y = min(breakpoints, key=lambda bp: bp[1])[1] resolution = max(breakpoints, key=lambda bp: bp[1])[1] - min_y - breakpoints = [[(bp[0] - min_x) / float(domain), (bp[1] - min_y) / float(resolution), None if not len(bp) == 3 else bp[2]] for bp in breakpoints] + breakpoints = [ + [(bp[0] - min_x) / float(domain), (bp[1] - min_y) / float(resolution), None if not len(bp) == 3 else bp[2]] for + bp in breakpoints] - def f(pos): + def f(pos): index = 0 while index < len(breakpoints) and breakpoints[index][0] < pos: index += 1 @@ -91,23 +198,23 @@ def f(pos): if end_point[2] is None: return start_point[1] pos = (pos - start_point[0]) / (end_point[0] - start_point[0]) - if end_point[2] is not linear: + if end_point[2] is not linear: pos = end_point[2](pos) return start_point[1] + (pos * (end_point[1] - start_point[1])) return f + def cross(division, degree): bps = [[0, 0]] for d in range(division): d += 1 - bps.append([d, d/division, ease_in(degree)]) + bps.append([d, d / division, ease_in(degree)]) f = breakpoints(*bps) return f class Plotter(): - instance = None def __init__(self): @@ -115,15 +222,18 @@ def __init__(self): self.master = tkinter.Tk() self.width, self.height = 1000, 250 self.margin = 20 - self.w = tkinter.Canvas(self.master, width=self.width + self.margin*2, height=self.height + self.margin*2) + self.w = tkinter.Canvas(self.master, width=self.width + self.margin * 2, height=self.height + self.margin * 2) self.w.pack() - self.w.create_rectangle(self.margin - 1, self.margin - 1, self.width + self.margin + 1, self.height + self.margin + 1) + self.w.create_rectangle(self.margin - 1, self.margin - 1, self.width + self.margin + 1, + self.height + self.margin + 1) @classmethod def plot(cls, bp_f, color="red"): if not hasattr(__main__, "__file__") or cls.instance is None: cls.instance = Plotter() - points = [(i + cls.instance.margin, ((1.0 - bp_f(float(i) / cls.instance.width)) * cls.instance.height) + cls.instance.margin) for i in range(int(cls.instance.width))] + points = [(i + cls.instance.margin, + ((1.0 - bp_f(float(i) / cls.instance.width)) * cls.instance.height) + cls.instance.margin) for i in + range(int(cls.instance.width))] cls.instance.w.create_line(points, fill=color, width=2.0) @classmethod @@ -131,8 +241,10 @@ def show_plots(cls): if cls.instance is not None: cls.instance.master.update() + def plot(signal, color="red"): Plotter.plot(signal, color) + def show_plots(): Plotter.show_plots() diff --git a/braid/thread.py b/braid/thread.py index 2536f66..e4cef92 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -5,8 +5,8 @@ from .notation import * from .tween import * -class Thread(object): +class Thread(object): """Class definitions""" threads = driver.threads @@ -14,10 +14,12 @@ class Thread(object): @classmethod def add_attr(cls, name, default=0): """Add a property with tweening capability (won't send MIDI)""" + def getter(self): if isinstance(getattr(self, "_%s" % name), Tween): return getattr(self, "_%s" % name).value return getattr(self, "_%s" % name) + def setter(self, value): if isinstance(value, Tween): value.start(self, getattr(self, name)) @@ -26,12 +28,15 @@ def setter(self, value): if value is False: value = 0 setattr(self, "_%s" % name, value) + setattr(cls, "_%s" % name, default) setattr(cls, name, property(getter, setter)) @classmethod def setup(cls): # standard properties + Thread.add_attr('transpose', 0) + Thread.add_attr('transpose_step_len', 1) Thread.add_attr('chord', None) Thread.add_attr('velocity', 1.0) Thread.add_attr('grace', 0.75) @@ -39,7 +44,6 @@ def setup(cls): Thread.add_attr('micro', None) Thread.add_attr('controls', None) - """Instance definitions""" def __setattr__(self, key, value): @@ -68,8 +72,10 @@ def __init__(self, channel, sync=True): self._channel = channel self._running = False self._cycles = 0.0 + self._base_phase = 0.0 self._last_edge = 0 self._index = -1 + self._transpose_index = -1 self._steps = [0] self._previous_pitch = 60 self._previous_step = 1 @@ -91,7 +97,6 @@ def __init__(self, channel, sync=True): if not LIVECODING: self.start() - def update(self, delta_t): """Run each tick and update the state of the Thread""" if not self._running: @@ -102,22 +107,25 @@ def update(self, delta_t): pc = self._rate.get_phase() if pc is not None: self.__phase_correction.target_value = pc - p = (self._cycles + self.phase + self._phase_correction) % 1.0 + self._base_phase = (self._cycles + self.phase + self._phase_correction) % 1.0 if self.micro is not None: - p = self.micro(p) - i = int(p * len(self._steps)) - if i != self._index or (len(self._steps) == 1 and int(self._cycles) != self._last_edge): # contingency for whole notes + self._base_phase = self.micro(self._base_phase) + i = int(self._base_phase * len(self._steps)) + if i != self._index or ( + len(self._steps) == 1 and int(self._cycles) != self._last_edge): # contingency for whole notes if self._start_lock: - self._index = i + self._index = self._transpose_index = i else: - self._index = (self._index + 1) % len(self._steps) # dont skip steps + self._index = (self._index + 1) % len(self._steps) # dont skip steps + if type(self.transpose) == list and not self._index % int(self.transpose_step_len): + self._transpose_index = (self._transpose_index + 1) % len(self.transpose) if self._index == 0: self.update_triggers() - if isinstance(self.pattern, Tween): # pattern tweens only happen on an edge + if isinstance(self.pattern, Tween): # pattern tweens only happen on an edge pattern = self.pattern.value() else: pattern = self.pattern - self._steps = pattern.resolve() # new patterns kick in here + self._steps = pattern.resolve() # new patterns kick in here if self._start_lock: self._start_lock = False else: @@ -133,7 +141,8 @@ def update_controls(self): value = int(getattr(self, control)) if self._channel not in self._control_values: self._control_values[self._channel] = {} - if control not in self._control_values[self._channel] or value != self._control_values[self._channel][control]: + if control not in self._control_values[self._channel] or value != self._control_values[self._channel][ + control]: midi_out.send_control(self._channel, midi_clamp(self.controls[control]), value) self._control_values[self._channel][control] = value # print("[CTRL %d: %s %s]" % (self._channel, control, value)) @@ -142,8 +151,9 @@ def update_triggers(self): """Check trigger functions a fire as necessary""" updated = False for t, trigger in enumerate(self._triggers): - trigger[3] += 1 # increment edge - if (trigger[1] + 1) - trigger[3] == 0: # have to add 1 because trigger[1] is total 'elapsed' cycles but we're counting edges + trigger[3] += 1 # increment edge + if (trigger[1] + 1) - trigger[ + 3] == 0: # have to add 1 because trigger[1] is total 'elapsed' cycles but we're counting edges try: if num_args(trigger[0]): trigger[0](self) @@ -152,12 +162,12 @@ def update_triggers(self): except Exception as e: print("\n[Trigger error: %s]" % e) if trigger[2] is True: - self.trigger(trigger[0], trigger[1], True) # create new trigger with same properties + self.trigger(trigger[0], trigger[1], True) # create new trigger with same properties else: trigger[2] -= 1 if trigger[2] > 0: - self.trigger(trigger[0], trigger[1], trigger[2] - 1) # same, but decrement repeats - self._triggers[t] = None # clear this trigger + self.trigger(trigger[0], trigger[1], trigger[2] - 1) # same, but decrement repeats + self._triggers[t] = None # clear this trigger updated = True if updated: self._triggers = [trigger for trigger in self._triggers if trigger is not None] @@ -167,19 +177,28 @@ def play(self, step, velocity=None): while isinstance(step, collections.Callable): step = step(self) if num_args(step) else step() self.update_controls() # to handle note-level CC changes - v = self.grace if type(step) == float else 1.0 # floats signify gracenotes + if type(step) == float: # use the part after the decimal to scale velocity + v = step % 1 + v = self.grace if v == 0.0 else v # if decimal part is 0.0 fallback to self.grace to scale velocity + else: + v = 1.0 step = int(step) if type(step) == float else step if step == Z: self.rest() elif step == 0 or step is None: self.hold() else: + transposition = self.transpose + if type(transposition) == list: + transposition = transposition[self._transpose_index % len(transposition)] + while type(transposition) == tuple: + transposition = choice(transposition) if self.chord is None: - pitch = step + pitch = step + int(transposition) else: root, scale = self.chord try: - pitch = root + scale[step] + pitch = scale.quantize(root + int(transposition) + scale[step]) except ScaleError as e: print("\n[Error: %s]" % e) return @@ -212,9 +231,40 @@ def end(self): """Override to add behavior for the end of the piece, otherwise rest""" self.rest() - """Specialized parameters""" + # Convenience methods for getting/setting chord root + # Does NOT support tweening, for that use chord or transpose + @property + def root(self): + if isinstance(self.chord, Tween): + return self.chord.value[0] + return self.chord[0] if self.chord else None + + @root.setter + def root(self, root): + if isinstance(self.chord, Tween): + scale = self.chord.value[1] + else: + scale = self.chord[1] if self.chord else CHR # Default to Chromatic Scale + self.chord = root, scale + + # Convenience methods for getting/setting chord scale + # Does NOT support tweening, for that use chord + @property + def scale(self): + if isinstance(self.chord, Tween): + return self.chord.value[1] + return self.chord[1] if self.chord else None + + @scale.setter + def scale(self, scale): + if isinstance(self.chord, Tween): + root = self.chord.value[0] + else: + root = self.chord[0] if self.chord else C # Default to Middle C + self.chord = root, scale + @property def channel(self): return self._channel @@ -246,15 +296,17 @@ def rate(self): @rate.setter def rate(self, rate): if isinstance(rate, Tween): - rate = RateTween(rate.target_value, rate.cycles, rate.signal_f, rate.end_f, rate.osc) # downcast tween + rate = RateTween(rate.target_value, rate.cycles, rate.signal_f, rate.end_f, rate.osc) # downcast tween if self._sync: def rt(): rate.start(self, self.rate) - phase_correction = tween(89.9, rate.cycles) # make a tween for the subsequent phase correction + phase_correction = tween(89.9, rate.cycles) # make a tween for the subsequent phase correction phase_correction.start(driver, self._phase_correction) self.__phase_correction = phase_correction self._rate = rate - self.trigger(rt) # this wont work unless it happens on an edge, and we need to do that here unlike other tweens + + self.trigger( + rt) # this wont work unless it happens on an edge, and we need to do that here unlike other tweens return else: rate.start(self, self.rate) @@ -266,7 +318,6 @@ def _phase_correction(self): return self.__phase_correction.value return self.__phase_correction - """Sequencing""" def start(self, thread=None): @@ -297,13 +348,13 @@ def trigger(self, f=None, cycles=0, repeat=0): self._triggers = [] else: try: - assert(callable(f)) + assert (callable(f)) if cycles == 0: assert repeat == 0 except AssertionError as e: print("\n[Bad arguments for trigger]") else: - self._triggers.append([f, cycles, repeat, 0]) # last parameter is cycle edges so far + self._triggers.append([f, cycles, repeat, 0]) # last parameter is cycle edges so far def midi_clamp(value): @@ -317,13 +368,14 @@ def midi_clamp(value): def make(controls={}, defaults={}): """Make a Thread with MIDI control values and defaults (will send MIDI)""" - name = "T%s" % str(random())[-4:] # name doesn't really do anything + name = "T%s" % str(random())[-4:] # name doesn't really do anything T = type(name, (Thread,), {}) T.add_attr('controls', controls) for control in controls: - T.add_attr(control, defaults[control] if control in defaults else 0) # mid-level for knobs, off for switches + T.add_attr(control, defaults[control] if control in defaults else 0) # mid-level for knobs, off for switches return T + Thread.setup() """Create all synths in config file--look in the current directory, in the directory above the braid module, and in /usr/local/braid""" diff --git a/braid/tween.py b/braid/tween.py index 1cbc11c..8526886 100644 --- a/braid/tween.py +++ b/braid/tween.py @@ -1,36 +1,64 @@ import collections, math -from random import random -from .signal import linear -from .pattern import Pattern, blend, euc, add, xor +from random import random, uniform, choice +from .signal import linear, sine, pulse, inverse_linear, triangle +from .pattern import Q, Pattern, blend, euc, add, xor from .core import driver class Tween(object): - def __init__(self, target_value, cycles, signal_f=linear(), on_end=None, osc=False, saw=False, start_value=None): + def __init__(self, target_value, cycles, signal_f=linear(), on_end=None, osc=False, phase_offset=0, random=False, saw=False, start_value=None): self.target_value = target_value self.cycles = cycles self.signal_f = signal_f self.end_f = on_end self.osc = osc self.saw = saw + self.phase_offset = phase_offset self.start_value = start_value + if start_value is not None: + self._min_value = min(self.start_value, self.target_value) + self._max_value = max(self.start_value, self.target_value) + self.random = random + self.rand_lock = False + self._random_value = 0 + self._lock_values = collections.deque([0]) + self._lag = 1. + self._step_len = 1/4 + self._steps = [0., .25, .5, .75] + self._step = 0 self.finished = False + def start(self, thread, start_value): self.thread = thread - self.start_value = start_value if self.start_value is None else self.start_value # needed for osc + self._step = 0 + if self.start_value is None: + self.start_value = start_value + self._min_value = min(self.start_value, self.target_value) + self._max_value = max(self.start_value, self.target_value) self.start_cycle = float(math.ceil(self.thread._cycles)) # tweens always start on next cycle @property def value(self): if self.finished: return self.target_value - return self.calc_value(self.signal_position) + if self.random: + if self._steps[self._step] <= self.position < self._steps[self._step] + self._step_len: + if self.rand_lock: + self._lock_values.rotate(-1) + self._random_value = self._lock_values[-1] + else: + lag_diff = (uniform(self._min_value, self._max_value) - self._random_value) * self._lag + self._random_value += lag_diff + self._step = (self._step + 1) % len(self._steps) + return self._random_value + else: + return self.calc_value(self.signal_position) @property def signal_position(self): # can reference this to see where we are on the signal function - return self.signal_f(self.position) + return self.signal_f((self.position + self.phase_offset) % 1.0) @property def position(self): # can reference this to see where we are in the tween @@ -58,6 +86,21 @@ def position(self): # can reference this to see where we are in the tween print('finished is true') return position + @property + def step_len(self): + return self._step_len + + @step_len.setter + def step_len(self, length): + steps = [] + i = 0 + while 0 <= i < 1.0: + steps.append(i) + i += length + self._steps = steps + self._step_len = length + + class ScalarTween(Tween): @@ -105,18 +148,99 @@ def get_phase(self): return phase_correction -def tween(value, cycles, signal_f=linear(), on_end=None, osc=False, saw=False, start=None): +def tween( + value, + cycles, + signal_f=linear(), + on_end=None, + osc=False, + phase_offset=0, + random=False, + saw=False, + start=None, +): if type(value) == int or type(value) == float: - return ScalarTween(value, cycles, signal_f, on_end, osc, saw, start) + return ScalarTween(value, cycles, signal_f, on_end, osc, phase_offset, random, saw, start) if type(value) == tuple: - return ChordTween(value, cycles, signal_f, on_end, osc, saw, start) + return ChordTween(value, cycles, signal_f, on_end, osc, phase_offset, False, saw, start) if type(value) == list: # careful, lists are always patterns value = Pattern(value) if type(value) == Pattern: - return PatternTween(value, cycles, signal_f, on_end, osc, saw, start) - -def osc(start, value, cycles, signal_f=linear(), on_end=None): - return tween(value, cycles, signal_f, on_end, True, False, start) - -def saw(start, value, cycles, signal_f=linear(), on_end=None, saw=True): - return tween(value, cycles, signal_f, on_end, False, True, start) + return PatternTween(value, cycles, signal_f, on_end, osc, phase_offset, False, saw, start) + +def osc(start, value, cycles, signal_f=linear(), phase_offset=0, on_end=None): + return tween( + value, + cycles, + signal_f=signal_f, + on_end=on_end, + osc=True, + phase_offset=phase_offset, + start=start, + ) + +def saw(start, value, cycles, up=True, signal_f=linear(), phase_offset=0, on_end=None): + if not up and signal_f == linear(): + signal_f = inverse_linear() + return tween( + value, + cycles, + signal_f=signal_f, + on_end=on_end, + phase_offset=phase_offset, + saw=True, + start=start, + ) + +def sin(start, value, cycles, phase_offset=0, loop=True, on_end=None): + return tween( + value, + cycles, + signal_f=sine(), + on_end=on_end, + phase_offset=phase_offset, + saw=loop, + start=start, + ) + +def tri(start, value, cycles, symmetry=0.5, phase_offset=0, loop=True, on_end=None): + return tween( + value, + cycles, + signal_f=triangle(symmetry), + on_end=on_end, + phase_offset=phase_offset, + saw=loop, + start=start, + ) + +def pw(start, value, cycles, width=0.5, phase_offset=0, loop=True, on_end=None): + return tween( + value, + cycles, + signal_f=pulse(width), + on_end=on_end, + phase_offset=phase_offset, + saw=loop, + start=start, + ) + +def sh(lo, hi, cycles, step_len, lag=1., loop=True, lock=False, lock_len=None, on_end=None): + t = tween( + hi, + cycles, + start=lo, + on_end=on_end, + random=True, + saw=loop, + ) + t.step_len = step_len + t._lag = lag # scale the distance between the random values + if lock: # pre-generate a list of random values to choose from instead of generating new ones continuously + lock_len = lock_len if lock_len else len(t._steps) + t.rand_lock = True + lock_vals = collections.deque([lo + (uniform(t._min_value, t._max_value) - lo) * lag]) + for x in range(1, lock_len): + lock_vals.append(lock_vals[x - 1] + (uniform(t._min_value, t._max_value) - lock_vals[x - 1]) * lag) + t._lock_values = lock_vals + return t