From 745e798f5079968e4256fe0236974b27609b07d7 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Mon, 24 Feb 2020 19:22:14 -0800 Subject: [PATCH 01/24] Add note Qs and more euc params --- braid/notation.py | 4 ++- braid/pattern.py | 74 ++++++++++++++++++++++++++++++++--------------- braid/thread.py | 53 ++++++++++++++++----------------- 3 files changed, 80 insertions(+), 51 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index fd726db..ccbbff6 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -1,10 +1,11 @@ from random import randint, choice, random, shuffle +from collections import deque 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""" + """ -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): super(Scale, self).__init__(*args) @@ -223,6 +224,7 @@ def __str__(self): R = 'R' # random Z = 'REST' # rest +Q = deque # alias for collections.deque, used to create note queue steps that rotate each cycle: Q([1, 2, 3]) def g(note): # create grace note from named step diff --git a/braid/pattern.py b/braid/pattern.py index 07f8d6d..b01aedd 100644 --- a/braid/pattern.py +++ b/braid/pattern.py @@ -1,7 +1,7 @@ import collections from random import choice, random from . import num_args - +from .signal import ease_in, ease_out class Pattern(list): @@ -9,7 +9,7 @@ class Pattern(list): ... with the addition of the Markov expansion of tuples on calling resolve ... and some blending functions """ - + def __init__(self, value=[0]): list.__init__(self, value) @@ -21,14 +21,18 @@ 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) == collections.deque: + if type(step) == tuple: + step = choice(step) + else: + step.rotate(-1) + step = step[-1] if type(step) == list: step = self._subresolve(step) - steps.append(step) + steps.append(step) return steps - def _unroll(self, pattern, divs=None, r=None): + def _unroll(self, pattern, divs=None, r=None): """Unroll a compacted form to a pattern with lcm steps""" if divs is None: divs = self._get_divs(pattern) @@ -42,7 +46,7 @@ def _unroll(self, pattern, divs=None, r=None): r.append(step) for i in range((divs // len(pattern)) - 1): r.append(0) - return r + return r def _get_divs(self, pattern): """Find lcm for a subpattern""" @@ -53,13 +57,16 @@ def _get_divs(self, pattern): return divs def __repr__(self): - return "P%s" % list.__repr__(self) + return "P%s" % list.__repr__(self) 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) @@ -80,11 +87,11 @@ def prep(pattern_1, pattern_2): if type(pattern_2) is not Pattern: pattern_2 = Pattern(pattern_2) p1_steps = pattern_1.resolve() - p2_steps = pattern_2.resolve() + p2_steps = pattern_2.resolve() pattern = [None] * lcm(len(p1_steps), len(p2_steps)) p1_div = len(pattern) / len(p1_steps) - p2_div = len(pattern) / len(p2_steps) - return pattern, p1_steps, p2_steps, p1_div, p2_div + p2_div = len(pattern) / len(p2_steps) + return pattern, p1_steps, p2_steps, p1_div, p2_div def blend(pattern_1, pattern_2, balance=0.5): @@ -96,7 +103,7 @@ def blend(pattern_1, pattern_2, balance=0.5): pattern[i] = p1_steps[int(i / p1_div)] else: pattern[i] = p2_steps[int(i / p2_div)] - elif i % p1_div == 0: + elif i % p1_div == 0: if random() > ease_out()(balance): # avoid empty middle from linear blend pattern[i] = p1_steps[int(i / p1_div)] elif i % p2_div == 0: @@ -117,7 +124,7 @@ def add(pattern_1, pattern_2): elif i % p2_div == 0 and step_2 != 0 and step_2 != None: pattern[i] = p2_steps[int(i / p2_div)] pattern = Pattern(pattern) - return pattern + return pattern def xor(pattern_1, pattern_2): @@ -125,10 +132,10 @@ def xor(pattern_1, pattern_2): pattern, p1_steps, p2_steps, p1_div, p2_div = prep(pattern_1, pattern_2) for i, cell in enumerate(pattern): step_1 = p1_steps[int(i / p1_div)] - step_2 = p2_steps[int(i / p2_div)] + step_2 = p2_steps[int(i / p2_div)] if i % p1_div == 0 and step_1 != 0 and step_1 != None and i % p2_div == 0 and step_2 != 0 and step_2 != None: pass - elif i % p1_div == 0 and step_1 != 0 and step_1 != None: + elif i % p1_div == 0 and step_1 != 0 and step_1 != None: pattern[i] = p1_steps[int(i / p1_div)] elif i % p2_div == 0 and step_2 != 0 and step_2 != None: pattern[i] = p2_steps[int(i / p2_div)] @@ -140,16 +147,16 @@ def lcm(a, b): gcd, tmp = a, b while tmp != 0: gcd, tmp = tmp, gcd % tmp - return a * b // gcd + return a * b // gcd -def euc(steps, pulses, note=1): +def euc(steps, pulses, rotation=0, invert=False, note=1, rest=0, note_list=None, rest_list=None): steps = int(steps) pulses = int(pulses) if pulses > steps: print("Make pulses > steps") return None - pattern = [] + pattern = [] counts = [] remainders = [] divisor = steps - pulses @@ -163,19 +170,38 @@ def euc(steps, pulses, note=1): if remainders[level] <= 1: break counts.append(divisor) - + def build(level): if level == -1: - pattern.append(0) + pattern.append(rest) elif level == -2: - pattern.append(note) + pattern.append(note) else: for i in range(0, counts[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 \ No newline at end of file + + if rotation: + pattern = Pattern(pattern) + pattern.rotate(rotation) + pattern = list(pattern) + if note_list is None: + note_list = [note] + if rest_list is None: + rest_list = [rest] + pulse = rest 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(rest_list[j % len(rest_list)]) + j += 1 + + return final_pattern diff --git a/braid/thread.py b/braid/thread.py index 2433aa5..c504729 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -27,11 +27,12 @@ def setter(self, value): value = 0 setattr(self, "_%s" % name, value) setattr(cls, "_%s" % name, default) - setattr(cls, name, property(getter, setter)) + setattr(cls, name, property(getter, setter)) @classmethod def setup(cls): # standard properties + Thread.add_attr('transpose', 0) Thread.add_attr('chord', None) Thread.add_attr('velocity', 1.0) Thread.add_attr('grace', 0.75) @@ -52,7 +53,7 @@ def __setattr__(self, key, value): print("[Error: \"%s\"]" % e) def __getattr__(self, key): - print("[No property %s]" % key) + print("[No property %s]" % key) return None def add(self, param, default=0): @@ -61,29 +62,29 @@ def add(self, param, default=0): self._attr_frozen = True def __init__(self, channel, sync=True): - Thread.threads.append(self) + Thread.threads.append(self) # private reference variables - self._attr_frozen = False + self._attr_frozen = False self._channel = channel self._running = False self._cycles = 0.0 self._last_edge = 0 - self._index = -1 + self._index = -1 self._steps = [0] self._previous_pitch = 60 self._previous_step = 1 self.__phase_correction = 0.0 - self._control_values = {} + self._control_values = {} self._triggers = [] - self._sync = sync + self._sync = sync self._start_lock = False - # specialized properties + # specialized properties self.pattern = [0] - self.rate = 1.0 - self.keyboard = False - self.micro = linear() + self.rate = 1.0 + self.keyboard = False + self.micro = linear() print("[Created thread on channel %d]" % self._channel) self._attr_frozen = True @@ -102,7 +103,7 @@ 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 + p = (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)) @@ -163,7 +164,7 @@ def update_triggers(self): self._triggers = [trigger for trigger in self._triggers if trigger is not None] def play(self, step, velocity=None): - """Interpret a step value to play a note""" + """Interpret a step value to play a note""" while isinstance(step, collections.Callable): step = step(self) if num_args(step) else step() self.update_controls() # to handle note-level CC changes @@ -175,11 +176,11 @@ def play(self, step, velocity=None): self.hold() else: if self.chord is None: - pitch = step + pitch = step + int(self.transpose) else: root, scale = self.chord try: - pitch = root + scale[step] + pitch = root + scale[step + int(self.transpose)] except ScaleError as e: print("\n[Error: %s]" % e) return @@ -187,8 +188,8 @@ def play(self, step, velocity=None): velocity *= self.velocity velocity *= v self.note(pitch, velocity) - if step != 0: - self._previous_step = step + if step != 0: + self._previous_step = step def note(self, pitch, velocity): """Override for custom MIDI behavior""" @@ -206,11 +207,11 @@ def hold(self): def rest(self): """Send a MIDI off""" - midi_out.send_note(self._channel, self._previous_pitch, 0) + midi_out.send_note(self._channel, self._previous_pitch, 0) def end(self): """Override to add behavior for the end of the piece, otherwise rest""" - self.rest() + self.rest() """Specialized parameters""" @@ -226,7 +227,7 @@ def channel(self, channel): @property def pattern(self): if isinstance(self._pattern, Tween): - return self._pattern.value + return self._pattern.value return self._pattern @pattern.setter @@ -240,7 +241,7 @@ def pattern(self, pattern): @property def rate(self): if isinstance(self._rate, Tween): - return self._rate.value + return self._rate.value return self._rate @rate.setter @@ -263,7 +264,7 @@ def rt(): @property def _phase_correction(self): if isinstance(self.__phase_correction, Tween): - return self.__phase_correction.value + return self.__phase_correction.value return self.__phase_correction @@ -318,7 +319,7 @@ 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 - T = type(name, (Thread,), {}) + 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 @@ -330,17 +331,17 @@ def make(controls={}, defaults={}): synths = {} try: with open(os.path.join(os.getcwd(), "synths.yaml")) as f: - synths.update(yaml.load(f)) + synths.update(yaml.load(f, Loader=yaml.FullLoader)) except FileNotFoundError as e: pass try: with open(os.path.join(os.path.dirname(__file__), "..", "synths.yaml")) as f: - synths.update(yaml.load(f)) + synths.update(yaml.load(f, Loader=yaml.FullLoader)) except FileNotFoundError as e: pass try: with open("/usr/local/braid/synths.yaml") as f: - synths.update(yaml.load(f)) + synths.update(yaml.load(f, Loader=yaml.FullLoader)) except FileNotFoundError as e: pass if len(synths): From e4d4a8ede10c68f290741af91c70f501bf312f74 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Mon, 24 Feb 2020 23:35:04 -0800 Subject: [PATCH 02/24] add quantize method to Scale --- braid/notation.py | 37 ++++++++++++++++++++++++++++++++----- braid/thread.py | 5 ++++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index ccbbff6..19d9785 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -1,13 +1,18 @@ from random import randint, choice, random, shuffle from collections import deque +from bisect import bisect_left 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: """ + """Allows for specifying scales by degree, up to octaves_below below and octaves_above above""" + """Set constrain=True to keep degree values in range while preserving pitch class, else raise 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): + def __init__(self, *args, constrain=False, octaves_below=1, octaves_above=2): + self.constrain = constrain + self.octaves_below = octaves_below + self.octaves_above = octaves_above super(Scale, self).__init__(*args) def __getitem__(self, degree): @@ -20,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 < self._lower_bound(): + degree = degree + len(self) + else: + if degree > self._upper_bound() or degree < self._lower_bound(): + raise ScaleError(degree) if degree < 0: degree = abs(degree) octave_shift -= 12 @@ -34,6 +45,12 @@ def __getitem__(self, degree): return float(semitone) return semitone + def _lower_bound(self): + return 0 - len(self) * self.octaves_below + + def _upper_bound(self): + return len(self) * self.octaves_above + def rotate(self, steps): l = list(self) scale = l[steps:] + l[:steps] @@ -41,6 +58,16 @@ def rotate(self, steps): scale = [(degree + 12) if degree < 0 else degree for degree in scale] return Scale(scale) + def quantize(self, interval): + if interval in self: + return interval + octave = interval // self[len(self)] + interval = interval - octave * 12 + degree = bisect_left(list(self), interval) + 1 + if octave: + return self[degree] + octave * 12 + return self[degree] + class ScaleError(Exception): diff --git a/braid/thread.py b/braid/thread.py index c504729..43e4e8d 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -179,8 +179,11 @@ def play(self, step, velocity=None): pitch = step + int(self.transpose) else: root, scale = self.chord + transposition = 0 + if self.transpose: + transposition = scale.quantize(int(self.transpose)) try: - pitch = root + scale[step + int(self.transpose)] + pitch = root + scale[step] + transposition except ScaleError as e: print("\n[Error: %s]" % e) return From 4503f152688f7c80e1479bed6ba67200ae207655 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Tue, 25 Feb 2020 08:51:39 -0800 Subject: [PATCH 03/24] remove _lower_bound add convenience methods for thread root scale --- braid/notation.py | 30 +++++++++++++----------------- braid/thread.py | 31 +++++++++++++++++++++++++++++++ braid/tween.py | 2 +- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index 19d9785..76e542d 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -1,17 +1,16 @@ from random import randint, choice, random, shuffle from collections import deque -from bisect import bisect_left +from bisect import bisect class Scale(list): - """Allows for specifying scales by degree, up to octaves_below below and octaves_above above""" - """Set constrain=True to keep degree values in range while preserving pitch class, else raise ScaleError""" + """Allows for specifying scales by degree, up to 1 octave below and octaves_above above (default 2)""" + """Set constrain=True to octave shift degree values into range thus preserving pitch class, else raise 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_below=1, octaves_above=2): + def __init__(self, *args, constrain=False, octaves_above=2): self.constrain = constrain - self.octaves_below = octaves_below self.octaves_above = octaves_above super(Scale, self).__init__(*args) @@ -28,10 +27,10 @@ def __getitem__(self, degree): if self.constrain: while degree > self._upper_bound(): degree = degree - len(self) - while degree < self._lower_bound(): + while degree < 0 - len(self): degree = degree + len(self) else: - if degree > self._upper_bound() or degree < self._lower_bound(): + if degree > self._upper_bound() or degree < 0 - len(self): raise ScaleError(degree) if degree < 0: degree = abs(degree) @@ -45,9 +44,6 @@ def __getitem__(self, degree): return float(semitone) return semitone - def _lower_bound(self): - return 0 - len(self) * self.octaves_below - def _upper_bound(self): return len(self) * self.octaves_above @@ -59,15 +55,15 @@ def rotate(self, steps): 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 = interval // self[len(self)] - interval = interval - octave * 12 - degree = bisect_left(list(self), interval) + 1 - if octave: - return self[degree] + octave * 12 - return self[degree] - + octave_shift = interval // self[len(self)] * 12 + interval = interval - octave_shift + degree = bisect(list(self), interval) + 1 + return self[degree] + octave_shift class ScaleError(Exception): diff --git a/braid/thread.py b/braid/thread.py index 43e4e8d..b21845d 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -218,6 +218,37 @@ def end(self): """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/settings 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): diff --git a/braid/tween.py b/braid/tween.py index bb3613a..f8c9181 100644 --- a/braid/tween.py +++ b/braid/tween.py @@ -110,7 +110,7 @@ def tween(value, cycles, signal_f=linear(), on_end=None, osc=False, saw=False, s if type(value) == int or type(value) == float: return ScalarTween(value, cycles, signal_f, on_end, osc, saw, start) if type(value) == tuple: - return ChordTween(value, cycles, signal_f, one_end, osc, saw, start) + return ChordTween(value, cycles, signal_f, on_end, osc, saw, start) if type(value) == list: # careful, lists are always patterns value = Pattern(value) if type(value) == Pattern: From f8c1e5b7d8593a767c206895fb31fb26f264ba9e Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Tue, 25 Feb 2020 10:43:51 -0800 Subject: [PATCH 04/24] add notes and invert methods to Pattern --- braid/notation.py | 2 +- braid/pattern.py | 31 +++++++++++++++++++++++++------ braid/thread.py | 2 +- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index 76e542d..2747ffe 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -5,7 +5,7 @@ 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 degree values into range thus preserving pitch class, else raise ScaleError""" + """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""" diff --git a/braid/pattern.py b/braid/pattern.py index b01aedd..d4507d1 100644 --- a/braid/pattern.py +++ b/braid/pattern.py @@ -59,6 +59,10 @@ 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]) @@ -80,6 +84,21 @@ 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 values from note_list which defaults to self.notes()""" + """and replace all occurrences of NOT off_note with off_note""" + 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: @@ -150,7 +169,7 @@ def lcm(a, b): return a * b // gcd -def euc(steps, pulses, rotation=0, invert=False, note=1, rest=0, note_list=None, rest_list=None): +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: @@ -173,7 +192,7 @@ def euc(steps, pulses, rotation=0, invert=False, note=1, rest=0, note_list=None, def build(level): if level == -1: - pattern.append(rest) + pattern.append(off_note) elif level == -2: pattern.append(note) else: @@ -191,9 +210,9 @@ def build(level): pattern = list(pattern) if note_list is None: note_list = [note] - if rest_list is None: - rest_list = [rest] - pulse = rest if invert else 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: @@ -201,7 +220,7 @@ def build(level): final_pattern.append(note_list[i % len(note_list)]) i += 1 else: - final_pattern.append(rest_list[j % len(rest_list)]) + final_pattern.append(off_note_list[j % len(off_note_list)]) j += 1 return final_pattern diff --git a/braid/thread.py b/braid/thread.py index b21845d..dcbc27d 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -234,7 +234,7 @@ def root(self, root): scale = self.chord[1] if self.chord else CHR # Default to Chromatic Scale self.chord = root, scale - # Convenience methods for getting/settings chord scale + # Convenience methods for getting/setting chord scale # Does NOT support tweening, for that use chord @property def scale(self): From ead25186d9d2ab99fe69cda1bc502401d24a1b14 Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Tue, 25 Feb 2020 11:26:28 -0800 Subject: [PATCH 05/24] update comment --- braid/pattern.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/braid/pattern.py b/braid/pattern.py index d4507d1..9b75d11 100644 --- a/braid/pattern.py +++ b/braid/pattern.py @@ -85,8 +85,9 @@ def xor(self, pattern_2): list.__init__(self, l) def invert(self, off_note=0, note_list=None): - """replace all occurrences of off_note with values from note_list which defaults to self.notes()""" + """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 = [] From 6ca8ba3cfc32cba024194174e98e0703461d7e93 Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Tue, 25 Feb 2020 20:15:19 -0800 Subject: [PATCH 06/24] Make Q class add drunk properties --- braid/notation.py | 2 -- braid/pattern.py | 24 ++++++++++++++++++++---- braid/tween.py | 24 ++++++++++++------------ 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index 2747ffe..c4fe0b3 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -247,8 +247,6 @@ def __str__(self): R = 'R' # random Z = 'REST' # rest -Q = deque # alias for collections.deque, used to create note queue steps that rotate each cycle: Q([1, 2, 3]) - def g(note): # create grace note from named step return float(note) diff --git a/braid/pattern.py b/braid/pattern.py index 9b75d11..05c89d6 100644 --- a/braid/pattern.py +++ b/braid/pattern.py @@ -3,14 +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): @@ -21,12 +32,17 @@ def _subresolve(self, pattern): """Resolve a subbranch of the pattern""" steps = [] for step in pattern: - while type(step) == tuple or type(step) == collections.deque: + while type(step) == tuple or type(step) == Q: if type(step) == tuple: step = choice(step) else: - step.rotate(-1) + 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) diff --git a/braid/tween.py b/braid/tween.py index 623726f..8260038 100644 --- a/braid/tween.py +++ b/braid/tween.py @@ -1,7 +1,7 @@ import collections, math from random import random from .signal import linear -from .pattern import Pattern, blend, euc, add, xor +from .pattern import Q, Pattern, blend, euc, add, xor from .core import driver @@ -23,14 +23,14 @@ def start(self, thread, start_value): self.start_cycle = float(math.ceil(self.thread._cycles)) # tweens always start on next cycle @property - def value(self): + def value(self): if self.finished: return self.target_value 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) @property def position(self): # can reference this to see where we are in the tween @@ -40,7 +40,7 @@ def position(self): # can reference this to see where we are in the tween if position <= 0.0: position = 0.0 if position >= 1.0: - position = 1.0 + position = 1.0 if self.end_f is not None: try: self.end_f() @@ -56,26 +56,26 @@ def position(self): # can reference this to see where we are in the tween else: self.finished = True print('finished is true') - return position + return position + - class ScalarTween(Tween): - def calc_value(self, position): + def calc_value(self, position): value = (position * (self.target_value - self.start_value)) + self.start_value return value - + class ChordTween(Tween): def calc_value(self, position): - if random() > position: + if random() > position: return self.start_value else: return self.target_value -class PatternTween(Tween): +class PatternTween(Tween): def calc_value(self, position): return blend(self.start_value, self.target_value, position) @@ -99,7 +99,7 @@ def get_phase(self): cycles_at_completion = syncer_cycles_remaining + self.syncer._cycles phase_at_completion = cycles_at_completion % 1.0 phase_correction = phase_at_completion - phase_correction *= -1 + phase_correction *= -1 if phase_correction < 0.0: phase_correction = 1.0 + phase_correction return phase_correction @@ -120,4 +120,4 @@ 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 tween(value, cycles, signal_f, on_end, False, True, start) From ffa9b513089372b9e88dd0f5975d7409001f3026 Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Wed, 26 Feb 2020 08:41:34 -0800 Subject: [PATCH 07/24] scale step velocity by part after decimal --- braid/thread.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/braid/thread.py b/braid/thread.py index dcbc27d..1d3d5c5 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -168,7 +168,11 @@ 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() From cb1a14fe7ad41b9d983049d87d9f2492e7f31820 Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Wed, 26 Feb 2020 09:50:44 -0800 Subject: [PATCH 08/24] add v() for generating random velocity notes --- braid/notation.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index c4fe0b3..563a932 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -1,4 +1,4 @@ -from random import randint, choice, random, shuffle +from random import randint, choice, random, shuffle, uniform from collections import deque from bisect import bisect @@ -244,9 +244,24 @@ def __str__(self): # -R = 'R' # random +R = 'R' # random note +Rv = 'Rv' # random note and random velocity 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 step + # v_scale can be a single float value 0.0 < v_scale < 1.0 + # or it can be a tuple of lo, hi to randomly select from, e.g. v(C4, (.2, .9)) + # or if v_scale is None randomly generate 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 note + v_scale From 789d24cf13e1a2c601f7bd12ac0302dd0e2bdcd9 Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Wed, 26 Feb 2020 09:54:30 -0800 Subject: [PATCH 09/24] remove Rv --- braid/notation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/braid/notation.py b/braid/notation.py index 563a932..f08ffa6 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -245,7 +245,6 @@ def __str__(self): # R = 'R' # random note -Rv = 'Rv' # random note and random velocity Z = 'REST' # rest def g(note): From 7e69565bf0bcc8696e85db8f0a4306b0fd589314 Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Wed, 26 Feb 2020 10:08:14 -0800 Subject: [PATCH 10/24] update v() comment and cast note to int before adding v_scale --- braid/notation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index f08ffa6..f2c55fd 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -244,7 +244,7 @@ def __str__(self): # -R = 'R' # random note +R = 'R' # random Z = 'REST' # rest def g(note): @@ -252,10 +252,10 @@ def g(note): return float(note) def v(note, v_scale=None): - # create a note with scaled velocity from named step - # v_scale can be a single float value 0.0 < v_scale < 1.0 - # or it can be a tuple of lo, hi to randomly select from, e.g. v(C4, (.2, .9)) - # or if v_scale is None randomly generate between 0.17 and 0.999 + # 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): @@ -263,4 +263,4 @@ def v(note, v_scale=None): v_scale = uniform(v_scale[0] % 1, hi) else: v_scale = uniform(0.17, 0.999) - return note + v_scale + return int(note) + v_scale From f532b65d6a8906d25cf5f9673e4e5554d70d055c Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Sat, 29 Feb 2020 13:23:56 -0800 Subject: [PATCH 11/24] add sample-and-hold-like tween sh() --- .gitignore | 3 ++- braid/thread.py | 4 +--- braid/tween.py | 62 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 54 insertions(+), 15 deletions(-) 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/thread.py b/braid/thread.py index 1d3d5c5..d59d8c3 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -183,9 +183,7 @@ def play(self, step, velocity=None): pitch = step + int(self.transpose) else: root, scale = self.chord - transposition = 0 - if self.transpose: - transposition = scale.quantize(int(self.transpose)) + transposition = scale.quantize(int(self.transpose)) try: pitch = root + scale[step] + transposition except ScaleError as e: diff --git a/braid/tween.py b/braid/tween.py index 8260038..1e11bb1 100644 --- a/braid/tween.py +++ b/braid/tween.py @@ -1,5 +1,5 @@ import collections, math -from random import random +from random import random, uniform from .signal import linear from .pattern import Q, Pattern, blend, euc, add, xor from .core import driver @@ -7,7 +7,7 @@ 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, random=False, saw=False, start_value=None): self.target_value = target_value self.cycles = cycles self.signal_f = signal_f @@ -15,18 +15,38 @@ def __init__(self, target_value, cycles, signal_f=linear(), on_end=None, osc=Fal self.osc = osc self.saw = saw 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._random_value = 0 + 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 and (self._step != 0 or self.position <= self._steps[-1]): + self._step += 1 + self._step %= len(self._steps) + self._random_value = uniform(self._min_value, self._max_value) + 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 @@ -58,6 +78,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,19 +140,24 @@ 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, random=False, saw=False, start=None): print('new tween') 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, 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, 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) + return PatternTween(value, cycles, signal_f, on_end, osc, False, saw, start) def osc(start, value, cycles, signal_f=linear(), on_end=None): - return tween(value, cycles, signal_f, on_end, True, False, start) + return tween(value, cycles, signal_f, on_end, True, False, False, start) + +def saw(start, value, cycles, signal_f=linear(), on_end=None): + return tween(value, cycles, signal_f, on_end, False, False, True, 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) +def sh(hi, lo, cycles, step_len, continuous=True, on_end=None): + t = tween(hi, cycles, start=lo, on_end=on_end, random=True, saw=continuous) + t.step_len = step_len + return t From 1b1939b191b491e6b444bf66779eafbcd8f65d19 Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Sat, 29 Feb 2020 16:28:29 -0800 Subject: [PATCH 12/24] phase_offset tweens, support lists and tuples for transpose --- braid/thread.py | 16 ++++++++++++---- braid/tween.py | 26 +++++++++++++------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/braid/thread.py b/braid/thread.py index d59d8c3..0503463 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -33,6 +33,7 @@ def setter(self, value): 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) @@ -71,6 +72,7 @@ def __init__(self, channel, sync=True): self._cycles = 0.0 self._last_edge = 0 self._index = -1 + self._transpose_index = -1 self._steps = [0] self._previous_pitch = 60 self._previous_step = 1 @@ -109,9 +111,11 @@ def update(self, delta_t): 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 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 + 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 @@ -179,13 +183,17 @@ def play(self, step, velocity=None): 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 + int(self.transpose) + pitch = step + int(transposition) else: root, scale = self.chord - transposition = scale.quantize(int(self.transpose)) try: - pitch = root + scale[step] + transposition + pitch = root + scale[step] + scale.quantize(int(transposition)) except ScaleError as e: print("\n[Error: %s]" % e) return diff --git a/braid/tween.py b/braid/tween.py index 1e11bb1..6a4641f 100644 --- a/braid/tween.py +++ b/braid/tween.py @@ -7,13 +7,14 @@ class Tween(object): - def __init__(self, target_value, cycles, signal_f=linear(), on_end=None, osc=False, random=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) @@ -40,9 +41,8 @@ def value(self): if self.finished: return self.target_value if self.random: - if self._steps[self._step] <= self.position and (self._step != 0 or self.position <= self._steps[-1]): - self._step += 1 - self._step %= len(self._steps) + if self._steps[self._step] <= self.position < self._steps[self._step] + self._step_len: + self._step = (self._step + 1) % len(self._steps) self._random_value = uniform(self._min_value, self._max_value) return self._random_value else: @@ -56,7 +56,7 @@ def signal_position(self): # can reference this to see where we are on the signa def position(self): # can reference this to see where we are in the tween if self.cycles == 0.0: return 1.0 - position = (self.thread._cycles - self.start_cycle) / self.cycles + position = (self.thread._cycles - self.start_cycle) / self.cycles + self.phase_offset if position <= 0.0: position = 0.0 if position >= 1.0: @@ -140,22 +140,22 @@ def get_phase(self): return phase_correction -def tween(value, cycles, signal_f=linear(), on_end=None, osc=False, random=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): print('new tween') if type(value) == int or type(value) == float: - return ScalarTween(value, cycles, signal_f, on_end, osc, random, 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, False, 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, False, saw, start) + return PatternTween(value, cycles, signal_f, on_end, osc, phase_offset, False, saw, start) -def osc(start, value, cycles, signal_f=linear(), on_end=None): - return tween(value, cycles, signal_f, on_end, True, False, False, start) +def osc(start, value, cycles, signal_f=linear(), phase_offset=0, on_end=None): + return tween(value, cycles, signal_f, on_end, True, phase_offset, False, False, start) -def saw(start, value, cycles, signal_f=linear(), on_end=None): - return tween(value, cycles, signal_f, on_end, False, False, True, start) +def saw(start, value, cycles, signal_f=linear(), phase_offset=0, on_end=None): + return tween(value, cycles, signal_f, on_end, False, phase_offset, False, True, start) def sh(hi, lo, cycles, step_len, continuous=True, on_end=None): t = tween(hi, cycles, start=lo, on_end=on_end, random=True, saw=continuous) From 3c274d3449d1fba503afd6a717ed71e482bf21fa Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sat, 29 Feb 2020 20:32:06 -0800 Subject: [PATCH 13/24] fix double assignment --- braid/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/braid/thread.py b/braid/thread.py index 0503463..589217a 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -111,7 +111,7 @@ def update(self, delta_t): 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 if self._start_lock: - self._index, self._transpose_index = i + self._index = self._transpose_index = i else: 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): From deb90810b910f7dbbf118158f86de5adff1750e3 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 1 Mar 2020 09:00:04 -0800 Subject: [PATCH 14/24] quantize(root+transpose) --- braid/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/braid/thread.py b/braid/thread.py index 589217a..3f817b8 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -193,7 +193,7 @@ def play(self, step, velocity=None): else: root, scale = self.chord try: - pitch = root + scale[step] + scale.quantize(int(transposition)) + pitch = scale.quantize(root + int(transposition)) + scale[step] except ScaleError as e: print("\n[Error: %s]" % e) return From a6266ab437ebcfe6babd0c9904f682a2899275aa Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 1 Mar 2020 10:31:02 -0800 Subject: [PATCH 15/24] fix quatnize octatve_shift calculation --- braid/notation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index f2c55fd..50cc06b 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -1,6 +1,6 @@ from random import randint, choice, random, shuffle, uniform from collections import deque -from bisect import bisect +from bisect import bisect_left class Scale(list): @@ -60,9 +60,9 @@ def quantize(self, interval): """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 // self[len(self)] * 12 + octave_shift = (interval // 12) * 12 interval = interval - octave_shift - degree = bisect(list(self), interval) + 1 + degree = bisect_left(list(self), interval) + 1 return self[degree] + octave_shift From e31b2f2b659024cfa1423502bd047e4f5c4819f8 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 1 Mar 2020 12:43:35 -0800 Subject: [PATCH 16/24] quantize(root + transpose + scale[step]), add a few scales --- braid/notation.py | 33 +++++++++++++++++++++++++++++---- braid/thread.py | 2 +- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index 50cc06b..21e1f61 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -213,21 +213,46 @@ def __str__(self): 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, 4, 5, 6, 9, 11]) +NMI = Scale([0, 2, 3, 5, 7, 8, 10]) # Natural Minor -BLU = Scale([0, 3, 5, 6, 7, 10]) +MMI = Scale([0, 2, 4, 5, 6, 9, 11]) # Melodic Minor + +BLU = BMI = Scale([0, 3, 5, 6, 7, 10]) # Blues Minor + +BMA = Scale([[0, 3, 4, 7, 9, 10]]) # Blues major (From midipal/BitT source code) PEN = Scale([0, 2, 5, 7, 10]) +PMI = Scale([0, 2, 4, 7, 9]) # Pentatonic major (From midipal/BitT source code) + +PMA = 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]) diff --git a/braid/thread.py b/braid/thread.py index 3f817b8..609efb6 100644 --- a/braid/thread.py +++ b/braid/thread.py @@ -193,7 +193,7 @@ def play(self, step, velocity=None): else: root, scale = self.chord try: - pitch = scale.quantize(root + int(transposition)) + scale[step] + pitch = scale.quantize(root + int(transposition) + scale[step]) except ScaleError as e: print("\n[Error: %s]" % e) return From 72e767bce89ac01049e4faebbb38108fcc06c461 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 1 Mar 2020 12:45:33 -0800 Subject: [PATCH 17/24] fix pentatonic scale names --- braid/notation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index 21e1f61..4922e09 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -225,9 +225,9 @@ def __str__(self): PEN = Scale([0, 2, 5, 7, 10]) -PMI = Scale([0, 2, 4, 7, 9]) # Pentatonic major (From midipal/BitT source code) +PMA = Scale([0, 2, 4, 7, 9]) # Pentatonic major (From midipal/BitT source code) -PMA = Scale([0, 3, 5, 7, 10]) # Pentatonic minor (From midipal/BitT source code) +PMI = Scale([0, 3, 5, 7, 10]) # Pentatonic minor (From midipal/BitT source code) # world From 10f1c15a767aaca52ff30bb2a45143593e59d961 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 1 Mar 2020 12:57:22 -0800 Subject: [PATCH 18/24] remove redundant parens from BMA Scale --- braid/notation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/braid/notation.py b/braid/notation.py index 4922e09..d4d6b77 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -221,7 +221,7 @@ def __str__(self): BLU = BMI = Scale([0, 3, 5, 6, 7, 10]) # Blues Minor -BMA = Scale([[0, 3, 4, 7, 9, 10]]) # Blues major (From midipal/BitT source code) +BMA = Scale([0, 3, 4, 7, 9, 10]) # Blues major (From midipal/BitT source code) PEN = Scale([0, 2, 5, 7, 10]) From c38b30c219eb5cd1f54609085ecaf12d6f4aa325 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 1 Mar 2020 13:05:31 -0800 Subject: [PATCH 19/24] remove redundant NMI definition, reuse AOL --- braid/notation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index d4d6b77..251a5e2 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -209,14 +209,12 @@ def __str__(self): MYX = DOM = ION.rotate(4) -AOL = ION.rotate(5) +AOL = NMI = ION.rotate(5) LOC = ION.rotate(6) MIN = HMI = Scale([0, 2, 3, 5, 7, 8, 11]) # Harmonic Minor -NMI = Scale([0, 2, 3, 5, 7, 8, 10]) # Natural Minor - MMI = Scale([0, 2, 4, 5, 6, 9, 11]) # Melodic Minor BLU = BMI = Scale([0, 3, 5, 6, 7, 10]) # Blues Minor From d84e1f269d89becedf9ed9b767d184aff0bf7f0d Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 1 Mar 2020 19:34:56 -0800 Subject: [PATCH 20/24] add various signal and tween generators, add lock & lag to sh() --- braid/signal.py | 35 ++++++++++++++++ braid/tween.py | 105 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 129 insertions(+), 11 deletions(-) diff --git a/braid/signal.py b/braid/signal.py index 951b860..fd2bb29 100644 --- a/braid/signal.py +++ b/braid/signal.py @@ -1,6 +1,11 @@ import time, math, __main__ from random import random +def pos2rad(pos): + pos = clamp(pos) + degrees = pos * 360 + return math.radians(degrees) + def clamp(pos): if pos > 1.0: return 1.0 @@ -15,6 +20,31 @@ def f(pos): return pos return f +def inverse_linear(): + def f(pos): + pos = clamp(1 - pos) + return pos + return f + +def triangle(s=0.5): + def f(pos): + pos = clamp(pos) + if pos < s: + pos *= 1/s + else: + pos = 1 - ((pos - s) * (1 / (1 - s))) + return pos + return f + +def pulse(w=0.5): + def f(pos): + pos = clamp(pos) + if pos < w: + return 0.0 + else: + return 1.0 + return f + def ease_in(exp=2): def f(pos): pos = clamp(pos) @@ -48,6 +78,11 @@ def f(pos): return 1.0 - (0.5 * pos**exp + 0.5) return f +def sine(): + def f(pos): + return math.sin(pos2rad(pos)) * 0.5 + 0.5 + return f + def normalize(signal): min_value = min(signal) max_value = max(signal) diff --git a/braid/tween.py b/braid/tween.py index 6a4641f..1e984a1 100644 --- a/braid/tween.py +++ b/braid/tween.py @@ -1,6 +1,6 @@ import collections, math -from random import random, uniform -from .signal import linear +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 @@ -20,7 +20,10 @@ def __init__(self, target_value, cycles, signal_f=linear(), on_end=None, osc=Fal 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 = [0] + self._lag = 1. self._step_len = 1/4 self._steps = [0., .25, .5, .75] self._step = 0 @@ -42,8 +45,12 @@ def value(self): return self.target_value if self.random: if self._steps[self._step] <= self.position < self._steps[self._step] + self._step_len: + if self.rand_lock: + self._random_value = self._lock_values[self._step] + 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) - self._random_value = uniform(self._min_value, self._max_value) return self._random_value else: return self.calc_value(self.signal_position) @@ -140,7 +147,17 @@ def get_phase(self): return phase_correction -def tween(value, cycles, signal_f=linear(), on_end=None, osc=False, phase_offset=0, random=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, +): print('new tween') if type(value) == int or type(value) == float: return ScalarTween(value, cycles, signal_f, on_end, osc, phase_offset, random, saw, start) @@ -152,12 +169,78 @@ def tween(value, cycles, signal_f=linear(), on_end=None, osc=False, phase_offset 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, on_end, True, phase_offset, False, False, start) - -def saw(start, value, cycles, signal_f=linear(), phase_offset=0, on_end=None): - return tween(value, cycles, signal_f, on_end, False, phase_offset, False, True, start) - -def sh(hi, lo, cycles, step_len, continuous=True, on_end=None): - t = tween(hi, cycles, start=lo, on_end=on_end, random=True, saw=continuous) + 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 = [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 From f24b9268ead2b382fc23b627f769d075b72a9bc1 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 1 Mar 2020 20:20:33 -0800 Subject: [PATCH 21/24] use phase_offset in signal_position not position --- braid/tween.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/braid/tween.py b/braid/tween.py index 1e984a1..05e9756 100644 --- a/braid/tween.py +++ b/braid/tween.py @@ -57,13 +57,13 @@ def value(self): @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 if self.cycles == 0.0: return 1.0 - position = (self.thread._cycles - self.start_cycle) / self.cycles + self.phase_offset + position = (self.thread._cycles - self.start_cycle) / self.cycles if position <= 0.0: position = 0.0 if position >= 1.0: From 0fe6e1c576f9939f088683777cb6180051ab28ae Mon Sep 17 00:00:00 2001 From: jaredmixpanel Date: Mon, 2 Mar 2020 09:49:11 -0800 Subject: [PATCH 22/24] make lock_vals a deque --- braid/tween.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/braid/tween.py b/braid/tween.py index 05e9756..6a962d0 100644 --- a/braid/tween.py +++ b/braid/tween.py @@ -22,7 +22,7 @@ def __init__(self, target_value, cycles, signal_f=linear(), on_end=None, osc=Fal self.random = random self.rand_lock = False self._random_value = 0 - self._lock_values = [0] + self._lock_values = collections.deque([0]) self._lag = 1. self._step_len = 1/4 self._steps = [0., .25, .5, .75] @@ -46,7 +46,8 @@ def value(self): if self.random: if self._steps[self._step] <= self.position < self._steps[self._step] + self._step_len: if self.rand_lock: - self._random_value = self._lock_values[self._step] + 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 @@ -239,7 +240,7 @@ def sh(lo, hi, cycles, step_len, lag=1., loop=True, lock=False, lock_len=None, o 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 = [lo + (uniform(t._min_value, t._max_value) - lo) * lag] + 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 From 4ca06e4ece151c94249e76d111de97b60d010f97 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sun, 15 Mar 2020 12:29:06 -0700 Subject: [PATCH 23/24] make signals composeable --- braid/notation.py | 24 ++++-- braid/signal.py | 183 ++++++++++++++++++++++++++++++++-------------- braid/thread.py | 57 ++++++++------- 3 files changed, 181 insertions(+), 83 deletions(-) diff --git a/braid/notation.py b/braid/notation.py index 251a5e2..8515e5c 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -1,9 +1,10 @@ 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): +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: """ @@ -255,25 +256,25 @@ def __str__(self): 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) @@ -287,3 +288,16 @@ def v(note, v_scale=None): 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/signal.py b/braid/signal.py index fd2bb29..1208a9b 100644 --- a/braid/signal.py +++ b/braid/signal.py @@ -1,11 +1,26 @@ 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: return 1.0 @@ -14,88 +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 inverse_linear(): + +def noise(l=0, h=1, a=1, r=1, p=0, b=0, m=None): def f(pos): - pos = clamp(1 - pos) - return 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 triangle(s=0.5): + +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 = clamp(pos) - if pos < s: - pos *= 1/s + 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: - pos = 1 - ((pos - s) * (1 / (1 - s))) - return pos + value = 1 - ((pos - sym) * (1 / (1 - sym))) + return amp_bias(value, a, b, pos) + return f -def pulse(w=0.5): + +def pulse(w=.5, a=1, r=1, p=0, b=0): def f(pos): - pos = clamp(pos) - if pos < w: - return 0.0 + 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 1.0 + return amp_bias(1.0, a, b, pos) + return f -def ease_in(exp=2): + +def ease_in(exp=2, a=1, r=1, p=0, b=0): def f(pos): - pos = clamp(pos) - return pos**exp + 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_out(exp=3): + + +def ease_out(exp=3, 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 - 1) ** e + 1), 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_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): + + +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(): + +def sine(a=1, r=1, p=0, b=0): def f(pos): - return math.sin(pos2rad(pos)) * 0.5 + 0.5 + 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: @@ -112,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 @@ -126,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): @@ -150,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 @@ -166,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 609efb6..604795b 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,6 +28,7 @@ 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)) @@ -41,7 +44,6 @@ def setup(cls): Thread.add_attr('micro', None) Thread.add_attr('controls', None) - """Instance definitions""" def __setattr__(self, key, value): @@ -70,6 +72,7 @@ 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 @@ -94,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: @@ -105,24 +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 = 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: @@ -138,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)) @@ -147,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) @@ -157,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] @@ -226,8 +231,8 @@ 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 @@ -291,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) @@ -311,7 +318,6 @@ def _phase_correction(self): return self.__phase_correction.value return self.__phase_correction - """Sequencing""" def start(self, thread=None): @@ -342,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): @@ -362,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""" From b99a8146006e7660b62c50712b141b32ea09e7f2 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Sat, 4 Apr 2020 13:02:28 -0700 Subject: [PATCH 24/24] fix melodic minor scale --- braid/notation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/braid/notation.py b/braid/notation.py index 8515e5c..90b748e 100644 --- a/braid/notation.py +++ b/braid/notation.py @@ -216,7 +216,7 @@ def __str__(self): MIN = HMI = Scale([0, 2, 3, 5, 7, 8, 11]) # Harmonic Minor -MMI = Scale([0, 2, 4, 5, 6, 9, 11]) # Melodic Minor +MMI = Scale([0, 2, 3, 5, 7, 9, 11]) # Melodic Minor BLU = BMI = Scale([0, 3, 5, 6, 7, 10]) # Blues Minor