init commit
This commit is contained in:
commit
acc1df0870
28 changed files with 760 additions and 0 deletions
BIN
assets/audio/SJO - Bassoon.sf2
Normal file
BIN
assets/audio/SJO - Bassoon.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Cello.sf2
Normal file
BIN
assets/audio/SJO - Cello.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Choir Aahs.sf2
Normal file
BIN
assets/audio/SJO - Choir Aahs.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Clarinet.sf2
Normal file
BIN
assets/audio/SJO - Clarinet.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Double Bass.sf2
Normal file
BIN
assets/audio/SJO - Double Bass.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - English Horn.sf2
Normal file
BIN
assets/audio/SJO - English Horn.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Flute.sf2
Normal file
BIN
assets/audio/SJO - Flute.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - French Horn.sf2
Normal file
BIN
assets/audio/SJO - French Horn.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Glockenspiel.sf2
Normal file
BIN
assets/audio/SJO - Glockenspiel.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Harp.sf2
Normal file
BIN
assets/audio/SJO - Harp.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Marimbas.sf2
Normal file
BIN
assets/audio/SJO - Marimbas.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Strings slow.sf2
Normal file
BIN
assets/audio/SJO - Strings slow.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Strings.sf2
Normal file
BIN
assets/audio/SJO - Strings.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Timpani.sf2
Normal file
BIN
assets/audio/SJO - Timpani.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Trombone.sf2
Normal file
BIN
assets/audio/SJO - Trombone.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Trumpet - Velocity fix.sf2
Normal file
BIN
assets/audio/SJO - Trumpet - Velocity fix.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Tuba.sf2
Normal file
BIN
assets/audio/SJO - Tuba.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Vibes.sf2
Normal file
BIN
assets/audio/SJO - Vibes.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Viola.sf2
Normal file
BIN
assets/audio/SJO - Viola.sf2
Normal file
Binary file not shown.
BIN
assets/audio/SJO - Xylophone.sf2
Normal file
BIN
assets/audio/SJO - Xylophone.sf2
Normal file
Binary file not shown.
BIN
assets/rhodes.sf2
Normal file
BIN
assets/rhodes.sf2
Normal file
Binary file not shown.
1
assets/settings.json
Normal file
1
assets/settings.json
Normal file
|
|
@ -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}
|
||||||
44
audio_mixin.py
Normal file
44
audio_mixin.py
Normal file
|
|
@ -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()
|
||||||
86
constants.py
Normal file
86
constants.py
Normal file
|
|
@ -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"
|
||||||
64
display_mixin.py
Normal file
64
display_mixin.py
Normal file
|
|
@ -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()
|
||||||
91
input_mixin.py
Normal file
91
input_mixin.py
Normal file
|
|
@ -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'<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)
|
||||||
204
settings_mixin.py
Normal file
204
settings_mixin.py
Normal file
|
|
@ -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))
|
||||||
270
tonerow.py
Normal file
270
tonerow.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Reference in a new issue