commit acc1df0870460e5b39e00914440b3266b1afe662 Author: Dominic DiTaranto Date: Sun Aug 9 00:10:59 2026 -0400 init commit diff --git a/assets/audio/SJO - Bassoon.sf2 b/assets/audio/SJO - Bassoon.sf2 new file mode 100644 index 0000000..b1e1bea Binary files /dev/null and b/assets/audio/SJO - Bassoon.sf2 differ diff --git a/assets/audio/SJO - Cello.sf2 b/assets/audio/SJO - Cello.sf2 new file mode 100644 index 0000000..c0de451 Binary files /dev/null and b/assets/audio/SJO - Cello.sf2 differ diff --git a/assets/audio/SJO - Choir Aahs.sf2 b/assets/audio/SJO - Choir Aahs.sf2 new file mode 100644 index 0000000..549127a Binary files /dev/null and b/assets/audio/SJO - Choir Aahs.sf2 differ diff --git a/assets/audio/SJO - Clarinet.sf2 b/assets/audio/SJO - Clarinet.sf2 new file mode 100644 index 0000000..9509445 Binary files /dev/null and b/assets/audio/SJO - Clarinet.sf2 differ diff --git a/assets/audio/SJO - Double Bass.sf2 b/assets/audio/SJO - Double Bass.sf2 new file mode 100644 index 0000000..62bc328 Binary files /dev/null and b/assets/audio/SJO - Double Bass.sf2 differ diff --git a/assets/audio/SJO - English Horn.sf2 b/assets/audio/SJO - English Horn.sf2 new file mode 100644 index 0000000..35fdf49 Binary files /dev/null and b/assets/audio/SJO - English Horn.sf2 differ diff --git a/assets/audio/SJO - Flute.sf2 b/assets/audio/SJO - Flute.sf2 new file mode 100644 index 0000000..dd22931 Binary files /dev/null and b/assets/audio/SJO - Flute.sf2 differ diff --git a/assets/audio/SJO - French Horn.sf2 b/assets/audio/SJO - French Horn.sf2 new file mode 100644 index 0000000..3a3f6d4 Binary files /dev/null and b/assets/audio/SJO - French Horn.sf2 differ diff --git a/assets/audio/SJO - Glockenspiel.sf2 b/assets/audio/SJO - Glockenspiel.sf2 new file mode 100644 index 0000000..c8ea9c2 Binary files /dev/null and b/assets/audio/SJO - Glockenspiel.sf2 differ diff --git a/assets/audio/SJO - Harp.sf2 b/assets/audio/SJO - Harp.sf2 new file mode 100644 index 0000000..cfb6fc0 Binary files /dev/null and b/assets/audio/SJO - Harp.sf2 differ diff --git a/assets/audio/SJO - Marimbas.sf2 b/assets/audio/SJO - Marimbas.sf2 new file mode 100644 index 0000000..37f96a3 Binary files /dev/null and b/assets/audio/SJO - Marimbas.sf2 differ diff --git a/assets/audio/SJO - Strings slow.sf2 b/assets/audio/SJO - Strings slow.sf2 new file mode 100644 index 0000000..65f2cae Binary files /dev/null and b/assets/audio/SJO - Strings slow.sf2 differ diff --git a/assets/audio/SJO - Strings.sf2 b/assets/audio/SJO - Strings.sf2 new file mode 100644 index 0000000..6c03279 Binary files /dev/null and b/assets/audio/SJO - Strings.sf2 differ diff --git a/assets/audio/SJO - Timpani.sf2 b/assets/audio/SJO - Timpani.sf2 new file mode 100644 index 0000000..1df1e20 Binary files /dev/null and b/assets/audio/SJO - Timpani.sf2 differ diff --git a/assets/audio/SJO - Trombone.sf2 b/assets/audio/SJO - Trombone.sf2 new file mode 100644 index 0000000..d404b68 Binary files /dev/null and b/assets/audio/SJO - Trombone.sf2 differ diff --git a/assets/audio/SJO - Trumpet - Velocity fix.sf2 b/assets/audio/SJO - Trumpet - Velocity fix.sf2 new file mode 100644 index 0000000..e945ee1 Binary files /dev/null and b/assets/audio/SJO - Trumpet - Velocity fix.sf2 differ diff --git a/assets/audio/SJO - Tuba.sf2 b/assets/audio/SJO - Tuba.sf2 new file mode 100644 index 0000000..06191dc Binary files /dev/null and b/assets/audio/SJO - Tuba.sf2 differ diff --git a/assets/audio/SJO - Vibes.sf2 b/assets/audio/SJO - Vibes.sf2 new file mode 100644 index 0000000..6a8b3a1 Binary files /dev/null and b/assets/audio/SJO - Vibes.sf2 differ diff --git a/assets/audio/SJO - Viola.sf2 b/assets/audio/SJO - Viola.sf2 new file mode 100644 index 0000000..6b32bbc Binary files /dev/null and b/assets/audio/SJO - Viola.sf2 differ diff --git a/assets/audio/SJO - Xylophone.sf2 b/assets/audio/SJO - Xylophone.sf2 new file mode 100644 index 0000000..b2f9467 Binary files /dev/null and b/assets/audio/SJO - Xylophone.sf2 differ diff --git a/assets/rhodes.sf2 b/assets/rhodes.sf2 new file mode 100644 index 0000000..ec62cde Binary files /dev/null and b/assets/rhodes.sf2 differ diff --git a/assets/settings.json b/assets/settings.json new file mode 100644 index 0000000..e7f199b --- /dev/null +++ b/assets/settings.json @@ -0,0 +1 @@ +{"BPM": 150, "INSTRUMENT": "Xylophone", "OCTAVE": 4, "OCTAVE_RANDOMIZATION": 0, "RHYTHM_COMPLEXITY": 0, "DEFAULT_DIVISION": 3, "REST_PROBABILITY": 0, "SAVE_SETTINGS": null, "SAVE_MATRIX": "oops", "LOAD_MATRIX": null} \ No newline at end of file diff --git a/audio_mixin.py b/audio_mixin.py new file mode 100644 index 0000000..1b19d5c --- /dev/null +++ b/audio_mixin.py @@ -0,0 +1,44 @@ +import time +import os + +import fluidsynth + +from constants import NOTE_TO_MIDI, GENERAL_MIDI_PROGRAMS, SOUNDFONTS +from settings_mixin import Settings + + +class AudioMixin: + def __init__(self): + self.fs = fluidsynth.Synth() + self.fs.start(driver="alsa" if os.name != "nt" else "dsound") + + self.sf_id = None + self.update_sf(SOUNDFONTS[Settings.INSTRUMENT]) + self.clear_screen() + + def update_sf(self, path): + if self.sf_id is not None and self.sf_id != -1: + self.fs.sfunload(self.sf_id) + sf2_path = os.path.join(os.path.dirname(__file__), "assets/audio", path) + + self.sf_id = self.fs.sfload(sf2_path) + self.fs.program_select(0, self.sf_id, 0, GENERAL_MIDI_PROGRAMS[Settings.INSTRUMENT]) + + def play(self, note_name, duration, rest=False): + if note_name not in NOTE_TO_MIDI: + return + + midi_num = NOTE_TO_MIDI[note_name] + channel = 0 + velocity = 100 + + if not rest: + self.fs.noteon(channel, midi_num, velocity) + + time.sleep(duration) + + if not rest: + self.fs.noteoff(channel, midi_num) + + def close(self): + self.fs.delete() diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..51314c7 --- /dev/null +++ b/constants.py @@ -0,0 +1,86 @@ +BPM = 180 +QUARTER = 60.0 / BPM +HALF = QUARTER * 2 + +NOTE_TO_FREQ = { + "C1": 32.70, "C#1": 34.65, "D1": 36.71, "D#1": 38.89, "E1": 41.20, "F1": 43.65, + "F#1": 46.25, "G1": 49.00, "G#1": 51.91, "A1": 55.00, "A#1": 58.27, "B1": 61.74, + "C2": 65.41, "C#2": 69.30, "D2": 73.42, "D#2": 77.78, "E2": 82.41, "F2": 87.31, + "F#2": 92.50, "G2": 98.00, "G#2": 103.83, "A2": 110.00, "A#2": 116.54, "B2": 123.47, + "C3": 130.81, "C#3": 138.59, "D3": 146.83, "D#3": 155.56, "E3": 164.81, "F3": 174.61, + "F#3": 185.00, "G3": 196.00, "G#3": 207.65, "A3": 220.00, "A#3": 233.08, "B3": 246.94, + "C4": 261.63, "C#4": 277.18, "D4": 293.66, "D#4": 311.13, "E4": 329.63, "F4": 349.23, + "F#4": 369.99, "G4": 392.00, "G#4": 415.30, "A4": 440.00, "A#4": 466.16, "B4": 493.88, + "C5": 523.25, "C#5": 554.37, "D5": 587.33, "D#5": 622.25, "E5": 659.25, "F5": 698.46, + "F#5": 739.99, "G5": 783.99, "G#5": 830.61, "A5": 880.00, "A#5": 932.33, "B5": 987.77 +} + +NOTE_TO_MIDI = { + "C1": 24, "C#1": 25, "D1": 26, "D#1": 27, "E1": 28, "F1": 29, "F#1": 30, "G1": 31, "G#1": 32, "A1": 33, "A#1": 34, "B1": 35, + "C2": 36, "C#2": 37, "D2": 38, "D#2": 39, "E2": 40, "F2": 41, "F#2": 42, "G2": 43, "G#2": 44, "A2": 45, "A#2": 46, "B2": 47, + "C3": 48, "C#3": 49, "D3": 50, "D#3": 51, "E3": 52, "F3": 53, "F#3": 54, "G3": 55, "G#3": 56, "A3": 57, "A#3": 58, "B3": 59, + "C4": 60, "C#4": 61, "D4": 62, "D#4": 63, "E4": 64, "F4": 65, "F#4": 66, "G4": 67, "G#4": 68, "A4": 69, "A#4": 70, "B4": 71, + "C5": 72, "C#5": 73, "D5": 74, "D#5": 75, "E5": 76, "F5": 77, "F#5": 78, "G5": 79, "G#5": 80, "A5": 81, "A#5": 82, "B5": 83 +} + +NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] + + +SOUNDFONTS = { + 'Bassoon': 'SJO - Bassoon.sf2', + 'Cello': 'SJO - Cello.sf2', + 'Choir Aahs': 'SJO - Choir Aahs.sf2', + 'Clarinet': 'SJO - Clarinet.sf2', + 'Double Bass': 'SJO - Double Bass.sf2', + 'English Horn': 'SJO - English Horn.sf2', + 'Flute': 'SJO - Flute.sf2', + 'French Horn': 'SJO - French Horn.sf2', + 'Glockenspiel': 'SJO - Glockenspiel.sf2', + 'Harp': 'SJO - Harp.sf2', + 'Marimbas': 'SJO - Marimbas.sf2', + 'Strings slow': 'SJO - Strings slow.sf2', + 'Strings': 'SJO - Strings.sf2', + 'Timpani': 'SJO - Timpani.sf2', + 'Trombone': 'SJO - Trombone.sf2', + 'Trumpet - Velocity fix': 'SJO - Trumpet - Velocity fix.sf2', + 'Tuba': 'SJO - Tuba.sf2', + 'Vibes': 'SJO - Vibes.sf2', + 'Viola': 'SJO - Viola.sf2', + 'Xylophone': 'SJO - Xylophone.sf2' +} + +GENERAL_MIDI_PROGRAMS = { + 'Glockenspiel': 9, + 'Vibes': 11, + 'Marimbas': 12, + 'Xylophone': 13, + 'Harp': 46, + 'Timpani': 47, + 'Strings': 48, + 'Strings slow': 49, + 'Viola': 41, + 'Cello': 42, + 'Double Bass': 43, + 'Choir Aahs': 52, + 'Trumpet - Velocity fix': 56, + 'Trombone': 57, + 'Tuba': 58, + 'French Horn': 60, + 'Flute': 73, + 'English Horn': 69, + 'Clarinet': 71, + 'Bassoon': 70 +} + + +RESET = "\033[0m" +BOLD = "\033[1m" +REVERSE = "\033[7m" +BLACK = "\033[30m" +RED = "\033[31m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +BLUE = "\033[34m" +MAGENTA = "\033[35m" +CYAN = "\033[36m" +WHITE = "\033[37m" diff --git a/display_mixin.py b/display_mixin.py new file mode 100644 index 0000000..6f12946 --- /dev/null +++ b/display_mixin.py @@ -0,0 +1,64 @@ +import sys +import subprocess +from time import sleep + +from constants import RESET, REVERSE, BOLD, GREEN, RED, BLUE, YELLOW, MAGENTA +from settings_mixin import Settings + + +class DisplayMixin: + + def message(self, msg, callback, sleep_time=1.5, type="ERROR"): + self.clear_screen() + + color = RED if type == "ERROR" else GREEN + print(f"{REVERSE}{color}{msg}{RESET}") + + sleep(sleep_time) + + self.clear_screen() + + callback() + + def clear_screen(self): + subprocess.run(['clear']) + + def display(self, play_mode=False): + print(f"{GREEN}============================== TWELVE TONE MATRIX ==============================={RESET}\n") + + print(f"{YELLOW}{Settings.BPM} BPM - OCTAVE {Settings.OCTAVE} - {Settings.INSTRUMENT} {RESET}") + print(f'{GREEN}{REVERSE} {BOLD}{" | ".join(f"I{i}".ljust(3) for i in self.inversion)} {RESET}') + + for row, prime in zip(self.string_matrix, self.prime): + padded_row = [note.ljust(3) for note in row] + pr_label = self.reordered_notes.index(row[0]) + + if not self.pr_map_populated: + self.pr_map[f"P{pr_label}"] = row + + print(f'{BOLD}{GREEN}{REVERSE}{f"P{pr_label}".ljust(3)}{RESET} | {" | ".join(padded_row)}| {BOLD}{GREEN}{REVERSE}{f"R{pr_label}".ljust(3)}{RESET}') + + self.pr_map_populated = True + + if not self.inv_map_populated: + for idx, i in enumerate(self.inversion): + new_row = [] + for idx2, row in enumerate(self.matrix): + new_row.append(self.reordered_notes[row[idx]]) + self.inv_map[f"I{i}"] = new_row + self.inv_map_populated = True + + print(f'{GREEN}{REVERSE} {BOLD}{" | ".join(f"i{i}".ljust(3) for i in self.inversion)} {RESET}\n') + + if not play_mode: + print(f"{MAGENTA}{BOLD}{REVERSE}TONE | {' | '.join([i.ljust(3) for i in self.tone_row])} {RESET}") + print(f"{YELLOW}{BOLD}{REVERSE}RTHM | {' | '.join([str(x).ljust(3) for x in self.rhythm_row])} {RESET}") + print(f"{GREEN}{BOLD}{REVERSE}OCTV | {' | '.join([str(x).ljust(3) for x in self.octave_row])} {RESET}") + + def loop(self): + self.clear_screen() + self.display() + + self.get_user_input() + + self.loop() diff --git a/input_mixin.py b/input_mixin.py new file mode 100644 index 0000000..c1105bc --- /dev/null +++ b/input_mixin.py @@ -0,0 +1,91 @@ +import sys + +from prompt_toolkit import prompt +from prompt_toolkit.formatted_text import HTML + + +class InputMixin: + + def get_user_input(self): + loop_count = 1 + + request = self.request(help_text='Type "help" for more options, "q" to quit') + self.check_valid_request(request) + + request = request.split(' ') + + if "loop" in request: + loop_count = self.parse_loop_request(request) + + if "seq" in request: + self.parse_seq_request(request, loop_count) + + if "debug" in request: + breakpoint() + + if request[0] == "x" or request[0] == "regen": + self.regenerate() + self.loop() + + elif request[0] == "q" or request[0] == "quit": + self.close() + sys.exit() + + elif request[0] == "s" or request[0] == "settings": + self.display_settings() + + elif request[0][0] in ["P", "R", "I", "i"]: + self.play_tone_row(request[0][0], request[0][1], loop=loop_count) + + def request(self, help_text): + return prompt( + "\n>>> ", placeholder=HTML(f'') + ) + + def check_valid_request(self, request): + if not request or request == "": + self.loop() + + def parse_loop_request(self, request): + loop_index = request.index("loop") + loop_count = request[loop_index + 1] + + if not loop_count.isdigit(): + self.message("Not a valid loop count", self.loop) + else: + loop_count = int(loop_count) + + request.pop(loop_index + 1) + request.pop(loop_index) + return loop_count + + def parse_seq_request(self, request, loop_count): + seq_index = request.index("seq") + sequence = request[seq_index + 1].split(',') + for seq in sequence: + if seq[0] not in ["P", "R", "I", "i"] or int(seq[1:]) > 11: + self.message(f"Invalid sequence at {seq}", self.loop) + + for loop in range(loop_count): + for seq in sequence: + self.play_tone_row(seq[0], seq[1:], loop=1) + + def play_tone_row(self, row_type, row_id, loop=1): + reverse = True if row_type in ["R", "i"] else False + + if row_id in [str(x) for x in range(0, 12)]: + + if not reverse: + if row_type == "I": + target_row = self.inv_map[row_type + row_id] + else: + target_row = self.pr_map[row_type + row_id] + else: + row_type = "P" if row_type == "R" else "I" + if row_type == "I": + target_row = self.inv_map[row_type + row_id][::-1] + else: + target_row = self.pr_map[row_type + row_id][::-1] + + for i in range(loop): + self.play_row(target_row) diff --git a/settings_mixin.py b/settings_mixin.py new file mode 100644 index 0000000..a8b1cd4 --- /dev/null +++ b/settings_mixin.py @@ -0,0 +1,204 @@ +import os +import json +from functools import cached_property +from dataclasses import dataclass, fields +from constants import RESET, REVERSE, SOUNDFONTS + +_INSTRUMENT_LIST = [f"[{str(idx).rjust(2)}] {name}" for idx, name in enumerate(SOUNDFONTS.keys())] + + +_SETTINGS_SAVE_FILE = 'assets/settings.json' + +_MATRIX_SAVE_DIR = os.path.join(os.path.dirname(__file__), "assets/saves") + + +def get_saved_matrix_list(): + if not os.path.exists(_MATRIX_SAVE_DIR): + return [" [No saved matrices found]"] + + save_files = [ + f for f in os.listdir(_MATRIX_SAVE_DIR) + if os.path.isfile(os.path.join(_MATRIX_SAVE_DIR, f)) and f.endswith(".sav") + ] + save_files.sort() + + if not save_files: + return [" [No saved matrices found]"] + + return [f"[{str(idx).rjust(2)}] {name}" for idx, name in enumerate(save_files)] + + +_SAVED_MATRIX_LIST = get_saved_matrix_list() + + +@dataclass +class Settings: + BPM: int = 140 + INSTRUMENT: str = "Bassoon" + OCTAVE: int = 3 + OCTAVE_RANDOMIZATION: int = 0 + RHYTHM_COMPLEXITY: int = 0 + DEFAULT_DIVISION: int = 2 + REST_PROBABILITY: int = 0 + SAVE_SETTINGS: None = None + SAVE_MATRIX: None = None + LOAD_MATRIX: None = None + + @classmethod + def max_len(cls): + return max(len(f.name) for f in fields(cls)) + 2 + + +@dataclass +class SettingsHelp: + BPM: str = "Enter a BPM between 30 - 300" + INSTRUMENT: str = f"Select Soundfont:\n\n{'\n'.join(_INSTRUMENT_LIST)}" + OCTAVE: str = "Change the octave. Select an octave between 1 and 5" + OCTAVE_RANDOMIZATION: int = 0 + RHYTHM_COMPLEXITY: str = "\n[0] Off, All Eigth Notes\n[1] On, Engage Rhythm Matrix\n[2] On, Rhythm Matrix and Rest Randomization\n[3] On, Chaos Mode, A random duration is chosen between 0.05 seconds and 1 second" + DEFAULT_DIVISION: str = "\n[0] Whole Note\n[1] Half Note\n[2] Quarter Note\n[3] Eight Note" + REST_PROBABILITY: str = "Probability in which a note in row will be replaced by a rest.\nEnter a value between 0 and 100, where 0 is absolutely no chance of a rest and 100 is all rests.\nThis only works in RHYTHM_COMPLEXITY MODE 2." + SAVE_SETTINGS: str = "Save current settings to file" + SAVE_MATRIX: str = "Save current matrix to file, Enter name of matrix to save" + LOAD_MATRIX: str = f"Select Saved Matrix to Load:\n\n{'\n'.join(_SAVED_MATRIX_LIST)}" + EXIT: None = None + + +SETTINGS_RULES = { + "BPM": {"type": int, "min": 30, "max": 300}, + "OCTAVE": {"type": int, "min": 1, "max": 5}, + "OCTAVE_RANDOMIZATION": {"type": int, "min": 0, "max": 4}, + "RHYTHM_COMPLEXITY": {"type": int, "min": 0, "max": 3}, + "DEFAULT_DIVISION": {"type": int, "min": 0, "max": 3}, + "REST_PROBABILITY": {"type": int, "min": 0, "max": 100}, + "INSTRUMENT": {"type": int, "min": 0, "max": len(SOUNDFONTS)}, + "SAVE_MATRIX": {"type": str, "min": 0, "max": 10}, + "LOAD_MATRIX": {"type": str, "min": 0, "max": 100}, + +} + + +class SettingsMixin: + + @cached_property + def settings(self): + return [f.name for f in fields(Settings)] + + def save_settings(self): + setting_obj = {} + + for field in fields(Settings): + setting_obj[field.name] = getattr(Settings, field.name) + + with open(_SETTINGS_SAVE_FILE, "w+") as f: + json.dump(setting_obj, f) + + def load_settings(self): + with open(_SETTINGS_SAVE_FILE, "r") as f: + data = json.load(f) + for k, v in data.items(): + setattr(Settings, k, int(v) if str(v).isdigit() else v) + + self.update_sf(SOUNDFONTS[Settings.INSTRUMENT]) + self.bpm = int(Settings.BPM) + + def display_settings(self): + self.clear_screen() + print(f"{REVERSE} SETTINGS: {RESET}\n") + for idx, setting in enumerate(self.settings): + value = getattr(Settings, setting) + print(f"[{idx}] {setting.ljust(Settings.max_len())}{": " + str(value) if value != None else ''}") + + self.user_input() + + self.display_settings() + + def user_input(self, setting_name=None): + request = self.request('Enter ID of setting you want to change, "q" to exit') + + if request[0] == "q": + self.loop() + + request = self.validate_request(request) + + setting_name = self.settings[request] + + if setting_name == "SAVE_SETTINGS": + self.save_settings() + self.message("Settings Saved..", self.display_settings, type="SUCCESS") + + self.display_setting(setting_name) + + value = self.request("Update setting by entering value") + + value = self.validate_new_setting(setting_name, value) + + setattr(Settings, setting_name, value) + + if setting_name == "LOAD_MATRIX": + self.handle_load_matrix() + + if setting_name == "SAVE_MATRIX": + self.handle_save_matrix() + + if setting_name == "BPM": + self.bpm = int(Settings.BPM) + + if setting_name == "INSTRUMENT": + instrument = list(SOUNDFONTS.keys())[value] + setattr(Settings, setting_name, instrument) + self.update_sf(SOUNDFONTS[instrument]) + + if setting_name == "OCTAVE_RANDOMIZATION": + del self.octave_row + del self.octave_map + _ = self.octave_row + _ = self.octave_map + + if setting_name == "OCTAVE": + del self.octave_row + del self.octave_map + _ = self.octave_row + _ = self.octave_map + + def validate_request(self, request): + try: + request = int(request) + if request >= len(self.settings): + raise ValueError + return request + except ValueError: + self.message(f"'{request}' is not a valid input, try again!", self.display_settings) + + def display_setting(self, setting_name): + self.clear_screen() + print(f"{REVERSE}{setting_name.ljust(Settings.max_len())} CURRENT VALUE: {getattr(Settings, setting_name)} {RESET}\n") + print(f"INFO: {getattr(SettingsHelp, setting_name)}\n") + + def validate_new_setting(self, setting_name, value): + try: + if SETTINGS_RULES[setting_name]['type'] == int: + value = int(value) + if value >= SETTINGS_RULES[setting_name]['min'] and value <= SETTINGS_RULES[setting_name]['max']: + pass + else: + raise ValueError + return value + else: + return value + except ValueError: + self.message(f"'{value}' is not a valid input, try again!", self.display_settings) + + def handle_load_matrix(self): + file_name = " ".join(_SAVED_MATRIX_LIST[int(Settings.LOAD_MATRIX)].split("] ")[1:]) + with open(f"assets/saves/{file_name}", 'r') as f: + seed = f.readline() + + self.current_seed = int(seed) + self.regenerate() + self.use_current_seed = True + + def handle_save_matrix(self): + Settings.SAVE_MATRIX = Settings.SAVE_MATRIX.strip().replace(".", " ").replace(" ", "_") + with open(f"assets/saves/{Settings.SAVE_MATRIX}.sav", 'w+') as f: + f.write(str(self.current_seed)) diff --git a/tonerow.py b/tonerow.py new file mode 100644 index 0000000..1af9fa3 --- /dev/null +++ b/tonerow.py @@ -0,0 +1,270 @@ +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()