packaging

This commit is contained in:
Dominic DiTaranto 2026-08-09 21:09:55 -04:00
parent d2b54ae907
commit 51d53c38ab
31 changed files with 12 additions and 761 deletions

View file

@ -1,7 +1,7 @@
# Maintainer: Dominic DiTaranto <me@domdit.com>
pkgname=atonatui
_name=atonatui
pkgver=0.1.1
pkgver=0.1.2
pkgrel=1
pkgdesc="12 Tone Matrix Generator, Looper, Sequencer."
arch=('any')

View file

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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}

View file

@ -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()

View file

@ -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"

View file

@ -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()

View file

@ -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'<style fg="#888888">{help_text}</style>')
)
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)

View file

@ -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"

View file

@ -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))

View file

@ -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()