diff --git a/PKGBUILD b/PKGBUILD index 7cfe366..41c0436 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Dominic DiTaranto pkgname=atonatui _name=atonatui -pkgver=0.1.1 +pkgver=0.1.2 pkgrel=1 pkgdesc="12 Tone Matrix Generator, Looper, Sequencer." arch=('any') diff --git a/README.md b/README.md index d4bdf14..f40f7c9 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Features: ## Installation Guide +PIP: `pipx install atonatui` +ARCH: `yay -S python-atonatui` + ## Main Page The main page consists of a 12 Tone Matrix. @@ -81,3 +84,10 @@ __Advanced Commands:__ - Save the current tone matrix to a file, you can manually load this later - LOAD_MATRIX - Load a previously saved tone matrix + +# Upcoming Features +- Support independent generation, saving, and loading for rhythm and octave rows +- Support loading of custom soundfonts +- Support custom input of Tone Row + Rhythm and Octave rows +- Output rows to midi +- Up arrow functionality so users can rerun commands easily diff --git a/assets/audio/SJO - Bassoon.sf2 b/assets/audio/SJO - Bassoon.sf2 deleted file mode 100644 index b1e1bea..0000000 Binary files a/assets/audio/SJO - Bassoon.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Cello.sf2 b/assets/audio/SJO - Cello.sf2 deleted file mode 100644 index c0de451..0000000 Binary files a/assets/audio/SJO - Cello.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Choir Aahs.sf2 b/assets/audio/SJO - Choir Aahs.sf2 deleted file mode 100644 index 549127a..0000000 Binary files a/assets/audio/SJO - Choir Aahs.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Clarinet.sf2 b/assets/audio/SJO - Clarinet.sf2 deleted file mode 100644 index 9509445..0000000 Binary files a/assets/audio/SJO - Clarinet.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Double Bass.sf2 b/assets/audio/SJO - Double Bass.sf2 deleted file mode 100644 index 62bc328..0000000 Binary files a/assets/audio/SJO - Double Bass.sf2 and /dev/null differ diff --git a/assets/audio/SJO - English Horn.sf2 b/assets/audio/SJO - English Horn.sf2 deleted file mode 100644 index 35fdf49..0000000 Binary files a/assets/audio/SJO - English Horn.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Flute.sf2 b/assets/audio/SJO - Flute.sf2 deleted file mode 100644 index dd22931..0000000 Binary files a/assets/audio/SJO - Flute.sf2 and /dev/null differ diff --git a/assets/audio/SJO - French Horn.sf2 b/assets/audio/SJO - French Horn.sf2 deleted file mode 100644 index 3a3f6d4..0000000 Binary files a/assets/audio/SJO - French Horn.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Glockenspiel.sf2 b/assets/audio/SJO - Glockenspiel.sf2 deleted file mode 100644 index c8ea9c2..0000000 Binary files a/assets/audio/SJO - Glockenspiel.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Harp.sf2 b/assets/audio/SJO - Harp.sf2 deleted file mode 100644 index cfb6fc0..0000000 Binary files a/assets/audio/SJO - Harp.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Marimbas.sf2 b/assets/audio/SJO - Marimbas.sf2 deleted file mode 100644 index 37f96a3..0000000 Binary files a/assets/audio/SJO - Marimbas.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Strings slow.sf2 b/assets/audio/SJO - Strings slow.sf2 deleted file mode 100644 index 65f2cae..0000000 Binary files a/assets/audio/SJO - Strings slow.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Strings.sf2 b/assets/audio/SJO - Strings.sf2 deleted file mode 100644 index 6c03279..0000000 Binary files a/assets/audio/SJO - Strings.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Timpani.sf2 b/assets/audio/SJO - Timpani.sf2 deleted file mode 100644 index 1df1e20..0000000 Binary files a/assets/audio/SJO - Timpani.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Trombone.sf2 b/assets/audio/SJO - Trombone.sf2 deleted file mode 100644 index d404b68..0000000 Binary files a/assets/audio/SJO - Trombone.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Trumpet - Velocity fix.sf2 b/assets/audio/SJO - Trumpet - Velocity fix.sf2 deleted file mode 100644 index e945ee1..0000000 Binary files a/assets/audio/SJO - Trumpet - Velocity fix.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Tuba.sf2 b/assets/audio/SJO - Tuba.sf2 deleted file mode 100644 index 06191dc..0000000 Binary files a/assets/audio/SJO - Tuba.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Vibes.sf2 b/assets/audio/SJO - Vibes.sf2 deleted file mode 100644 index 6a8b3a1..0000000 Binary files a/assets/audio/SJO - Vibes.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Viola.sf2 b/assets/audio/SJO - Viola.sf2 deleted file mode 100644 index 6b32bbc..0000000 Binary files a/assets/audio/SJO - Viola.sf2 and /dev/null differ diff --git a/assets/audio/SJO - Xylophone.sf2 b/assets/audio/SJO - Xylophone.sf2 deleted file mode 100644 index b2f9467..0000000 Binary files a/assets/audio/SJO - Xylophone.sf2 and /dev/null differ diff --git a/assets/rhodes.sf2 b/assets/rhodes.sf2 deleted file mode 100644 index ec62cde..0000000 Binary files a/assets/rhodes.sf2 and /dev/null differ diff --git a/assets/settings.json b/assets/settings.json deleted file mode 100644 index e7f199b..0000000 --- a/assets/settings.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 1b19d5c..0000000 --- a/audio_mixin.py +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 51314c7..0000000 --- a/constants.py +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 6f12946..0000000 --- a/display_mixin.py +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index c1105bc..0000000 --- a/input_mixin.py +++ /dev/null @@ -1,91 +0,0 @@ -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/pyproject.toml b/pyproject.toml index 19863c8..ef17842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "atonatui" -version = "0.1.1" +version = "0.1.2" description = "12 Tone Matrix Generator, Looper, Sequencer." readme = "README.md" requires-python = ">=3.10" diff --git a/settings_mixin.py b/settings_mixin.py deleted file mode 100644 index a8b1cd4..0000000 --- a/settings_mixin.py +++ /dev/null @@ -1,204 +0,0 @@ -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 deleted file mode 100644 index 201b001..0000000 --- a/tonerow.py +++ /dev/null @@ -1,269 +0,0 @@ -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()