"""Sequence, Event and Phrase classes""" import threading from . import seqgens def list_methods_to_pitches(cls): """Inject list methods into Sequence such that they apply to the "pitches" attribute.""" overrides = ('__repr__', '__ge__','__gt__','__lt__', '__le__','__eq__', '__ne__') for name, meth in vars(list).items(): if not hasattr(cls, name) or name in overrides: def new_meth(self, *args, meth=meth): reference = self.pitches[:] result = meth(reference, *args) if reference != self.pitches: self.__init__(*reference, cardinal=self.cardinal) if isinstance(result, list): return cls(*result, cardinal=self.cardinal) return result new_meth.__name__ = name new_meth.__doc__ = meth.__doc__ setattr(cls, name, new_meth) return cls @list_methods_to_pitches class Sequence: """Sequence, a representation of sequences of numbers either as absolute values or as "step" intervals, with a modulo value called "cardinal". Its methods return various characteristics of the sequence.""" #Stores values written by leaders and read by followers: master_dict = dict.fromkeys(('variety', 'open_scale', 'is_mirror', 'consonance', 'symmetry', 'closed_scale', 'z_depth', 'z_variety')) def __init__(self, *sequence, steps=False, cardinal=12): """doc""" if not all(isinstance(i, int) for i in sequence): raise ValueError('Arguments must be integers') if steps: #Sequence is input as consecutive intervals sequence = [sum(sequence[:n]) for n in range(len(sequence) + 1)] if sequence: pcs = sorted(set([i % cardinal for i in sequence])) steps = ([pcs[n] - pcs[n - 1] for n in range(1, len(pcs))] + [cardinal + pcs[0] - pcs[-1]]) p_steps, offset = min((steps[i:] + steps[:i], pcs[i % len(pcs)]) for i, j in enumerate(steps, 1) if j == max(steps)) else: p_steps, offset = [], 0 prime = [sum(p_steps[:n]) for n in range(len(p_steps))] self.order = [prime.index(j) + i * len(prime) for i, j in (divmod(i - offset, cardinal) for i in sequence)] self.__prime_steps = p_steps self.__cardinal = cardinal self.__offset = offset self.__prime = prime @property def cardinal(self): """doc""" return self.__cardinal @property def prime(self): """doc""" return self.__prime @property def prime_steps(self): """doc""" return self.__prime_steps @property def offset(self): """doc""" return self.__offset def __stepify(self, seq): """doc""" return [seq[n + 1] - seq[n] for n in range(len(seq) - 1)] #Contingent properties @property def pitches(self): """The actual notes""" return [self.prime[j] + i * self.cardinal + self.offset for i, j in (divmod(i, len(self.prime)) for i in self.order)] def pcset(self): """The pitch-class set""" return sorted ([(i + self.__offset) % self.cardinal for i in self.prime]) def norm(self): """The normal form""" pcs = self.pcset() mini = pcs[0] return [i - mini for i in pcs] def unique_pitches(self): """Unique sorted pitches""" return sorted(set(self)) def full_steps(self): """Unique sorted pitches""" if self: span = max(self) - min (self) octs = (span // self.cardinal + 1) * self.cardinal return self.steps() + [octs - span] return [] def steps(self): """Intervals between the unique sorted pitches""" return self.__stepify(self.unique_pitches()) def pcsteps(self): """The intervals in the normal form""" return self.__stepify(self.pcset()) def all_steps(self): """All intervals up and down the pitches""" return self.__stepify(self) def is_prime(self): """doc""" return self == self.prime def __compute_once(meth): """Inherent attributes should ony be computed once""" att = '__' + meth.__name__ def compute_once(self): """Only compute attribute if it is not present""" if not hasattr(self, att): setattr(self, att, meth(self)) return getattr(self, att) return compute_once #Inherent properties @property @__compute_once def variety(self): """How many different step sizes""" return len(set(self.prime_steps)) @property @__compute_once def open_scale(self): """No consecutive semitones""" prime = self.prime_steps return not any(prime[i] == prime[i - 1] == 1 for i in range(len(prime))) @property @__compute_once def closed_scale(self): """Like open_scale but also has no room for more notes""" prime = self.prime_steps length = len(prime) return self.open_scale and not any(prime[i] > 3 or (prime[i] == 3 and (prime[i - 1] > 1 or prime[(i + 1) % length] > 1)) for i in range(length)) @property @__compute_once def inv_prime(self): """doc""" if self: spets = list(reversed(self.prime_steps)) maxi = max(spets) rotations = (spets[i:] + spets[:i] for i in range(len(self.prime))) return min(i for i in rotations if i[-1] == maxi) return [] @property @__compute_once def antiprime_steps(self): """doc""" if self: steps = self.prime_steps return max(steps[i:] + steps[:i] for i, j in enumerate(steps, 1) if j == min(steps)) return [] @property @__compute_once def antiprime(self): """doc""" return [sum(self.antiprime_steps[:n]) for n in range(len(self.prime))] @property @__compute_once def is_forte(self): """doc""" return self.prime_steps <= self.inv_prime @property @__compute_once def is_mirror(self): """doc""" return self.prime_steps == self.inv_prime @property @__compute_once def symmetry(self): """How many symmetries in the step structure""" ps = self.prime_steps return [ps[n:] + ps[:n] for n in range(len(ps))].count(ps) - 1 #Z-vector properies @property @__compute_once def vector(self): """Returns the Z-vector of the pc-set""" pcs = self.prime cardinal = self.cardinal z_vector = [0] * (cardinal // 2) for i, j in enumerate(pcs): for k in pcs[ i + 1:]: interval = k - j z_vector[min(interval, cardinal - interval) - 1] += 1 return tuple(z_vector) @property @__compute_once def z_variety(self): """How many different interval types""" return len([i for i in self.vector if i]) @property @__compute_once def z_depth(self): """How many different interval counts""" return len(set(i for i in self.vector if i)) @property @__compute_once def consonance(self): """A measure of consonance""" vector = self.vector return sum(vector[2:5]) - sum(vector[:2]) - vector[-1] #actions: def transpose(self, num): """Increment all values by the same amount""" octinc, self.__offset = divmod(num + self.__offset, self.cardinal) self.order = [i + octinc * len(self.prime) for i in self.order] def voice(self, index, num): """Raise or lower an element by a number of octaves""" self.order[index] += num * len(self.prime) def sort(self, **kwargs): """Sort the pitches""" self.order.sort(**kwargs) def reverse(self): """Reverse the order of pitches""" self.order.reverse() def emult(self, n): """Multiply all elements by an integer""" self.__init__(*[i * n for i in self], cardinal=self.cardinal) def inverted(self): """Reverses the order of the intervals, starting from the same note.""" if self: s = self.__class__(*reversed(self.all_steps()), cardinal=self.cardinal, steps=True) s.transpose(min(self)) return s return [] class SeqGen(): """doc""" def __init__(self, optdict, moddict): """doc""" self.optdict = optdict self.moddict = moddict self._gen = self._squiterate() self.counter = {} def __repr__(self, lvl=0): string = '\n' + str(self.__class__.__name__) + ': ' lvl += 1 for arg in self.optdict, self.moddict: string += '\n' + lvl * ' ' if arg: for k, v in arg.items(): if isinstance(v, self.__class__): string += k.__name__ + ': ' + v.__repr__(lvl) else: string += str(arg) else: string += 'defaults' return string def seq(self): "Number sequences to iterate through. Repeatable." seq = self.optdict.get(self.__class__.seq) if seq: return iter([next(seq)]) def generator(self, cardinal): """Which number generators to use. Currently 'full', 'combs', or 'randgen'. Controllable.""" gengen = self.optdict.get(self.__class__.generator) if gengen: gens = [i for i in dir(seqgens) if i[0] != '_'] gen = gens[next(gengen) % len (gens)] return getattr(seqgens, gen)(cardinal) else: return self.seq() or seqgens.full(cardinal) def cardinal(self): "The number (of notes/beats) to be processed." cardgen = self.optdict.get(self.__class__.cardinal) if cardgen: for _ in range(100): cardinal = next(cardgen) if cardinal: return cardinal else: raise StopIteration else: return 12 def mortal(self): "Generator does not loop but dies when complete. Boolean." mort = self.optdict.get(self.__class__.mortal) return mort and next(mort) def repeat(self): "How many times to repeat sequence." rep = self.optdict.get(self.__class__.repeat) return rep and next(rep) or 1 def _modhandle(self, seq): """Ensure the required number of sequences have been yielded been applications of a filter or modification """ counter = self.counter for func, gen in self.moddict.items(): if func not in counter: counter[func] = [next(gen), 1] result = None if 'Boolean' in func.__doc__: if counter[func][1] >= counter[func][0]: result = func(seq) else: result = func(seq, counter[func][0]) if result is False: break else: for func, gen in self.moddict.items(): value = counter[func] if 'Boolean' in func.__doc__: if value[1] >= value[0]: value[1] = 0 value[0] = next(gen) value[1] += 1 else: value[0] = next(gen) return True def _squiterate(self): """doc""" empty = 0 while True: cardinal = self.cardinal() for seqlist in self.generator(cardinal): seq = Sequence(*seqlist, cardinal=cardinal) if self._modhandle(seq): empty = 0 for _ in range(self.repeat()): yield seq if self.mortal(): break empty += 1 if empty == 100: print("""One hundred consecutive empty generators: broaden filters.""") def __next__(self): """doc""" return next(self._gen) def __iter__(self): """doc""" return self class Event(): """Events produced within phrase_builder""" def __init__(self, pitches=[0], start=0, playtime=None, duration=1, volume=10, tempo=250, octave=12, instrument=0, pedal=False): self.__dict__.update({k: v for k, v in vars().items() if k != 'self'}) def __repr__(self): return str(vars(self)) def __len__(self): return len(self.pitches) class PhraseMaker(): """Phrase building options""" def __init__(self, gendict): self.gendict = gendict self._gen = self._phriterate() self.length = None self.boolcounter = {} def __repr__(self): string = str(self.__class__.__name__) + ':' if self.gendict: for k, v in self.gendict.items(): string += '\n ' + k.__name__ + ': ' + v.__repr__() return string else: return string + '\n defaults' def _boolcount(meth): """Return True if we have yeilded the currently required number of Phrases""" def wrapper(self): key = getattr(self.__class__, meth.__name__) if key in self.gendict: if key not in self.boolcounter: self.boolcounter[key] = [next(self.gendict[key]), 0] self.boolcounter[key][1] += 1 if self.boolcounter[key][0] == self.boolcounter[key][1]: self.boolcounter[key] = [next(self.gendict[key]), 0] return meth(self) wrapper.__name__ = meth.__name__ wrapper.__doc__ = meth.__doc__ return wrapper def _set_len(self, key): """The first parameter's value sets the length of the others; the parameters are returned""" gen = self.gendict[key] result = next(gen) if self.length: while len(result) < self.length: result += next(gen) else: self.length = len(result) return result def pitches(self): """Space-separated number sequence giving the pitch of each note. Repeatable.""" key = self.pitches.__func__ if self.chord(): if self.length: return [next(self.gendict[key]) for i in range(self.length)] self.length = 1 return [next(self.gendict[key])] return [[i] for i in self._set_len(key)] def duration(self): """Step values determining the duration of each note.""" return self._set_len(self.duration.__func__) def playtime(self): """Step values determining how long to hold each note.""" return self._set_len(self.playtime.__func__) def staccato(self): """How many twelfths of the duration to hold note for. Repeatable.""" key = self.staccato.__func__ if key in self.gendict: return [i / 12 for i in next(self.gendict[key])] return [1] def volume(self): "Volume, 0-10. Default 10. Repeatable." return self._set_len(self.volume.__func__) def tempo(self): "Tempo in BPM." for _ in range(100): tempo = next(self.gendict[self.tempo.__func__]) if tempo: return [15000.0 // tempo] raise StopIteration def octave(self): "The number to divide the octave by." for _ in range(100): octave = next(self.gendict[self.octave.__func__]) if octave: return [octave] raise StopIteration @_boolcount def chord(self): "Play sequences as chords. Boolean." return True @_boolcount def pedal(self): "Let pitches ring out. Boolean." return [True] def instrument(self): "MIDI program number." return [next(self.gendict[self.instrument.__func__ ])] def __iter__(self): return self def __next__(self): return next(self._gen) def _phriterate(self): "Generate phrases" start = 0 eventvars = Event.__init__.__code__.co_varnames while True: eventdict = {func.__name__: func(self) for func in self.gendict if func.__name__ in eventvars} phrase = [Event(**{k: val[i % len(val)] for k, val in eventdict.items()}) for i in range(self.length or 1)] staccatos = self.staccato() for i, event in enumerate(phrase): event.start = start start += event.duration * event.tempo event.playtime = ((event.playtime or event.duration) * staccatos[i % len(staccatos)]) self.length = None yield phrase class IterNode(): "Iterator wrapper to give access to argument" def __init__(self, genfunc, *args): self.args = args self.genfunc = genfunc self.gen = genfunc(*args) self.__name__= genfunc.__name__ def __iter__(self): return self def __next__(self): "doc" return next(self.gen) def __repr__(self, lvl=0): string = str(self.genfunc.__name__) + ': ' lvl += 1 for arg in self.args : string += '\n' + lvl * ' ' if isinstance(arg, dict): if arg: for k, val in arg.items(): string += k.__name__ + ': ' + val.__repr__(lvl) else: string += 'defaults' else: string += str(arg) return string def deepdate(self, other): "Deep-update a nested IterNode" if self.genfunc.__name__ != other.genfunc.__name__: self.__init__(other.genfunc, *other.args) else: for selfarg, otherarg in zip(self.args, other.args): if (not all(isinstance(i, dict) for i in (selfarg, otherarg)) and selfarg != otherarg): self.__init__(other.genfunc, *other.args) break elif selfarg != otherarg: for k in selfarg: if k not in otherarg: del selfarg[k] for k, val in otherarg.items(): if k in selfarg: selfarg[k] = selfarg.pop(k) selfarg[k].deepdate(val) else: selfarg[k] = val