import random from collections import defaultdict from functools import cached_property from constants import NOTES, RESET, REVERSE, RED, MAGENTA, GREEN, YELLOW, BOLD, WHITE from audio_mixin import AudioMixin from display_mixin import DisplayMixin from settings_mixin import Settings, SettingsMixin from input_mixin import InputMixin class ToneRow(AudioMixin, DisplayMixin, SettingsMixin, InputMixin): def __init__(self): AudioMixin.__init__(self) self.bpm = Settings.BPM self.pr_map = {} self.pr_map_populated = False self.inv_map = {} self.inv_map_populated = False self.current_seed = None self.use_current_seed = False self.current_rhythm_seed = None self.use_current_rhythm_seed = False self.current_octave_seed = None self.use_current_octave_seed = False self.rhythm_to_divison_map = { 0: self.whole, 1: self.half, 2: self.quarter, 3: self.eigth } self.generate() self.load_settings() @cached_property def octaves(self): return [str(Settings.OCTAVE)] * 12 @property def eigth(self): return self.quarter / 2 @property def quarter(self): return 60.0 / self.bpm @property def half(self): return self.quarter * 2 @property def whole(self): return self.half * 2 @cached_property def tone_row(self): if not self.use_current_seed: self.current_seed = random.randint(100000, 999999) else: self.use_current_seed = False random.seed(self.current_seed) notes = NOTES.copy() random.shuffle(notes) random.seed(None) return notes @cached_property def reordered_notes(self): first_note_idx = NOTES.index(self.tone_row[0]) notes = NOTES.copy() return notes[first_note_idx:] + notes[0:first_note_idx] @cached_property def prime(self): prime = [] for tone in self.tone_row: prime.append(self.reordered_notes.index(tone)) return prime @cached_property def inversion(self): inversion = [] for note in self.prime: inversion.append((2 * self.prime[0] - note) % 12) return inversion @cached_property def matrix(self): matrix = [] for shift in self.inversion: row = [] for item in self.prime: x = (item + shift) % 12 row.append(x) matrix.append(row) return matrix @cached_property def string_matrix(self): smatrix = [] for row in self.matrix: new_row = [] for tone in row: new_row.append(self.reordered_notes[tone]) smatrix.append(new_row) return smatrix @cached_property def rhythm_row(self): if not self.use_current_rhythm_seed: self.current_rhythm_seed = random.randint(100000, 999999) else: self.use_current_rhythm_seed = False random.seed(self.current_rhythm_seed) roptions = [0, 1, 2, 3] random.shuffle(roptions) random.seed(None) return roptions * 3 @cached_property def rhythm_map(self): prime = self.string_matrix[0] rmap = defaultdict(int) for r, t in zip(self.rhythm_row, prime): rmap[t] = r return rmap @cached_property def octave_row(self): if not self.use_current_octave_seed: self.current_octave_seed = random.randint(100000, 999999) else: self.use_current_octave_seed = False random.seed(self.current_octave_seed) octave_options = [Settings.OCTAVE] for i in range(Settings.OCTAVE_RANDOMIZATION): positive = Settings.OCTAVE + (i + 1) if positive > 5: positive = 5 if positive not in octave_options: octave_options.append(positive) negative = Settings.OCTAVE - (i + 1) if negative < 1: negative = 1 if negative not in octave_options: octave_options.append(negative) random.shuffle(octave_options) octave_row = [] left = 0 while len(octave_row) < 12: octave_row.append(octave_options[left]) left += 1 if left > len(octave_options) - 1: left = 0 random.seed(None) return octave_row @cached_property def octave_map(self): prime = self.string_matrix[0] omap = defaultdict(int) for o, t in zip(self.octave_row, prime): omap[t] = o return omap def regenerate(self): del self.tone_row del self.reordered_notes del self.prime del self.inversion del self.matrix del self.string_matrix del self.octaves del self.rhythm_row del self.rhythm_map del self.octave_row del self.octave_map self.pr_map_populated = False self.inv_map_populated = False self.generate() def generate(self): _ = self.tone_row _ = self.reordered_notes _ = self.prime _ = self.inversion _ = self.matrix _ = self.string_matrix _ = self.octaves _ = self.rhythm_row _ = self.rhythm_map _ = self.octave_row _ = self.octave_map def play_row(self, row): division_map = { 3: self.eigth, 2: self.quarter, 1: self.half, 0: self.whole } division = division_map[Settings.DEFAULT_DIVISION] for idx, note in enumerate(row): padded_tone_row = [f'{BOLD}{REVERSE}{MAGENTA}TONE'] padded_rhythm_row = [f'{BOLD}{REVERSE}{YELLOW}RTHM'] padded_octave_row = [f'{BOLD}{REVERSE}{GREEN}OCTV'] for idx2, x in enumerate(row): if idx == idx2: padded_tone_row.append(f"{REVERSE}{WHITE}{x.ljust(3)}{RESET}{REVERSE}{MAGENTA}{BOLD}") padded_rhythm_row.append(f"{REVERSE}{WHITE}{str(self.rhythm_map[x]).ljust(3)}{RESET}{REVERSE}{YELLOW}{BOLD}") padded_octave_row.append(f"{REVERSE}{WHITE}{str(self.octave_map[x]).ljust(3)}{RESET}{REVERSE}{GREEN}{BOLD}") else: padded_tone_row.append(x.ljust(3)) padded_rhythm_row.append(str(self.rhythm_map[x]).ljust(3)) padded_octave_row.append(str(self.octave_map[x]).ljust(3)) self.clear_screen() self.display(play_mode=True) print(" | ".join(padded_tone_row) + f" {RESET}") print(" | ".join(padded_rhythm_row) + f" {RESET}") print(" | ".join(padded_octave_row) + f" {RESET}") if Settings.RHYTHM_COMPLEXITY == 0: self.play(note + str(self.octave_map[note]), division) if Settings.RHYTHM_COMPLEXITY in [1, 2]: rest = False if Settings.RHYTHM_COMPLEXITY == 2: dice_roll = random.randint(1, 100) if dice_roll <= Settings.REST_PROBABILITY: rest = True division = self.rhythm_to_divison_map[self.rhythm_map[note]] self.play(note + str(self.octave_map[note]), division, rest=rest) if Settings.RHYTHM_COMPLEXITY == 3: division = random.uniform(0.05, 1) self.play(note + str(self.octave_map[note]), division) if __name__ == "__main__": t = ToneRow() t.loop()