Skip to content

Commit

Permalink
Merge pull request #16 from discohead/Q-and-euc-additions
Browse files Browse the repository at this point in the history
Adds various bits of functionality
  • Loading branch information
Brian House authored May 19, 2020
2 parents b191bd3 + 9df587a commit f31f02c
Show file tree
Hide file tree
Showing 6 changed files with 537 additions and 112 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ recordings
dev
refs
NOTES.md
*backup.zip
*backup.zip
.idea
110 changes: 92 additions & 18 deletions braid/notation.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
Expand All @@ -33,13 +45,26 @@ 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]
scale = [degree - scale[0] for degree in scale]
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):
Expand Down Expand Up @@ -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
82 changes: 72 additions & 10 deletions braid/pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -168,16 +209,37 @@ 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:
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

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

Loading

0 comments on commit f31f02c

Please sign in to comment.