diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..03c945e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2017 Dylan Araps + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..48724b4 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +graft src/atonatui/assets +global-exclude *.py[cod] diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..7cfe366 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,25 @@ +# Maintainer: Dominic DiTaranto +pkgname=atonatui +_name=atonatui +pkgver=0.1.1 +pkgrel=1 +pkgdesc="12 Tone Matrix Generator, Looper, Sequencer." +arch=('any') +url="https://git.domdit.com/dominic/atonatui" +license=('MIT') +depends=('python') +makedepends=('python-build' 'python-installer' 'python-wheel' 'python-setuptools') +source=("https://pythonhosted.org{_name::1}/$_name/$_name-$pkgver.tar.gz") +sha256sums=('SKIP') + +build() { + cd "$_name-$pkgver" + python -m build --wheel --no-isolation +} + +package() { + cd "$_name-$pkgver" + python -m installer --destdir="$pkgdir" dist/*.whl + install -Dm644 LICENSE.md "$pkgdir/usr/share/licenses/$pkgname/LICENSE.md" +} + diff --git a/dist/atonatui-0.1.1-py3-none-any.whl b/dist/atonatui-0.1.1-py3-none-any.whl new file mode 100644 index 0000000..cb38e18 Binary files /dev/null and b/dist/atonatui-0.1.1-py3-none-any.whl differ diff --git a/dist/atonatui-0.1.1.tar.gz b/dist/atonatui-0.1.1.tar.gz new file mode 100644 index 0000000..4a46efd Binary files /dev/null and b/dist/atonatui-0.1.1.tar.gz differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..19863c8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["setuptools>=61.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "atonatui" +version = "0.1.1" +description = "12 Tone Matrix Generator, Looper, Sequencer." +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "Dominic DiTaranto", email = "me@domdit.com" } +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Environment :: Console", + "Topic :: Multimedia :: Sound/Audio :: Analysis" +] + +dependencies = [ + "numpy==2.5.1", + "prompt_toolkit==3.0.53", + "pyfluidsynth==1.4.0", + "wcwidth==0.8.2" +] + +[project.urls] +Homepage = "https://git.domdit.com/dominic/atonatui" + +[project.scripts] +atonatui = "atonatui.main:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +# Tells setuptools to track assets inside the python environment +atonatui = ["assets/**/*", "assets/*"] + diff --git a/src/atonatui.egg-info/PKG-INFO b/src/atonatui.egg-info/PKG-INFO new file mode 100644 index 0000000..ed7bce4 --- /dev/null +++ b/src/atonatui.egg-info/PKG-INFO @@ -0,0 +1,113 @@ +Metadata-Version: 2.4 +Name: atonatui +Version: 0.1.1 +Summary: 12 Tone Matrix Generator, Looper, Sequencer. +Author-email: Dominic DiTaranto +License: MIT +Project-URL: Homepage, https://git.domdit.com/dominic/atonatui +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Environment :: Console +Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE.md +Requires-Dist: numpy==2.5.1 +Requires-Dist: prompt_toolkit==3.0.53 +Requires-Dist: pyfluidsynth==1.4.0 +Requires-Dist: wcwidth==0.8.2 +Dynamic: license-file + +# Twelve Tone Generator + +TUI Application for composing serialism music. + +Features: +- Generate 12 Tone Matrix +- Play 12 Tone rows with over 15 instruments +- Loop and/or Sequence 12 tone rows +- Generate Serialized Octave Sequences based on tone matrix +- Generate Serialized Rhythm Sequences based on tone matrix +- Save Matrices and Other Sequences for later use + +## Installation Guide + +PIP: `pipx install atonatui` +ARCH: `yay -S python-atonatui` + +## Main Page + +The main page consists of a 12 Tone Matrix. + +The only part of the main page that really needs explaining is the tri-colored table below the matrix. +- The TONE row contains the tones of P0 row. +- RTHM is the rhythmic division assigned to a given note. +- OCTV is the octave assigned to a given note. + +The rhythmic division and octave will always be the same for their associated note, the order just changes when you play a row other than P0. + +When you do begin playback, this tri-colored table will be replaced with the row that is currently being played. + +## Commands + +| Command | Action | Page | +|---|---|---| +| `P0-P11` | Play Prime Row | Main Page | +| `I0-I11` | Play Inversion Row | Main Page | +| `R0-R11` | Play Retrograde Row | Main Page | +| `i0-i11` | Play Retrograde Inversion Row | Main Page | +| `loop ` | Play row X times in loop | Main Page | +| `seq ,... ` | Play sequence of rows, comma separated | Main Page | +| `r` OR `regen` | Regenerate Matrix | Main Page | +| `s` OR `settings` | Go To Settings Page | Main Page | +| `h` OR `help` | Open This Document | Main Page | +| `q` OR `quit` | Quit Application | Main Page, Settings Page | +| Setting ID | Edit Setting | Settings Page | + + +__Advanced Commands:__ +- Loop a sequence: + - `loop seq ,` + - Example: `loop 5 seq P0,R3,i11,I0` +- Debug + - `debug`: Open up python console via breakpoint() + +## Settings: + +- BPM + - Set the BPM of the overall project, between 30 - 300 BPM +- INSTRUMENT + - Select an instrument for playback +- OCTAVE + - Select base octave for playback +- OCTAVE_RANDOMIZATION + - `0` Only play base octave + - `1` Randomize octaves, increasing octave range by +1 and -1 (example: base octave = 3, octave range = 2-4) + - `2` Randomize octaves, increasing octave range by +2 and -2 (example: base octave = 3, octave range = 1-5) + - `3` Randomize octaves, increasing octave range by +3 and -3 (example: base octave = 1, octave range = 1-4) + - When the range would result in an octave higher than 5 or lower than 0, it clamps range to fit 0-5 +- RHYTHM_COMPLEXITY + - `0` No Rhythmic Complexity, Will play default division + - `1` Will play a rhythm based on sequence generated by tone matrix + - `2` Will play a rhythm based on sequence generated by tone matrix, Including rests (if rest probability is greater than 0) + - `3` Will play a random rhythm not based on a sequence +- DEFAULT_DIVISION + - `0` Whole Note + - `1` Half Note + - `2` Quarter Note + - `3` Eight Note +- REST_PROBABILITY + - The probability in which a rest will occur during playback when RHYTHM_COMPLEXITY = 2 +- SAVE_SETTINGS + - Save current settings to a file, will automatically load when you restart the program +- SAVE_MATRIX + - 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/src/atonatui.egg-info/SOURCES.txt b/src/atonatui.egg-info/SOURCES.txt new file mode 100644 index 0000000..a85fbc6 --- /dev/null +++ b/src/atonatui.egg-info/SOURCES.txt @@ -0,0 +1,38 @@ +LICENSE.md +MANIFEST.in +README.md +pyproject.toml +src/atonatui/__init__.py +src/atonatui/audio_mixin.py +src/atonatui/constants.py +src/atonatui/display_mixin.py +src/atonatui/input_mixin.py +src/atonatui/main.py +src/atonatui/settings_mixin.py +src/atonatui.egg-info/PKG-INFO +src/atonatui.egg-info/SOURCES.txt +src/atonatui.egg-info/dependency_links.txt +src/atonatui.egg-info/entry_points.txt +src/atonatui.egg-info/requires.txt +src/atonatui.egg-info/top_level.txt +src/atonatui/assets/settings.json +src/atonatui/assets/audio/SJO - Bassoon.sf2 +src/atonatui/assets/audio/SJO - Cello.sf2 +src/atonatui/assets/audio/SJO - Choir Aahs.sf2 +src/atonatui/assets/audio/SJO - Clarinet.sf2 +src/atonatui/assets/audio/SJO - Double Bass.sf2 +src/atonatui/assets/audio/SJO - English Horn.sf2 +src/atonatui/assets/audio/SJO - Flute.sf2 +src/atonatui/assets/audio/SJO - French Horn.sf2 +src/atonatui/assets/audio/SJO - Glockenspiel.sf2 +src/atonatui/assets/audio/SJO - Harp.sf2 +src/atonatui/assets/audio/SJO - Marimbas.sf2 +src/atonatui/assets/audio/SJO - Strings slow.sf2 +src/atonatui/assets/audio/SJO - Strings.sf2 +src/atonatui/assets/audio/SJO - Timpani.sf2 +src/atonatui/assets/audio/SJO - Trombone.sf2 +src/atonatui/assets/audio/SJO - Trumpet - Velocity fix.sf2 +src/atonatui/assets/audio/SJO - Tuba.sf2 +src/atonatui/assets/audio/SJO - Vibes.sf2 +src/atonatui/assets/audio/SJO - Viola.sf2 +src/atonatui/assets/audio/SJO - Xylophone.sf2 \ No newline at end of file diff --git a/src/atonatui.egg-info/dependency_links.txt b/src/atonatui.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/atonatui.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/atonatui.egg-info/entry_points.txt b/src/atonatui.egg-info/entry_points.txt new file mode 100644 index 0000000..9cb29e3 --- /dev/null +++ b/src/atonatui.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +atonatui = atonatui.main:main diff --git a/src/atonatui.egg-info/requires.txt b/src/atonatui.egg-info/requires.txt new file mode 100644 index 0000000..1b12213 --- /dev/null +++ b/src/atonatui.egg-info/requires.txt @@ -0,0 +1,4 @@ +numpy==2.5.1 +prompt_toolkit==3.0.53 +pyfluidsynth==1.4.0 +wcwidth==0.8.2 diff --git a/src/atonatui.egg-info/top_level.txt b/src/atonatui.egg-info/top_level.txt new file mode 100644 index 0000000..a09382f --- /dev/null +++ b/src/atonatui.egg-info/top_level.txt @@ -0,0 +1 @@ +atonatui diff --git a/src/atonatui/__init__.py b/src/atonatui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/atonatui/__pycache__/__init__.cpython-314.pyc b/src/atonatui/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..8879914 Binary files /dev/null and b/src/atonatui/__pycache__/__init__.cpython-314.pyc differ diff --git a/src/atonatui/__pycache__/audio_mixin.cpython-314.pyc b/src/atonatui/__pycache__/audio_mixin.cpython-314.pyc new file mode 100644 index 0000000..ba479b0 Binary files /dev/null and b/src/atonatui/__pycache__/audio_mixin.cpython-314.pyc differ diff --git a/src/atonatui/__pycache__/constants.cpython-314.pyc b/src/atonatui/__pycache__/constants.cpython-314.pyc new file mode 100644 index 0000000..403e4e0 Binary files /dev/null and b/src/atonatui/__pycache__/constants.cpython-314.pyc differ diff --git a/src/atonatui/__pycache__/display_mixin.cpython-314.pyc b/src/atonatui/__pycache__/display_mixin.cpython-314.pyc new file mode 100644 index 0000000..8131651 Binary files /dev/null and b/src/atonatui/__pycache__/display_mixin.cpython-314.pyc differ diff --git a/src/atonatui/__pycache__/input_mixin.cpython-314.pyc b/src/atonatui/__pycache__/input_mixin.cpython-314.pyc new file mode 100644 index 0000000..b5f92a0 Binary files /dev/null and b/src/atonatui/__pycache__/input_mixin.cpython-314.pyc differ diff --git a/src/atonatui/__pycache__/main.cpython-314.pyc b/src/atonatui/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..083c53a Binary files /dev/null and b/src/atonatui/__pycache__/main.cpython-314.pyc differ diff --git a/src/atonatui/__pycache__/settings_mixin.cpython-314.pyc b/src/atonatui/__pycache__/settings_mixin.cpython-314.pyc new file mode 100644 index 0000000..a71435c Binary files /dev/null and b/src/atonatui/__pycache__/settings_mixin.cpython-314.pyc differ diff --git a/src/atonatui/assets/audio/SJO - Bassoon.sf2 b/src/atonatui/assets/audio/SJO - Bassoon.sf2 new file mode 100644 index 0000000..b1e1bea Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Bassoon.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Cello.sf2 b/src/atonatui/assets/audio/SJO - Cello.sf2 new file mode 100644 index 0000000..c0de451 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Cello.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Choir Aahs.sf2 b/src/atonatui/assets/audio/SJO - Choir Aahs.sf2 new file mode 100644 index 0000000..549127a Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Choir Aahs.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Clarinet.sf2 b/src/atonatui/assets/audio/SJO - Clarinet.sf2 new file mode 100644 index 0000000..9509445 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Clarinet.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Double Bass.sf2 b/src/atonatui/assets/audio/SJO - Double Bass.sf2 new file mode 100644 index 0000000..62bc328 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Double Bass.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - English Horn.sf2 b/src/atonatui/assets/audio/SJO - English Horn.sf2 new file mode 100644 index 0000000..35fdf49 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - English Horn.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Flute.sf2 b/src/atonatui/assets/audio/SJO - Flute.sf2 new file mode 100644 index 0000000..dd22931 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Flute.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - French Horn.sf2 b/src/atonatui/assets/audio/SJO - French Horn.sf2 new file mode 100644 index 0000000..3a3f6d4 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - French Horn.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Glockenspiel.sf2 b/src/atonatui/assets/audio/SJO - Glockenspiel.sf2 new file mode 100644 index 0000000..c8ea9c2 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Glockenspiel.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Harp.sf2 b/src/atonatui/assets/audio/SJO - Harp.sf2 new file mode 100644 index 0000000..cfb6fc0 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Harp.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Marimbas.sf2 b/src/atonatui/assets/audio/SJO - Marimbas.sf2 new file mode 100644 index 0000000..37f96a3 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Marimbas.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Strings slow.sf2 b/src/atonatui/assets/audio/SJO - Strings slow.sf2 new file mode 100644 index 0000000..65f2cae Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Strings slow.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Strings.sf2 b/src/atonatui/assets/audio/SJO - Strings.sf2 new file mode 100644 index 0000000..6c03279 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Strings.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Timpani.sf2 b/src/atonatui/assets/audio/SJO - Timpani.sf2 new file mode 100644 index 0000000..1df1e20 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Timpani.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Trombone.sf2 b/src/atonatui/assets/audio/SJO - Trombone.sf2 new file mode 100644 index 0000000..d404b68 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Trombone.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Trumpet - Velocity fix.sf2 b/src/atonatui/assets/audio/SJO - Trumpet - Velocity fix.sf2 new file mode 100644 index 0000000..e945ee1 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Trumpet - Velocity fix.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Tuba.sf2 b/src/atonatui/assets/audio/SJO - Tuba.sf2 new file mode 100644 index 0000000..06191dc Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Tuba.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Vibes.sf2 b/src/atonatui/assets/audio/SJO - Vibes.sf2 new file mode 100644 index 0000000..6a8b3a1 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Vibes.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Viola.sf2 b/src/atonatui/assets/audio/SJO - Viola.sf2 new file mode 100644 index 0000000..6b32bbc Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Viola.sf2 differ diff --git a/src/atonatui/assets/audio/SJO - Xylophone.sf2 b/src/atonatui/assets/audio/SJO - Xylophone.sf2 new file mode 100644 index 0000000..b2f9467 Binary files /dev/null and b/src/atonatui/assets/audio/SJO - Xylophone.sf2 differ diff --git a/src/atonatui/assets/settings.json b/src/atonatui/assets/settings.json new file mode 100644 index 0000000..e7f199b --- /dev/null +++ b/src/atonatui/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/src/atonatui/audio_mixin.py b/src/atonatui/audio_mixin.py new file mode 100644 index 0000000..d237179 --- /dev/null +++ b/src/atonatui/audio_mixin.py @@ -0,0 +1,50 @@ +import time +import os +from importlib import resources + +import fluidsynth + +from atonatui.constants import NOTE_TO_MIDI, GENERAL_MIDI_PROGRAMS, SOUNDFONTS +from atonatui.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) + + try: + audio_dir = resources.files("atonatui").joinpath("assets/audio") + sf2_path = str(audio_dir.joinpath(path)) + except Exception: + 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/src/atonatui/constants.py b/src/atonatui/constants.py new file mode 100644 index 0000000..a5d2e6c --- /dev/null +++ b/src/atonatui/constants.py @@ -0,0 +1,172 @@ +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" + + +README1 = f''' +{REVERSE}ATONATUI README{RESET} + +The main page consists of a 12 Tone Matrix. + +The only part of the main page that really needs explaining is the tri-colored table below the matrix. +- The TONE row contains the tones of P0 row. +- RTHM is the rhythmic division assigned to a given note. +- OCTV is the octave assigned to a given note. + +The rhythmic division and octave will always be the same for their associated note, the order just changes when you play a row other than P0. + +When you do begin playback, this tri-colored table will be replaced with the row that is currently being played. +''' + +README2 = f''' +{REVERSE}{GREEN}| Command | Action | Page |{RESET} +| P0-P11 | Play Prime Row | Main Page | +| I0-I11 | Play Inversion Row | Main Page | +| R0-R11 | Play Retrograde Row | Main Page | +| i0-i11 | Play Retrograde Inversion Row | Main Page | +| loop | Play row X times in loop | Main Page | +| seq ,... | Play sequence, comma separated | Main Page | +| r OR regen | Regenerate Matrix | Main Page | +| s OR settings | Go To Settings Page | Main Page | +| h OR help | Open This Document | Main Page | +| q OR quit | Quit Application | Main, Settings Page | +| Setting ID | Edit Setting | Settings Page | + +Advanced Commands: +- Loop a sequence: + - loop seq , + - Example: loop 5 seq P0,R3,i11,I0 +- Debug + - debug: Open up python console via breakpoint() +''' + +README3 = f''' +{REVERSE}{GREEN}Settings:{RESET} +- BPM + - Set the BPM of the overall project, between 30 - 300 BPM +- INSTRUMENT + - Select an instrument for playback +- OCTAVE + - Select base octave for playback +- OCTAVE_RANDOMIZATION + - [0] Only play base octave + - [1] Randomize octaves, increasing octave range by +1 and -1 (example: base octave = 3, octave range = 2-4) + - [2] Randomize octaves, increasing octave range by +2 and -2 (example: base octave = 3, octave range = 1-5) + - [3] Randomize octaves, increasing octave range by +3 and -3 (example: base octave = 1, octave range = 1-4) + - When the range would result in an octave higher than 5 or lower than 0, it clamps range to fit 0-5 +- RHYTHM_COMPLEXITY + - [0] No Rhythmic Complexity, Will play default division + - [1] Will play a rhythm based on sequence generated by tone matrix + - [2] Will play a rhythm based on sequence generated by tone matrix, Including rests (if rest probability is greater than 0) + - [3] Will play a random rhythm not based on a sequence +''' + +README4 = f''' +{REVERSE}{GREEN}Settings Continued:{RESET} +- DEFAULT_DIVISION + - 0 Whole Note + - 1 Half Note + - 2 Quarter Note + - 3 Eight Note +- REST_PROBABILITY + - The probability in which a rest will occur during playback when RHYTHM_COMPLEXITY = 2 +- SAVE_SETTINGS + - Save current settings to a file, will automatically load when you restart the program +- SAVE_MATRIX + - Save the current tone matrix to a file, you can manually load this later +- LOAD_MATRIX + - Load a previously saved tone matrix +''' + +README5 = f''' +{REVERSE}{GREEN}Thank you for using atonatui{RESET} + +Creator: Dominic DiTaranto +Website: https://www.domdit.com/ +Email: me@domdit.com + +v.0.1.2 +''' diff --git a/src/atonatui/display_mixin.py b/src/atonatui/display_mixin.py new file mode 100644 index 0000000..9088d98 --- /dev/null +++ b/src/atonatui/display_mixin.py @@ -0,0 +1,62 @@ +import subprocess +from time import sleep + +from atonatui.constants import RESET, REVERSE, BOLD, GREEN, RED, BLUE, YELLOW, MAGENTA +from atonatui.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}=================================== ATONATUI ===================================={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/src/atonatui/input_mixin.py b/src/atonatui/input_mixin.py new file mode 100644 index 0000000..5301e7d --- /dev/null +++ b/src/atonatui/input_mixin.py @@ -0,0 +1,102 @@ +import sys + +from prompt_toolkit import prompt +from prompt_toolkit.formatted_text import HTML +from atonatui.constants import README1, README2, README3, README4, README5 + + +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] == "h" or request[0] == "help": + self.display_help() + + elif request[0][0] in ["P", "R", "I", "i"]: + self.play_tone_row(request[0][0], request[0][1], loop=loop_count) + + def display_help(self): + for readme in [README1, README2, README3, README4, README5]: + self.clear_screen() + print(readme) + self.request("Press Enter to Continue") + self.loop() + + 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/src/atonatui/main.py b/src/atonatui/main.py new file mode 100644 index 0000000..59b0d7a --- /dev/null +++ b/src/atonatui/main.py @@ -0,0 +1,273 @@ +import random +from collections import defaultdict +from functools import cached_property + +from atonatui.constants import NOTES, RESET, REVERSE, MAGENTA, GREEN, YELLOW, BOLD, WHITE +from atonatui.audio_mixin import AudioMixin +from atonatui.display_mixin import DisplayMixin +from atonatui.settings_mixin import Settings, SettingsMixin +from atonatui.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) + + +def main(): + t = ToneRow() + t.loop() + + +if __name__ == "__main__": + main() diff --git a/src/atonatui/settings_mixin.py b/src/atonatui/settings_mixin.py new file mode 100644 index 0000000..28ba93e --- /dev/null +++ b/src/atonatui/settings_mixin.py @@ -0,0 +1,228 @@ +import os +import json +from pathlib import Path +from importlib import resources +from functools import cached_property +from dataclasses import dataclass, fields +from atonatui.constants import RESET, REVERSE, SOUNDFONTS + +_INSTRUMENT_LIST = [f"[{str(idx).rjust(2)}] {name}" for idx, name in enumerate(SOUNDFONTS.keys())] +_USER_SAVE_DIR = Path.home() / ".local" / "share" / "atonatui" / "saves" + +try: + _DEFAULT_SETTINGS_PATH = resources.files("atonatui").joinpath("assets/settings.json") +except Exception: + _DEFAULT_SETTINGS_PATH = os.path.join(os.path.dirname(__file__), "assets", "settings.json") + +_USER_CONFIG_DIR = Path.home() / ".config" / "atonatui" +_SETTINGS_SAVE_FILE = _USER_CONFIG_DIR / "settings.json" + +_MATRIX_SAVE_DIR = Path.home() / ".local" / "share" / "atonatui" / "saves" + + +def get_saved_matrix_list(): + if not _MATRIX_SAVE_DIR.exists(): + return [" [No saved matrices found]"] + + save_dir_str = str(_MATRIX_SAVE_DIR) + + save_files = [ + f for f in os.listdir(save_dir_str) + if os.path.isfile(os.path.join(save_dir_str, 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 = "Xylophone" + 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: str = "\n[0] Only play base octave\n[1] Randomize octaves, increasing octave range by +1 and -1 (example: base octave = 3, octave range = 2-4)\n[2] Randomize octaves, increasing octave range by +2 and -2 (example: base octave = 3, octave range = 1-5)\n[3] Randomize octaves, increasing octave range by +3 and -3 (example: base octave = 1, octave range = 1-4)\n\nWhen the range would result in an octave higher than 5 or lower than 0, it clamps range to fit 0-5" + RHYTHM_COMPLEXITY: str = "\n[0] Off, Use DEFAULT_DIVISION\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): + _USER_CONFIG_DIR.mkdir(parents=True, exist_ok=True) + + 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): + target_file = _SETTINGS_SAVE_FILE if _SETTINGS_SAVE_FILE.exists() else _DEFAULT_SETTINGS_PATH + + with open(target_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, 'q' to exit") + + 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): + if value == "q": + self.display_settings() + + 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:]) + + target_file = _USER_SAVE_DIR / file_name + + with open(target_file, 'r') as f: + seed = f.readline() + + self.current_seed = int(seed) + self.regenerate() + self.use_current_seed = True + + def handle_save_matrix(self): + _USER_SAVE_DIR.mkdir(parents=True, exist_ok=True) + + cleaned_name = Settings.SAVE_MATRIX.strip().replace(".", " ").replace(" ", "_") + Settings.SAVE_MATRIX = cleaned_name + + target_file = _USER_SAVE_DIR / f"{cleaned_name}.sav" + + with open(target_file, 'w+') as f: + f.write(str(self.current_seed))