packaging
This commit is contained in:
parent
b985581b74
commit
d2b54ae907
47 changed files with 1124 additions and 0 deletions
9
LICENSE.md
Normal file
9
LICENSE.md
Normal file
|
|
@ -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.
|
||||
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
graft src/atonatui/assets
|
||||
global-exclude *.py[cod]
|
||||
25
PKGBUILD
Normal file
25
PKGBUILD
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Maintainer: Dominic DiTaranto <me@domdit.com>
|
||||
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"
|
||||
}
|
||||
|
||||
BIN
dist/atonatui-0.1.1-py3-none-any.whl
vendored
Normal file
BIN
dist/atonatui-0.1.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
dist/atonatui-0.1.1.tar.gz
vendored
Normal file
BIN
dist/atonatui-0.1.1.tar.gz
vendored
Normal file
Binary file not shown.
41
pyproject.toml
Normal file
41
pyproject.toml
Normal file
|
|
@ -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/*"]
|
||||
|
||||
113
src/atonatui.egg-info/PKG-INFO
Normal file
113
src/atonatui.egg-info/PKG-INFO
Normal file
|
|
@ -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 <me@domdit.com>
|
||||
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 <count> <P0-i11>` | Play row X times in loop | Main Page |
|
||||
| `seq <P0-i11>,<P0-i11>... ` | 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 <x> seq <P0-i11>,<P0-i11>`
|
||||
- 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
|
||||
38
src/atonatui.egg-info/SOURCES.txt
Normal file
38
src/atonatui.egg-info/SOURCES.txt
Normal file
|
|
@ -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
|
||||
1
src/atonatui.egg-info/dependency_links.txt
Normal file
1
src/atonatui.egg-info/dependency_links.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
2
src/atonatui.egg-info/entry_points.txt
Normal file
2
src/atonatui.egg-info/entry_points.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[console_scripts]
|
||||
atonatui = atonatui.main:main
|
||||
4
src/atonatui.egg-info/requires.txt
Normal file
4
src/atonatui.egg-info/requires.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
numpy==2.5.1
|
||||
prompt_toolkit==3.0.53
|
||||
pyfluidsynth==1.4.0
|
||||
wcwidth==0.8.2
|
||||
1
src/atonatui.egg-info/top_level.txt
Normal file
1
src/atonatui.egg-info/top_level.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
atonatui
|
||||
0
src/atonatui/__init__.py
Normal file
0
src/atonatui/__init__.py
Normal file
BIN
src/atonatui/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
src/atonatui/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
src/atonatui/__pycache__/audio_mixin.cpython-314.pyc
Normal file
BIN
src/atonatui/__pycache__/audio_mixin.cpython-314.pyc
Normal file
Binary file not shown.
BIN
src/atonatui/__pycache__/constants.cpython-314.pyc
Normal file
BIN
src/atonatui/__pycache__/constants.cpython-314.pyc
Normal file
Binary file not shown.
BIN
src/atonatui/__pycache__/display_mixin.cpython-314.pyc
Normal file
BIN
src/atonatui/__pycache__/display_mixin.cpython-314.pyc
Normal file
Binary file not shown.
BIN
src/atonatui/__pycache__/input_mixin.cpython-314.pyc
Normal file
BIN
src/atonatui/__pycache__/input_mixin.cpython-314.pyc
Normal file
Binary file not shown.
BIN
src/atonatui/__pycache__/main.cpython-314.pyc
Normal file
BIN
src/atonatui/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
BIN
src/atonatui/__pycache__/settings_mixin.cpython-314.pyc
Normal file
BIN
src/atonatui/__pycache__/settings_mixin.cpython-314.pyc
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Bassoon.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Bassoon.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Cello.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Cello.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Choir Aahs.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Choir Aahs.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Clarinet.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Clarinet.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Double Bass.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Double Bass.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - English Horn.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - English Horn.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Flute.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Flute.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - French Horn.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - French Horn.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Glockenspiel.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Glockenspiel.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Harp.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Harp.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Marimbas.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Marimbas.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Strings slow.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Strings slow.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Strings.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Strings.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Timpani.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Timpani.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Trombone.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Trombone.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Trumpet - Velocity fix.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Trumpet - Velocity fix.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Tuba.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Tuba.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Vibes.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Vibes.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Viola.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Viola.sf2
Normal file
Binary file not shown.
BIN
src/atonatui/assets/audio/SJO - Xylophone.sf2
Normal file
BIN
src/atonatui/assets/audio/SJO - Xylophone.sf2
Normal file
Binary file not shown.
1
src/atonatui/assets/settings.json
Normal file
1
src/atonatui/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}
|
||||
50
src/atonatui/audio_mixin.py
Normal file
50
src/atonatui/audio_mixin.py
Normal file
|
|
@ -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()
|
||||
172
src/atonatui/constants.py
Normal file
172
src/atonatui/constants.py
Normal file
|
|
@ -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 <count> <P0-i11> | Play row X times in loop | Main Page |
|
||||
| seq <P0-i11>,<P0-i11>... | 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 <x> seq <P0-i11>,<P0-i11>
|
||||
- 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
|
||||
'''
|
||||
62
src/atonatui/display_mixin.py
Normal file
62
src/atonatui/display_mixin.py
Normal file
|
|
@ -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()
|
||||
102
src/atonatui/input_mixin.py
Normal file
102
src/atonatui/input_mixin.py
Normal file
|
|
@ -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'<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)
|
||||
273
src/atonatui/main.py
Normal file
273
src/atonatui/main.py
Normal file
|
|
@ -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()
|
||||
228
src/atonatui/settings_mixin.py
Normal file
228
src/atonatui/settings_mixin.py
Normal file
|
|
@ -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))
|
||||
Loading…
Reference in a new issue