170
.gitignore
vendored
Normal file
170
.gitignore
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Pytest
|
||||
.pytest_cache
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# Debugging file
|
||||
debug.py
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [2022] [Raphaël Forment]
|
||||
|
||||
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.
|
||||
|
||||
67
main.py
67
main.py
@ -1,39 +1,42 @@
|
||||
from ziffers import *
|
||||
from rich import print
|
||||
|
||||
# FIXME: TO BE REMOVED / CHANGED TO TEST?
|
||||
|
||||
if __name__ == "__main__":
|
||||
expressions = {
|
||||
'Pitches': "-2 -1 0 1 2",
|
||||
'Chords': "0 024 2 246",
|
||||
'Note lengths': "w 0 h 1 q 2 e 3 s 4",
|
||||
'Subdivision': "[1 2 [3 4]]",
|
||||
'Decimal durations': "0.25 0 1 [0.333]2 3",
|
||||
'Octaves': "^ 0 ^ 1 _ 2 _ 3",
|
||||
'Escaped octave': "<2> 1 <1>1<-2>3",
|
||||
'Roman chords': "i ii iii+4 iv+5 v+8 vi+10 vii+20",
|
||||
'Named chords': "i^7 i^min i^dim i^maj7",
|
||||
'Modal interchange (a-g)': "iiia ig ivf^7",
|
||||
'Escape/eval': "{10 11} {1.2 2.43} {3+1*2}",
|
||||
'Randoms': "% ? % ? % ?",
|
||||
'Random between': "(-3,6)",
|
||||
'Random selections': "[q 1 2, q 3 e 4 6]",
|
||||
'Repeat': "[: 1 (2,6) 3 :4]",
|
||||
'Repeat cycles': "[: <q e> (1,4) <(2 3) (3 (1,7))> :]",
|
||||
'Lists': "h 1 q(0 1 2 3) 2",
|
||||
'List cycles': "(: <q e> (1,4) <(2 3) (3 (1,7))> :)",
|
||||
'Loop cycles (for zloop or z0-z9)': "<0 <1 <2 <3 <4 5>>>>>",
|
||||
'Basic operations': "(1 2 (3 4)+2)*2 ((1 2 3)+(0 9 13))-2 ((3 4 {10})*(2 9 3))%7",
|
||||
'Product operations': "(0 1 2 3)+(1 4 2 3) (0 1 2)-(0 2 1)+2",
|
||||
'Euclid cycles': "(q1)<6,7>(q4 (e3 e4) q2) or (q1)<6,7<(q4 q3 q2)",
|
||||
'Transformations': "(0 1 2)<r> (0 1 2)<i>(-2 1)",
|
||||
'List assignation': "A=(0 (1,6) 3) B=(3 ? 2) B A B B A",
|
||||
'Random repeat': "(: 1 (2,6) 3 :4)",
|
||||
'Conditionals': "1 {%<0.5?3} 3 4 (: 1 2 {%<0.2?3:2} :3)",
|
||||
'Functions': "(0 1 2 3){x%3==0?x-2:x+2}",
|
||||
'Polynomials': "(-10..10){(x**3)*(x+1)%12}",
|
||||
"Pitches": "-2 -1 0 1 2",
|
||||
"Chords": "0 024 2 246",
|
||||
"Note lengths": "w 0 h 1 q 2 e 3 s 4",
|
||||
"Subdivision": "[1 2 [3 4]]",
|
||||
"Decimal durations": "0.25 0 1 <0.333>2 3",
|
||||
"Octaves": "^ 0 ^ 1 _ 2 _ 3",
|
||||
"Escaped octave": "<2> 1 <1>1<-2>3",
|
||||
"Roman chords": "i ii iii+4 iv+5 v+8 vi+10 vii+20",
|
||||
"Named chords": "i^7 i^min i^dim i^maj7",
|
||||
"Modal interchange (a-g)": "iiia ig ivf^7",
|
||||
"Escape/eval": "{10 11} {1.2 2.43} {3+1*2}",
|
||||
"Randoms": "% ? % ? % ?",
|
||||
"Random between": "(-3,6)",
|
||||
"Random selections": "[q 1 2, q 3 e 4 6]",
|
||||
"Repeat": "[: 1 (2,6) 3 :4]",
|
||||
"Repeat cycles": "[: <q e> (1,4) <(2 3) (3 (1,7))> :]",
|
||||
"Lists": "h 1 q(0 1 2 3) 2",
|
||||
"List cycles": "(: <q e> (1,4) <(2 3) (3 (1,7))> :)",
|
||||
"Loop cycles (for zloop or z0-z9)": "<0 <1 <2 <3 <4 5>>>>>",
|
||||
"Basic operations": "(1 2 (3 4)+2)*2 ((1 2 3)+(0 9 13))-2 ((3 4 {10})*(2 9 3))%7",
|
||||
"Product operations": "(0 1 2 3)+(1 4 2 3) (0 1 2)-(0 2 1)+2",
|
||||
"Euclid cycles": "(q1)<6,7>(q4 (e3 e4) q2) (q1)<6,7>(q4 q3 q2)",
|
||||
"Transformations": "(0 1 2)<r> (0 1 2)<i>(-2 1)",
|
||||
"List assignation": "A=(0 (1,6) 3) B=(3 ? 2) B A B B A",
|
||||
"Random repeat": "(: 1 (2,6) 3 :4)",
|
||||
"Conditionals": "1 {%<0.5?3} 3 4 (: 1 2 {%<0.2?3:2} :3)",
|
||||
"Functions": "(0 1 2 3){x%3==0?x-2:x+2}",
|
||||
"Polynomials": "(-10..10){(x**3)*(x+1)%12}",
|
||||
}
|
||||
for expression in expressions:
|
||||
try:
|
||||
parse_expression(expression)
|
||||
for ex in expressions:
|
||||
try:
|
||||
print("Parsed: " + parse_expression(expressions[ex]).text)
|
||||
except Exception as e:
|
||||
print(f"[red]Failed on {expression}[/red]: {str(e)[0:40]}...")
|
||||
print(f"[red]Failed on {ex}[/red]")
|
||||
# print(f"[red]Failed on {ex}[/red]: {str(e)[0:40]}...")
|
||||
|
||||
@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
description = "Port of the Ziffers numerical notation for Python"
|
||||
authors = [
|
||||
{name = "Raphaël Forment", email="raphael.forment@gmail.com"},
|
||||
{email = "raphael.forment@gmail.com"}
|
||||
{name = "Miika Alonen", email = "amiika@gmail.com"}
|
||||
]
|
||||
license = {file = "LICENSE.txt"}
|
||||
readme = "README.md"
|
||||
@ -17,8 +17,9 @@ classifiers = [
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"parsimonious>=0.10.0",
|
||||
"rich>=12.6.0"
|
||||
"lark>=1.1.5",
|
||||
"rich>=12.6.0",
|
||||
"pytest>=7.2.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@ -30,4 +31,3 @@ repository = "https://github.com/Bubobubobubobubo/ziffers-python"
|
||||
line-length = 88
|
||||
target_version = ['py310']
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
|
||||
17
repl.py
17
repl.py
@ -1,16 +1,19 @@
|
||||
from ziffers import *
|
||||
from rich import print
|
||||
""" Simple REPL for testing Ziffers notation """
|
||||
# pylint: disable=locally-disabled, redefined-builtin, broad-except
|
||||
|
||||
EXIT_CONDITION = ('exit', 'quit', '')
|
||||
from rich import print
|
||||
from ziffers import parse_expression
|
||||
import readline
|
||||
|
||||
EXIT_CONDITION = ("exit", "quit", "")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"[purple]== ZIFFERS REPL ==[/purple]")
|
||||
print("[purple]== ZIFFERS REPL ==[/purple]")
|
||||
while True:
|
||||
expr = input('> ')
|
||||
expr = input("> ")
|
||||
if expr not in EXIT_CONDITION:
|
||||
try:
|
||||
result = parse_expression(expr)
|
||||
print(result)
|
||||
print(parse_expression(expr))
|
||||
except Exception as e:
|
||||
print(f"[red]Failed:[/red] {e}")
|
||||
else:
|
||||
|
||||
4
test.py
Normal file
4
test.py
Normal file
@ -0,0 +1,4 @@
|
||||
from ziffers import *
|
||||
# a = z1("q e 1 ^ ^ 2 _ 3 4 <3> 3")
|
||||
a = z1("1 2")
|
||||
print(a.take(3))
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
54
tests/test_parser.py
Normal file
54
tests/test_parser.py
Normal file
@ -0,0 +1,54 @@
|
||||
""" Test cases for the parser """
|
||||
import pytest
|
||||
from ziffers import parse_expression
|
||||
|
||||
|
||||
def test_can_parse():
|
||||
expressions = [
|
||||
"[1 [2 3]]",
|
||||
"(1 (1,3) 1..3)",
|
||||
"_^ q _qe^3 qww_4 _123 <1 2>",
|
||||
"q _2 _ 3 ^ 343",
|
||||
"2 qe2 e4",
|
||||
"q 2 <3 343>",
|
||||
"q (2 <3 343 (3 4)>)",
|
||||
]
|
||||
results = []
|
||||
for expression in expressions:
|
||||
try:
|
||||
print(f"Parsing expression: {expression}")
|
||||
result = parse_expression(expression)
|
||||
results.append(True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
results.append(False)
|
||||
|
||||
# Return true if all the results are true (succesfully parsed)
|
||||
print(results)
|
||||
assert all(results)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pattern",
|
||||
[
|
||||
"1 2 3",
|
||||
"q3 e4 s5",
|
||||
],
|
||||
)
|
||||
def test_parsing_text(pattern: str):
|
||||
assert parse_expression(pattern).text == pattern
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pattern,expected",
|
||||
[
|
||||
("1 2 3", [1, 2, 3]),
|
||||
("q2 eq3", [2, 3]),
|
||||
],
|
||||
)
|
||||
def test_pitch_classes(pattern: str, expected: list):
|
||||
assert parse_expression(pattern).pitch_classes() == expected
|
||||
|
||||
# TODO: Add tests for octaves
|
||||
# ("__6 _0 _1 _2 _3 _4 _5 _6 0 1 2 3 4 5 6 ^0 ^1 ^2 ^3 ^4 ^5 ^6 ^^0", [-2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2]),
|
||||
# ("_ 1 _2 <3>3 ^^4", [-1,-2,3,-1]),
|
||||
53
tests/test_scale.py
Normal file
53
tests/test_scale.py
Normal file
@ -0,0 +1,53 @@
|
||||
""" Tests for the scale module """
|
||||
import pytest
|
||||
from ziffers import scale
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,expected",
|
||||
[
|
||||
("C4", 60),
|
||||
("A1", 33),
|
||||
("Bb3", 58),
|
||||
("C#1", 25),
|
||||
("foo", 60),
|
||||
],
|
||||
)
|
||||
def test_notenames(name: str, expected: int):
|
||||
assert scale.note_name_to_midi(name) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pitch_classes,expected",
|
||||
[
|
||||
(
|
||||
list(range(-9, 10)),
|
||||
[
|
||||
45,
|
||||
47,
|
||||
48,
|
||||
50,
|
||||
52,
|
||||
53,
|
||||
55,
|
||||
57,
|
||||
59,
|
||||
60,
|
||||
62,
|
||||
64,
|
||||
65,
|
||||
67,
|
||||
69,
|
||||
71,
|
||||
72,
|
||||
74,
|
||||
76,
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_note_to_midi(pitch_classes: str, expected: int):
|
||||
assert [
|
||||
scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pitch_classes
|
||||
] == expected
|
||||
@ -1 +1,6 @@
|
||||
from .parser import *
|
||||
from .mapper import *
|
||||
from .classes import *
|
||||
from .common import *
|
||||
from .defaults import *
|
||||
from .scale import *
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
676
ziffers/classes.py
Normal file
676
ziffers/classes.py
Normal file
@ -0,0 +1,676 @@
|
||||
""" Ziffers classes for the parsed notation """
|
||||
from dataclasses import dataclass, field, replace
|
||||
import itertools
|
||||
import operator
|
||||
import random
|
||||
from .defaults import DEFAULT_OPTIONS
|
||||
from .scale import note_from_pc, midi_to_pitch_class
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Meta:
|
||||
"""Abstract class for all Ziffers items"""
|
||||
|
||||
kwargs: dict = field(default=None, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.kwargs:
|
||||
for key, val in self.kwargs.items():
|
||||
setattr(self, key, val)
|
||||
|
||||
def update(self, new_values):
|
||||
"""Update attributes from dict"""
|
||||
for key, value in new_values.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
def update_new(self, new_values):
|
||||
"""Updates new attributes from dict"""
|
||||
for key, value in new_values.items():
|
||||
if hasattr(self, key):
|
||||
if getattr(self, key) is None:
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Item(Meta):
|
||||
"""Class for all Ziffers text based items"""
|
||||
|
||||
text: str = field(default=None)
|
||||
|
||||
def get_item(self):
|
||||
"""Return the item"""
|
||||
return self
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Whitespace:
|
||||
"""Class for whitespace"""
|
||||
|
||||
text: str
|
||||
item_type: str = field(default=None, repr=False, init=False)
|
||||
|
||||
def get_item(self):
|
||||
"""Returns None. Used in filtering"""
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class DurationChange(Item):
|
||||
"""Class for changing duration"""
|
||||
|
||||
value: float
|
||||
key: str = field(default="duration", repr=False, init=False)
|
||||
item_type: str = field(default="change", repr=False, init=False)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OctaveChange(Item):
|
||||
"""Class for changing octave"""
|
||||
|
||||
value: int
|
||||
key: str = field(default="octave", repr=False, init=False)
|
||||
item_type: str = field(default="change", repr=False, init=False)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class OctaveAdd(Item):
|
||||
"""Class for modifying octave"""
|
||||
|
||||
value: int
|
||||
key: str = field(default="octave", repr=False, init=False)
|
||||
item_type: str = field(default="add", repr=False, init=False)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Event(Item):
|
||||
"""Abstract class for events with duration"""
|
||||
|
||||
duration: float = field(default=None)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Pitch(Event):
|
||||
"""Class for pitch in time"""
|
||||
|
||||
pitch_class: int
|
||||
octave: int = field(default=None)
|
||||
modifier: int = field(default=0)
|
||||
note: int = field(default=None)
|
||||
key: str = field(default=None)
|
||||
scale: str | list = field(default=None)
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.text is None:
|
||||
self.text = str(self.pitch_class)
|
||||
self.update_note()
|
||||
|
||||
def update_note(self):
|
||||
"""Update note if Key, Scale and Pitch-class is present"""
|
||||
if (
|
||||
(self.key is not None)
|
||||
and (self.scale is not None)
|
||||
and (self.pitch_class is not None)
|
||||
and (self.note is None)
|
||||
):
|
||||
note = note_from_pc(
|
||||
root=self.key,
|
||||
pitch_class=self.pitch_class,
|
||||
intervals=self.scale,
|
||||
modifier=self.modifier if self.modifier is not None else 0,
|
||||
octave=self.octave if self.octave is not None else 0,
|
||||
)
|
||||
self.set_note(note)
|
||||
|
||||
def set_note(self, note: int) -> int:
|
||||
"""Sets a note for the pitch and returns the note.
|
||||
|
||||
Args:
|
||||
note (int): Midi note
|
||||
|
||||
Returns:
|
||||
int: Returns the saved note
|
||||
"""
|
||||
self.note = note
|
||||
return note
|
||||
|
||||
# pylint: disable=locally-disabled, unused-argument
|
||||
def get_value(self) -> int:
|
||||
"""Returns the pitch class
|
||||
|
||||
Returns:
|
||||
int: Integer value for the pitch
|
||||
"""
|
||||
return self.pitch_class
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RandomPitch(Event):
|
||||
"""Class for random pitch"""
|
||||
|
||||
pitch_class: int = field(default=None)
|
||||
|
||||
# pylint: disable=locally-disabled, unused-argument
|
||||
def get_value(self) -> int:
|
||||
"""Return random value
|
||||
|
||||
Returns:
|
||||
int: Returns random pitch
|
||||
"""
|
||||
return self.pitch_class
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RandomPercent(Item):
|
||||
"""Class for random percent"""
|
||||
|
||||
percent: float = field(default=None)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Chord(Event):
|
||||
"""Class for chords"""
|
||||
|
||||
pitch_classes: list[Pitch] = field(default=None)
|
||||
notes: list[int] = field(default=None)
|
||||
|
||||
def set_notes(self, notes: list[int]):
|
||||
"""Set notes to the class"""
|
||||
self.notes = notes
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RomanNumeral(Event):
|
||||
"""Class for roman numbers"""
|
||||
|
||||
value: str = field(default=None)
|
||||
chord_type: str = field(default=None)
|
||||
notes: list[int] = field(default_factory=[])
|
||||
pitch_classes: list = None
|
||||
|
||||
def set_notes(self, chord_notes: list[int]):
|
||||
"""Set notes to roman numeral
|
||||
|
||||
Args:
|
||||
chord_notes (list[int]): List of notes in midi to be added
|
||||
"""
|
||||
self.notes = chord_notes
|
||||
|
||||
def set_pitch_classes(self, pitches: list[tuple]):
|
||||
"""Set pitch classes to roman numeral
|
||||
|
||||
Args:
|
||||
pitches (list[tuple]): Pitch classes to be added
|
||||
"""
|
||||
if self.pitch_classes is None:
|
||||
self.pitch_classes = []
|
||||
for pitch in pitches:
|
||||
self.pitch_classes.append(Pitch(**pitch))
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Function(Event):
|
||||
"""Class for functions"""
|
||||
|
||||
run: str = field(default=None)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Sequence(Meta):
|
||||
"""Class for sequences of items"""
|
||||
|
||||
values: list
|
||||
text: str = field(default=None)
|
||||
wrap_start: str = field(default=None, repr=False)
|
||||
wrap_end: str = field(default=None, repr=False)
|
||||
local_index: int = field(default=0, init=False)
|
||||
evaluation: bool = field(default=False, init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.text = self.__collect_text()
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.values[index]
|
||||
|
||||
def update_values(self, new_values):
|
||||
"""Update value attributes from dict"""
|
||||
for key, value in new_values.items():
|
||||
for obj in self.values:
|
||||
if key != "text" and hasattr(obj, key):
|
||||
setattr(obj, key, value)
|
||||
|
||||
def __collect_text(self) -> str:
|
||||
"""Collect text value from values"""
|
||||
text = "".join([val.text for val in self.values])
|
||||
if self.wrap_start is not None:
|
||||
text = self.wrap_start + text
|
||||
if self.wrap_end is not None:
|
||||
text = text + self.wrap_end
|
||||
return text
|
||||
|
||||
def evaluate_tree(self, options=None):
|
||||
"""Evaluates and flattens the Ziffers object tree"""
|
||||
for item in self.values:
|
||||
if isinstance(item, Sequence):
|
||||
if item.evaluation:
|
||||
yield from item.evaluate(options)
|
||||
else:
|
||||
yield from item.evaluate_tree(options)
|
||||
else:
|
||||
# Get value / generated value from the item
|
||||
current = item.get_item()
|
||||
# Ignore items that returns None
|
||||
if current is not None:
|
||||
if isinstance(current, (DurationChange, OctaveChange, OctaveAdd)):
|
||||
options = self.__update_options(current, options)
|
||||
else:
|
||||
if set(("key", "scale")) <= options.keys():
|
||||
if isinstance(current, Cyclic):
|
||||
current = current.get_value()
|
||||
if isinstance(current, (Pitch, RandomPitch, RandomInteger)):
|
||||
current = self.__update_pitch(current, options)
|
||||
elif isinstance(current, Chord):
|
||||
current = self.__update_chord(current, options)
|
||||
elif isinstance(current, RomanNumeral):
|
||||
current = self.__create_chord_from_roman(
|
||||
current, options
|
||||
)
|
||||
current.update_new(options)
|
||||
yield current
|
||||
|
||||
def filter(self, keep: tuple):
|
||||
"""Filter out items from sequence.
|
||||
|
||||
Args:
|
||||
keep (tuple): Tuple describing classes to keep
|
||||
|
||||
Returns:
|
||||
Sequence: Copy of the sequence with filtered values.
|
||||
"""
|
||||
return replace(
|
||||
self, values=[item for item in self.values if isinstance(item, keep)]
|
||||
)
|
||||
|
||||
def __update_options(self, current: Item, options: dict) -> dict:
|
||||
"""Update options based on current item
|
||||
|
||||
Args:
|
||||
current (Item): Current item like Duration change, Octave change etc.
|
||||
options (dict): Current options
|
||||
|
||||
Returns:
|
||||
dict: Updated options
|
||||
"""
|
||||
if current.item_type == "change": # Change options
|
||||
options[current.key] = current.value
|
||||
elif current.item_type == "add":
|
||||
if current.key in options: # Add to existing value
|
||||
options[current.key] += current.value
|
||||
else: # Create value if not existing
|
||||
options[current.key] = current.value
|
||||
return options
|
||||
|
||||
def __update_pitch(self, current: Item, options: dict) -> dict:
|
||||
"""Update pich based on optons
|
||||
|
||||
Args:
|
||||
current (Item): _description_
|
||||
options (dict): _description_
|
||||
|
||||
Returns:
|
||||
dict: _description_
|
||||
"""
|
||||
if hasattr(current, "modifier"):
|
||||
c_modifier = 0
|
||||
elif "modifier" in options:
|
||||
c_modifier = options["modifier"]
|
||||
else:
|
||||
c_modifier = 0
|
||||
|
||||
if hasattr(current, "octave"):
|
||||
c_octave = 0
|
||||
elif "octave" in options:
|
||||
c_octave = options["octave"]
|
||||
else:
|
||||
c_octave = 0
|
||||
|
||||
note = note_from_pc(
|
||||
root=options["key"],
|
||||
pitch_class=current.get_value(),
|
||||
intervals=options["scale"],
|
||||
modifier=c_modifier,
|
||||
octave=c_octave,
|
||||
)
|
||||
new_pitch = Pitch(
|
||||
pitch_class=current.get_value(),
|
||||
text=str(current.get_value()),
|
||||
note=note,
|
||||
octave=c_octave,
|
||||
modifier=c_modifier,
|
||||
kwargs=options,
|
||||
)
|
||||
return new_pitch
|
||||
|
||||
def __update_chord(self, current: Chord, options: dict) -> Chord:
|
||||
"""Update chord based on options
|
||||
|
||||
Args:
|
||||
current (Chord): Current chord object
|
||||
options (dict): Options
|
||||
re (bool, optional): Re-evaluation flag. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Chord: Returns updated chord
|
||||
"""
|
||||
pcs = current.pitch_classes
|
||||
notes = [
|
||||
pc.set_note(note_from_pc(options["key"], pc.pitch_class, options["scale"]))
|
||||
for pc in pcs
|
||||
]
|
||||
current.set_notes(notes)
|
||||
return current
|
||||
|
||||
def __create_chord_from_roman(self, current: RomanNumeral, options: dict) -> Chord:
|
||||
"""Create chord fom roman numeral
|
||||
|
||||
Args:
|
||||
current (RomanNumeral): Current roman numeral
|
||||
options (dict): Options
|
||||
re (bool, optional): Re-evaluation flag. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Chord: New chord created from Roman numeral
|
||||
"""
|
||||
key = options["key"]
|
||||
scale = options["scale"]
|
||||
pitches = [midi_to_pitch_class(note, key, scale) for note in current.notes]
|
||||
chord_notes = [
|
||||
note_from_pc(
|
||||
root=key,
|
||||
pitch_class=pitch,
|
||||
intervals=scale,
|
||||
modifier=current.modifier if hasattr(current, "modifier") else 0,
|
||||
)
|
||||
for pitch in pitches
|
||||
]
|
||||
chord = Chord(text="".join(pitches), pitch_classes=pitches, notes=chord_notes)
|
||||
return chord
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Ziffers(Sequence):
|
||||
"""Main class for holding options and the current state"""
|
||||
|
||||
options: dict = field(default_factory=DEFAULT_OPTIONS)
|
||||
loop_i: int = 0
|
||||
iterator = None
|
||||
current: Item = field(default=None)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
self.current = next(self.iterator)
|
||||
self.loop_i += 1
|
||||
return self.current
|
||||
|
||||
# pylint: disable=locally-disabled, dangerous-default-value
|
||||
def init_opts(self, options=None):
|
||||
"""Evaluate the Ziffers tree using the options"""
|
||||
self.options.update(DEFAULT_OPTIONS)
|
||||
if options:
|
||||
self.options.update(options)
|
||||
else:
|
||||
self.options = DEFAULT_OPTIONS
|
||||
|
||||
self.iterator = iter(self.evaluate_tree(self.options))
|
||||
|
||||
def re_eval(self, options=None):
|
||||
"""Re-evaluate the iterator"""
|
||||
self.options.update(DEFAULT_OPTIONS)
|
||||
if options:
|
||||
self.options.update(options)
|
||||
self.iterator = iter(self.evaluate_tree(self.options))
|
||||
|
||||
def get_list(self):
|
||||
"""Return list"""
|
||||
return list(self)
|
||||
|
||||
def take(self, num: int) -> list[Pitch]:
|
||||
"""Take number of pitch classes from the parsed sequence. Cycles from the beginning.
|
||||
|
||||
Args:
|
||||
num (int): Number of pitch classes to take from the sequence
|
||||
|
||||
Returns:
|
||||
list: List of pitch class items
|
||||
"""
|
||||
return list(itertools.islice(itertools.cycle(self), num))
|
||||
|
||||
def loop(self) -> iter:
|
||||
"""Return cyclic loop"""
|
||||
return itertools.cycle(iter(self))
|
||||
|
||||
def set_defaults(self, options: dict):
|
||||
"""Sets options for the parser
|
||||
|
||||
Args:
|
||||
options (dict): Options as a dict
|
||||
"""
|
||||
self.options = DEFAULT_OPTIONS | options
|
||||
|
||||
# TODO: Refactor these
|
||||
def pitch_classes(self) -> list[int]:
|
||||
"""Return list of pitch classes as ints"""
|
||||
return [val.pitch_class for val in self.values if isinstance(val, Pitch)]
|
||||
|
||||
def durations(self) -> list[float]:
|
||||
"""Return list of pitch durations as floats"""
|
||||
return [val.dur for val in self.values if isinstance(val, Pitch)]
|
||||
|
||||
def pairs(self) -> list[tuple]:
|
||||
"""Return list of pitches and durations"""
|
||||
return [
|
||||
(val.pitch_class, val.dur) for val in self.values if isinstance(val, Pitch)
|
||||
]
|
||||
|
||||
def octaves(self) -> list[int]:
|
||||
"""Return list of octaves"""
|
||||
return [val.octave for val in self.values if isinstance(val, Pitch)]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ListSequence(Sequence):
|
||||
"""Class for Ziffers list sequences"""
|
||||
|
||||
wrap_start: str = field(default="(", repr=False)
|
||||
wrap_end: str = field(default=")", repr=False)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Integer(Item):
|
||||
"""Class for integers"""
|
||||
|
||||
value: int
|
||||
|
||||
# pylint: disable=locally-disabled, unused-argument
|
||||
def get_value(self):
|
||||
"""Return value of the integer"""
|
||||
return self.value
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RandomInteger(Item):
|
||||
"""Class for random integer"""
|
||||
|
||||
min: int
|
||||
max: int
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.min > self.max:
|
||||
new_max = self.min
|
||||
self.min = self.max
|
||||
self.max = new_max
|
||||
|
||||
# pylint: disable=locally-disabled, unused-argument
|
||||
def get_value(self):
|
||||
"""Evaluate the random value for the generator"""
|
||||
return random.randint(self.min, self.max)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RepeatedListSequence(Sequence):
|
||||
"""Class for Ziffers list sequences"""
|
||||
|
||||
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
|
||||
wrap_start: str = field(default="(:", repr=False)
|
||||
wrap_end: str = field(default=":)", repr=False)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Subdivision(Item):
|
||||
"""Class for subdivisions"""
|
||||
|
||||
values: list[Event]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Cyclic(Item):
|
||||
"""Class for cyclic sequences"""
|
||||
|
||||
values: list
|
||||
cycle: int = 0
|
||||
wrap_start: str = field(default="<", repr=False)
|
||||
wrap_end: str = field(default=">", repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.text = self.__collect_text()
|
||||
self.values = [val for val in self.values if not isinstance(val, Whitespace)]
|
||||
|
||||
def __collect_text(self) -> str:
|
||||
"""Collect text value from values"""
|
||||
text = "".join([val.text for val in self.values])
|
||||
if self.wrap_start is not None:
|
||||
text = self.wrap_start + text
|
||||
if self.wrap_end is not None:
|
||||
text = text + self.wrap_end
|
||||
return text
|
||||
|
||||
def get_value(self):
|
||||
"""Get the value for the current cycle"""
|
||||
value = self.values[self.cycle % len(self.values)]
|
||||
self.cycle += 1
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Range(Item):
|
||||
"""Class for range"""
|
||||
|
||||
start: int = field(default=None)
|
||||
end: int = field(default=None)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Operator(Item):
|
||||
"""Class for math operators"""
|
||||
|
||||
value: ...
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ListOperation(Sequence):
|
||||
"""Class for list operations"""
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.evaluation = True
|
||||
|
||||
def filter_operation(self, values):
|
||||
"""Filtering for the operation elements"""
|
||||
keep = (Sequence, Event, RandomInteger, Integer, Cyclic)
|
||||
for item in values:
|
||||
if isinstance(item, Sequence):
|
||||
yield item.filter(keep)
|
||||
elif isinstance(item, keep):
|
||||
yield item
|
||||
|
||||
def evaluate(self, options: dict):
|
||||
"""Evaluates the operation"""
|
||||
operators = self.values[1::2] # Fetch every second operator element
|
||||
values = self.values[::2] # Fetch every second list element
|
||||
values = list(self.filter_operation(values)) # Filter out crap
|
||||
result = values[0] # Start results with the first array
|
||||
for i, operand in enumerate(operators):
|
||||
operation = operand.value
|
||||
right_value = values[i + 1]
|
||||
if isinstance(right_value, Sequence):
|
||||
result = [
|
||||
Pitch(
|
||||
pitch_class=operation(x.get_value(), y.get_value()),
|
||||
kwargs=options,
|
||||
)
|
||||
for x in result
|
||||
for y in right_value
|
||||
]
|
||||
else:
|
||||
result = [
|
||||
Pitch(
|
||||
pitch_class=operation(x.get_value(), right_value.get_value()),
|
||||
kwargs=options,
|
||||
)
|
||||
for x in result
|
||||
]
|
||||
return Sequence(values=result)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Operation(Item):
|
||||
"""Class for lisp-like operations: (+ 1 2 3) etc."""
|
||||
|
||||
values: list
|
||||
operator: operator
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Eval(Sequence):
|
||||
"""Class for evaluation notation"""
|
||||
|
||||
result: ... = None
|
||||
wrap_start: str = field(default="{", repr=False)
|
||||
wrap_end: str = field(default="}", repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.result = eval(self.text)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Atom(Item):
|
||||
"""Class for evaluable atoms"""
|
||||
|
||||
value: ...
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Euclid(Item):
|
||||
"""Class for euclidean cycles"""
|
||||
|
||||
pulses: int
|
||||
length: int
|
||||
onset: list
|
||||
offset: list = field(default=None)
|
||||
rotate: int = field(default=None)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RepeatedSequence(Sequence):
|
||||
"""Class for repeats"""
|
||||
|
||||
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
|
||||
wrap_start: str = field(default="[:", repr=False)
|
||||
wrap_end: str = field(default=":]", repr=False)
|
||||
19
ziffers/common.py
Normal file
19
ziffers/common.py
Normal file
@ -0,0 +1,19 @@
|
||||
""" Common methods used in parsing """
|
||||
def flatten(arr: list) -> list:
|
||||
"""Flattens array"""
|
||||
return (
|
||||
flatten(arr[0]) + (flatten(arr[1:]) if len(arr) > 1 else [])
|
||||
if isinstance(arr, list) else [arr]
|
||||
)
|
||||
|
||||
|
||||
def sum_dict(arr: list[dict]) -> dict:
|
||||
"""Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}"""
|
||||
result = arr[0]
|
||||
for element in arr[1:]:
|
||||
for key in element.keys():
|
||||
if key in result:
|
||||
result[key] = result[key] + element[key]
|
||||
else:
|
||||
result[key] = element[key]
|
||||
return result
|
||||
1674
ziffers/defaults.py
Normal file
1674
ziffers/defaults.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,40 +0,0 @@
|
||||
|
||||
ebnf = r"""
|
||||
ziffers = (bar / octup / octdown / subdiv / escape / rhythm / float / chord / pc / ws?)+
|
||||
|
||||
escape = (lt (chord / pc) gt)
|
||||
subdiv = (lbra ziffers rbra)
|
||||
|
||||
chord = pc{2,}
|
||||
pc = (neg_pc / pc_basic)
|
||||
neg_pc = (~r"-" pc)
|
||||
pc_basic = ~r"[0-9TE]"
|
||||
|
||||
rhythm = ~r"[mklpdcwyhnqaefsxtgujoz]"
|
||||
|
||||
float = ~r"\d+\.\d+"
|
||||
|
||||
lpar = "("
|
||||
rpar = ")"
|
||||
lbra = "["
|
||||
rbra = "]"
|
||||
lcbra = "{"
|
||||
rcbra = "}"
|
||||
lt = "<"
|
||||
gt = ">"
|
||||
|
||||
|
||||
octup = "^"
|
||||
octdown = "_"
|
||||
|
||||
bar = "|"
|
||||
|
||||
plus = "+"
|
||||
minus = "-"
|
||||
times = "*"
|
||||
div = "/"
|
||||
|
||||
emptyline = ws+
|
||||
comma = ","
|
||||
ws = ~"\s*"
|
||||
"""
|
||||
351
ziffers/mapper.py
Normal file
351
ziffers/mapper.py
Normal file
@ -0,0 +1,351 @@
|
||||
""" Lark transformer for mapping Lark tokens to Ziffers objects """
|
||||
from typing import Optional
|
||||
from lark import Transformer
|
||||
from .classes import (
|
||||
Ziffers,
|
||||
Whitespace,
|
||||
DurationChange,
|
||||
OctaveChange,
|
||||
OctaveAdd,
|
||||
Pitch,
|
||||
RandomPitch,
|
||||
RandomPercent,
|
||||
Chord,
|
||||
RomanNumeral,
|
||||
Sequence,
|
||||
ListSequence,
|
||||
RepeatedListSequence,
|
||||
Subdivision,
|
||||
Cyclic,
|
||||
RandomInteger,
|
||||
Range,
|
||||
Operator,
|
||||
ListOperation,
|
||||
Operation,
|
||||
Eval,
|
||||
Atom,
|
||||
Integer,
|
||||
Euclid,
|
||||
RepeatedSequence,
|
||||
)
|
||||
from .common import flatten, sum_dict
|
||||
from .defaults import DEFAULT_DURS, OPERATORS
|
||||
from .scale import parse_roman, chord_from_roman_numeral
|
||||
|
||||
|
||||
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name
|
||||
class ZiffersTransformer(Transformer):
|
||||
"""Rules for transforming Ziffers expressions into tree."""
|
||||
|
||||
def __init__(self, options: Optional[dict] = None):
|
||||
super().__init__()
|
||||
self.options = options
|
||||
|
||||
def start(self, items) -> Ziffers:
|
||||
"""Root for the rules"""
|
||||
# seq = Sequence(values=items[0])
|
||||
return Ziffers(values=items[0], options={})
|
||||
|
||||
def sequence(self, items):
|
||||
"""Flatten sequence"""
|
||||
return flatten(items)
|
||||
|
||||
def random_integer(self, item) -> RandomInteger:
|
||||
"""Parses random integer syntax"""
|
||||
val = item[0][1:-1].split(",")
|
||||
return RandomInteger(min=int(val[0]), max=int(val[1]), text=item[0].value)
|
||||
|
||||
def range(self, item) -> Range:
|
||||
"""Parses range syntax"""
|
||||
val = item[0].split("..")
|
||||
return Range(start=val[0], end=val[1], text=item[0])
|
||||
|
||||
def cycle(self, items) -> Cyclic:
|
||||
"""Parses cycle"""
|
||||
values = items[0]
|
||||
return Cyclic(values=values)
|
||||
|
||||
def pitch_class(self, item):
|
||||
"""Parses pitch class"""
|
||||
|
||||
# If there are prefixes
|
||||
if len(item) > 1:
|
||||
# Collect&sum prefixes from any order: _qee^s4 etc.
|
||||
result = sum_dict(item)
|
||||
return Pitch(**result)
|
||||
|
||||
val = item[0]
|
||||
return Pitch(**val)
|
||||
|
||||
def pitch(self, items):
|
||||
"""Return pitch class info"""
|
||||
return {"pitch_class": int(items[0].value), "text": items[0].value}
|
||||
|
||||
def prefix(self, items):
|
||||
"""Return prefix"""
|
||||
return items[0]
|
||||
|
||||
def oct_change(self, items):
|
||||
"""Parses octave change"""
|
||||
octave = items[0]
|
||||
return [OctaveChange(value=octave["octave"], text=octave["text"]), items[1]]
|
||||
|
||||
def oct_mod(self, items):
|
||||
"""Parses octave modification"""
|
||||
octave = items[0]
|
||||
return [OctaveAdd(value=octave["octave"], text=octave["text"]), items[1]]
|
||||
|
||||
def escaped_octave(self, items):
|
||||
"""Return octave info"""
|
||||
value = items[0][1:-1]
|
||||
return {"octave": int(value), "text": items[0].value}
|
||||
|
||||
def octave(self, items):
|
||||
"""Return octaves ^ and _"""
|
||||
value = sum(1 if char == "^" else -1 for char in items[0].value)
|
||||
return {"octave": value, "text": items[0].value}
|
||||
|
||||
def modifier(self, items):
|
||||
"""Return modifiers # and b"""
|
||||
value = 1 if items[0].value == "#" else -1
|
||||
return {"modifier": value}
|
||||
|
||||
def chord(self, items):
|
||||
"""Parses chord"""
|
||||
return Chord(pitch_classes=items, text="".join([val.text for val in items]))
|
||||
|
||||
def named_roman(self, items) -> RomanNumeral:
|
||||
"""Parse chord from roman numeral"""
|
||||
numeral = items[0].value
|
||||
if len(items) > 1:
|
||||
name = items[1]
|
||||
chord_notes = chord_from_roman_numeral(numeral, name)
|
||||
parsed_number = parse_roman(numeral)
|
||||
return RomanNumeral(
|
||||
text=numeral, value=parsed_number, chord_type=name, notes=chord_notes
|
||||
)
|
||||
return RomanNumeral(
|
||||
value=parse_roman(numeral),
|
||||
text=numeral,
|
||||
notes=chord_from_roman_numeral(numeral),
|
||||
)
|
||||
|
||||
def chord_name(self, item):
|
||||
"""Return name for chord"""
|
||||
return item[0].value
|
||||
|
||||
def roman_number(self, item):
|
||||
"""Return roman numeral"""
|
||||
return item.value
|
||||
|
||||
def dur_change(self, items):
|
||||
"""Parses duration change"""
|
||||
durs = items[0]
|
||||
return DurationChange(value=durs["duration"], text=durs["text"])
|
||||
|
||||
def char_change(self, items):
|
||||
"""Return partial duration char info"""
|
||||
chars = ""
|
||||
durs = 0.0
|
||||
for dchar, dots in items:
|
||||
val = DEFAULT_DURS[dchar]
|
||||
if dots > 0:
|
||||
val = val * (2.0 - (1.0 / (2 * dots)))
|
||||
chars = chars + (dchar + "." * dots)
|
||||
durs = durs + val
|
||||
return {"text": chars, "duration": durs}
|
||||
|
||||
def dchar_not_prefix(self, items):
|
||||
"""Return partial duration char info"""
|
||||
dur = items[0].split(".", 1)
|
||||
dots = 0
|
||||
if len(dur) > 1:
|
||||
dots = len(dur[1]) + 1
|
||||
return [dur[0], dots]
|
||||
|
||||
def escaped_decimal(self, items):
|
||||
"""Return partial decimal info"""
|
||||
val = items[0]
|
||||
val["text"] = "<" + val["text"] + ">"
|
||||
return val
|
||||
|
||||
def random_pitch(self, items):
|
||||
"""Parses random pitch"""
|
||||
return RandomPitch(text="?")
|
||||
|
||||
def random_percent(self, items):
|
||||
"""Parses random percent"""
|
||||
return RandomPercent(text="%")
|
||||
|
||||
def duration_chars(self, items):
|
||||
"""Return partial duration info"""
|
||||
durations = [val[1] for val in items]
|
||||
characters = "".join([val[0] for val in items])
|
||||
return {"duration": sum(durations), "text": characters}
|
||||
|
||||
def dotted_dur(self, items):
|
||||
"""Return partial duration info"""
|
||||
key = items[0]
|
||||
val = DEFAULT_DURS[key]
|
||||
dots = len(items) - 1
|
||||
if dots > 0:
|
||||
val = val * (2.0 - (1.0 / (2 * dots)))
|
||||
return [key + "." * dots, val]
|
||||
|
||||
def decimal(self, items):
|
||||
"""Return partial duration info"""
|
||||
val = items[0]
|
||||
return {"duration": float(val), "text": val.value}
|
||||
|
||||
def dot(self, items):
|
||||
"""Return partial duration info"""
|
||||
return "."
|
||||
|
||||
def dchar(self, items):
|
||||
"""Return partial duration info"""
|
||||
chardur = items[0].value
|
||||
return chardur
|
||||
|
||||
def WS(self, items):
|
||||
"""Parse whitespace"""
|
||||
return Whitespace(text=items[0])
|
||||
|
||||
def subdivision(self, items):
|
||||
"""Parse subdivision"""
|
||||
values = flatten(items[0])
|
||||
return Subdivision(
|
||||
values=values, text="[" + "".join([val.text for val in values]) + "]"
|
||||
)
|
||||
|
||||
def subitems(self, items):
|
||||
"""Return subdivision items"""
|
||||
return items
|
||||
|
||||
# Eval rules
|
||||
|
||||
def eval(self, items):
|
||||
"""Parse eval"""
|
||||
val = items[0]
|
||||
return Eval(values=val)
|
||||
|
||||
def sub_operations(self, items):
|
||||
"""Returns list of operations"""
|
||||
return Eval(values=items[0], wrap_start="(", wrap_end=")")
|
||||
|
||||
def operation(self, items):
|
||||
"""Return partial eval operations"""
|
||||
return flatten(items)
|
||||
|
||||
def atom(self, token):
|
||||
"""Return partial eval item"""
|
||||
val = token[0].value
|
||||
return Atom(value=val, text=val)
|
||||
|
||||
# List rules
|
||||
|
||||
def list(self, items):
|
||||
"""Parse list sequence notation, ex: (1 2 3)"""
|
||||
if len(items) > 1:
|
||||
prefixes = sum_dict(items[0:-1])
|
||||
values = items[-1]
|
||||
seq = ListSequence(values=values, wrap_start=prefixes["text"] + "(")
|
||||
seq.update_values(prefixes)
|
||||
return seq
|
||||
else:
|
||||
seq = ListSequence(values=items[0])
|
||||
return seq
|
||||
|
||||
def repeated_list(self, items):
|
||||
"""Parse repeated list notation ex: (: 1 2 3 :)"""
|
||||
if len(items) > 2:
|
||||
prefixes = sum_dict(items[0:-2]) # If there are prefixes
|
||||
if items[-1] is not None:
|
||||
seq = RepeatedListSequence(
|
||||
values=items[-2],
|
||||
repeats=items[-1],
|
||||
wrap_end=":" + items[-1].text + ")",
|
||||
)
|
||||
else:
|
||||
seq = RepeatedListSequence(
|
||||
values=items[-2], repeats=Integer(text="1", value=1)
|
||||
)
|
||||
seq.update_values(prefixes)
|
||||
return seq
|
||||
else:
|
||||
if items[-1] is not None:
|
||||
seq = RepeatedListSequence(
|
||||
values=items[-2],
|
||||
repeats=items[-1],
|
||||
wrap_end=":" + items[-1].text + ")",
|
||||
)
|
||||
else:
|
||||
seq = RepeatedListSequence(
|
||||
values=items[-2], repeats=Integer(text="1", value=1)
|
||||
)
|
||||
return seq
|
||||
|
||||
def NUMBER(self, token):
|
||||
"""Parse integer"""
|
||||
val = token.value
|
||||
return Integer(text=val, value=int(val))
|
||||
|
||||
def number(self, item):
|
||||
"""Return partial number (Integer or RandomInteger)"""
|
||||
return item[0]
|
||||
|
||||
def cyclic_number(self, item):
|
||||
"""Parse cyclic notation"""
|
||||
return Cyclic(values=item)
|
||||
|
||||
def lisp_operation(self, items):
|
||||
"""Parse lisp like list operation"""
|
||||
op = items[0]
|
||||
values = items[1:]
|
||||
return Operation(
|
||||
operator=op,
|
||||
values=values,
|
||||
text="(+" + "".join([v.text for v in values]) + ")",
|
||||
)
|
||||
|
||||
def operator(self, token):
|
||||
"""Parse operator"""
|
||||
val = token[0].value
|
||||
return Operator(text=val, value=OPERATORS[val])
|
||||
|
||||
def list_items(self, items):
|
||||
"""Parse sequence"""
|
||||
return Sequence(values=items)
|
||||
|
||||
def list_op(self, items):
|
||||
"""Parse list operation"""
|
||||
return ListOperation(values=items)
|
||||
|
||||
def right_op(self,items):
|
||||
"""Get right value for the operation"""
|
||||
return items[0]
|
||||
|
||||
def euclid(self, items):
|
||||
"""Parse euclid notation"""
|
||||
params = items[1][1:-1].split(",")
|
||||
init = {"onset": items[0], "pulses": params[0], "length": params[1]}
|
||||
text = items[0].text + items[1]
|
||||
if len(params) > 2:
|
||||
init["rotate"] = params[2]
|
||||
if len(items) > 2:
|
||||
init["offset"] = items[2]
|
||||
text = text + items[2].text
|
||||
init["text"] = text
|
||||
return Euclid(**init)
|
||||
|
||||
def euclid_operator(self, token):
|
||||
"""Return euclid operators"""
|
||||
return token.value
|
||||
|
||||
def repeat(self, items):
|
||||
"""Parse repeated sequence, ex: [: 1 2 3 :]"""
|
||||
if items[-1] is not None:
|
||||
return RepeatedSequence(
|
||||
values=items[0], repeats=items[-1], wrap_end=":" + items[-1].text + "]"
|
||||
)
|
||||
else:
|
||||
return RepeatedSequence(values=items[0], repeats=Integer(value=1, text="1"))
|
||||
@ -1,57 +1,52 @@
|
||||
from parsimonious.grammar import Grammar
|
||||
from parsimonious.nodes import NodeVisitor
|
||||
from rich import print
|
||||
from .ebnf import ebnf
|
||||
""" Module for the parser """
|
||||
from pathlib import Path
|
||||
from lark import Lark
|
||||
from .classes import Ziffers
|
||||
from .mapper import ZiffersTransformer
|
||||
|
||||
__all__ = ('ZiffersVisitor', 'parse_expression',)
|
||||
|
||||
GRAMMAR = Grammar(ebnf)
|
||||
grammar_path = Path(__file__).parent
|
||||
grammar = grammar_path / "ziffers.lark"
|
||||
|
||||
class ZiffersVisitor(NodeVisitor):
|
||||
ziffers_parser = Lark.open(
|
||||
grammar,
|
||||
rel_to=__file__,
|
||||
start="root",
|
||||
parser="lalr",
|
||||
transformer=ZiffersTransformer(),
|
||||
)
|
||||
|
||||
|
||||
def parse_expression(expr: str) -> Ziffers:
|
||||
"""Parse an expression using the Ziffers parser
|
||||
|
||||
Args:
|
||||
expr (str): Ziffers expression as a string
|
||||
|
||||
Returns:
|
||||
Ziffers: Reutrns Ziffers iterable
|
||||
"""
|
||||
Visitor for the Ziffers syntax.
|
||||
return ziffers_parser.parse(expr)
|
||||
|
||||
|
||||
def zparse(expr: str, **opts) -> Ziffers:
|
||||
"""Parses ziffers expression with options
|
||||
|
||||
Args:
|
||||
expr (str): Ziffers expression as a string
|
||||
opts (dict, optional): Options for parsing the Ziffers expression. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Ziffers: Returns Ziffers iterable parsed with the given options
|
||||
"""
|
||||
parsed = parse_expression(expr)
|
||||
parsed.init_opts(opts)
|
||||
return parsed
|
||||
|
||||
def visit_ziffers(self, node, children):
|
||||
"""
|
||||
Top-level visiter. Will traverse and build something out of a complete and valid
|
||||
Ziffers expression.
|
||||
"""
|
||||
print(f"Node: {node}, Children: {children}")
|
||||
result = {'ziffers': []}
|
||||
|
||||
for i in children:
|
||||
if i[0] in (None, [], {}) and isinstance(i[0], dict):
|
||||
continue
|
||||
try:
|
||||
result['ziffers'].append(i[0])
|
||||
except Exception as e:
|
||||
print(f"[red]Error in ziffers:[/red] {e}")
|
||||
pass
|
||||
return result
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def visit_pc(self, node, children):
|
||||
return {node.expr_name: node.text}
|
||||
|
||||
def visit_escape(self, node, children):
|
||||
return {node.expr_name: node.text}
|
||||
|
||||
# def visit_subdiv(self, node, visited_children):
|
||||
# key, values = visited_children
|
||||
# ret)rn {key, dict(values)}
|
||||
|
||||
def generic_visit(self, node, children):
|
||||
"""
|
||||
This method seem to be the generic method to include in any NodeVisitor.
|
||||
Probably better to keep it as is for the moment. This is appending a whole
|
||||
lot of garbage to the final expression because I don't really know how to
|
||||
write it properly...
|
||||
"""
|
||||
return children or node
|
||||
|
||||
def parse_expression(expression: str) -> dict:
|
||||
tree = GRAMMAR.parse(expression)
|
||||
visitor = ZiffersVisitor()
|
||||
return visitor.visit(tree)
|
||||
|
||||
def z(expr: str, **opts) -> Ziffers:
|
||||
"""Shortened method name for zparse"""
|
||||
return zparse(expr, **opts)
|
||||
|
||||
260
ziffers/scale.py
Normal file
260
ziffers/scale.py
Normal file
@ -0,0 +1,260 @@
|
||||
""" Methods for calculating notes from scales and list of all intervals in scales"""
|
||||
#!/usr/bin/env python3
|
||||
# pylint: disable=locally-disabled, no-name-in-module
|
||||
import re
|
||||
from math import floor
|
||||
from .defaults import (
|
||||
DEFAULT_OCTAVE,
|
||||
SCALES,
|
||||
MODIFIERS,
|
||||
NOTES_TO_INTERVALS,
|
||||
INTERVALS_TO_NOTES,
|
||||
ROMANS,
|
||||
CIRCLE_OF_FIFTHS,
|
||||
CHORDS,
|
||||
)
|
||||
|
||||
|
||||
def midi_to_note_name(midi: int) -> str:
|
||||
"""Creates note name from midi number
|
||||
|
||||
Args:
|
||||
midi (int): Mii number
|
||||
|
||||
Returns:
|
||||
str: Note name
|
||||
"""
|
||||
return INTERVALS_TO_NOTES[midi % 12]
|
||||
|
||||
|
||||
def note_name_to_interval(name: str) -> int:
|
||||
"""Parse note name to interval
|
||||
|
||||
Args:
|
||||
name (str): Note name as: [a-gA-G][#bs]
|
||||
|
||||
Returns:
|
||||
int: Interval of the note name [-1 - 11]
|
||||
"""
|
||||
items = re.match(r"^([a-gA-G])([#bs])?$", name)
|
||||
if items is None:
|
||||
return 0
|
||||
values = items.groups()
|
||||
modifier = MODIFIERS[values[1]] if values[1] else 0
|
||||
interval = NOTES_TO_INTERVALS[values[0].capitalize()]
|
||||
return interval + modifier
|
||||
|
||||
|
||||
def note_name_to_midi(name: str) -> int:
|
||||
"""Parse note name to midi
|
||||
|
||||
Args:
|
||||
name (str): Note name in scientific notation: [a-gA-G][#bs][1-9]
|
||||
|
||||
Returns:
|
||||
int: Midi note
|
||||
"""
|
||||
items = re.match(r"^([a-gA-G])([#bs])?([1-9])?$", name)
|
||||
if items is None:
|
||||
return 60
|
||||
values = items.groups()
|
||||
octave = int(values[2]) if values[2] else 4
|
||||
modifier = MODIFIERS[values[1]] if values[1] else 0
|
||||
interval = NOTES_TO_INTERVALS[values[0].capitalize()]
|
||||
return 12 + octave * 12 + interval + modifier
|
||||
|
||||
|
||||
def get_scale(name: str) -> list[int]:
|
||||
"""Get a scale from the global scale list
|
||||
|
||||
Args:
|
||||
name (str): Name of the scale as named in https://allthescales.org/
|
||||
|
||||
Returns:
|
||||
list: List of intervals in the scale
|
||||
"""
|
||||
scale = SCALES.get(name.lower().capitalize(), SCALES["Chromatic"])
|
||||
return list(map(int, str(scale)))
|
||||
|
||||
|
||||
# pylint: disable=locally-disabled, too-many-arguments
|
||||
def note_from_pc(
|
||||
root: int | str,
|
||||
pitch_class: int,
|
||||
intervals: str | list[int | float],
|
||||
cents: bool = False,
|
||||
octave: int = 0,
|
||||
modifier: int = 0,
|
||||
) -> int:
|
||||
"""Resolve a pitch class into a note from a scale
|
||||
|
||||
Args:
|
||||
root (int | str): Root of the scale in MIDI or scientific pitch notation
|
||||
pitch_class (int): Pitch class to be resolved
|
||||
intervals (str | list[int | float]): Name or Intervals for the scale
|
||||
cents (bool, optional): Flag for interpreting intervals as cents. Defaults to False.
|
||||
octave (int, optional): Default octave. Defaults to 0.
|
||||
modifier (int, optional): Modifier for the pitch class (#=1, b=-1). Defaults to 0.
|
||||
|
||||
Returns:
|
||||
int: Resolved MIDI note
|
||||
"""
|
||||
|
||||
# Initialization
|
||||
root = note_name_to_midi(root) if isinstance(root, str) else root
|
||||
intervals = get_scale(intervals) if isinstance(intervals, str) else intervals
|
||||
intervals = list(map(lambda x: x / 100), intervals) if cents else intervals
|
||||
scale_length = len(intervals)
|
||||
|
||||
# Resolve pitch classes to the scale and calculate octave
|
||||
if pitch_class >= scale_length or pitch_class < 0:
|
||||
octave += floor(pitch_class / scale_length)
|
||||
pitch_class = (
|
||||
scale_length - (abs(pitch_class) % scale_length)
|
||||
if pitch_class < 0
|
||||
else pitch_class % scale_length
|
||||
)
|
||||
if pitch_class == scale_length:
|
||||
pitch_class = 0
|
||||
|
||||
# Computing the result
|
||||
note = root + sum(intervals[0:pitch_class])
|
||||
|
||||
return note + (octave * sum(intervals)) + modifier
|
||||
|
||||
|
||||
def parse_roman(numeral: str) -> int:
|
||||
"""Parse roman numeral from string
|
||||
|
||||
Args:
|
||||
numeral (str): Roman numeral as string
|
||||
|
||||
Returns:
|
||||
int: Integer parsed from roman numeral
|
||||
"""
|
||||
values = [ROMANS[val] for val in numeral]
|
||||
result = 0
|
||||
i = 0
|
||||
while i < len(values):
|
||||
if i < len(values) - 1 and values[i + 1] > values[i]:
|
||||
result += values[i + 1] - values[i]
|
||||
i += 2
|
||||
else:
|
||||
result += values[i]
|
||||
i += 1
|
||||
return result
|
||||
|
||||
|
||||
def accidentals_from_note_name(name: str) -> int:
|
||||
"""Generates number of accidentals from name of the note.
|
||||
|
||||
Args:
|
||||
name (str): Name of the note
|
||||
|
||||
Returns:
|
||||
int: Integer representing number of flats or sharps: -7 flat to 7 sharp.
|
||||
"""
|
||||
idx = CIRCLE_OF_FIFTHS.index(name.upper())
|
||||
return idx - 6
|
||||
|
||||
|
||||
def accidentals_from_midi_note(note: int) -> int:
|
||||
"""Generates number of accidentals from name of the note.
|
||||
|
||||
Args:
|
||||
note (int): Note as midi number
|
||||
|
||||
Returns:
|
||||
int: Integer representing number of flats or sharps: -7 flat to 7 sharp.
|
||||
"""
|
||||
name = midi_to_note_name(note)
|
||||
return accidentals_from_note_name(name)
|
||||
|
||||
|
||||
def midi_to_tpc(note: int, key: str | int):
|
||||
"""Return Tonal Pitch Class value for the note
|
||||
|
||||
Args:
|
||||
note (int): MIDI note
|
||||
key (str | int): Key as a string (A-G) or a MIDI note.
|
||||
|
||||
Returns:
|
||||
_type_: Tonal Pitch Class value for the note
|
||||
"""
|
||||
if isinstance(key, str):
|
||||
acc = accidentals_from_note_name(key)
|
||||
else:
|
||||
acc = accidentals_from_midi_note(key)
|
||||
return (note * 7 + 26 - (11 + acc)) % 12 + (11 + acc)
|
||||
|
||||
|
||||
def midi_to_octave(note: int) -> int:
|
||||
"""Return octave for the midi note
|
||||
|
||||
Args:
|
||||
note (int): Note in midi
|
||||
|
||||
Returns:
|
||||
int: Returns default octave in Ziffers where C4 is in octave 0
|
||||
"""
|
||||
return 0 if note <= 0 else floor(note / 12)
|
||||
|
||||
|
||||
def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
||||
"""Return pitch class and octave from given midi note, key and scale
|
||||
|
||||
Args:
|
||||
note (int): Note as MIDI number
|
||||
key (str | int): Used key
|
||||
scale (str): Used scale
|
||||
|
||||
Returns:
|
||||
tuple: Returns dict containing pitch-class values
|
||||
"""
|
||||
pitch_class = note % 12
|
||||
octave = midi_to_octave(note) - 5
|
||||
if scale.upper() == "CHROMATIC":
|
||||
return (str(pitch_class), pitch_class, octave)
|
||||
|
||||
sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"]
|
||||
flats = ["0", "b1", "1", "b2", "2", "3", "b4", "4", "b5", "5", "b6", "6"]
|
||||
tpc = midi_to_tpc(note, key)
|
||||
if tpc >= 6 and tpc <= 12 and len(flats[pitch_class]) == 2:
|
||||
npc = flats[pitch_class]
|
||||
elif tpc >= 20 and tpc <= 26 and len(sharps[pitch_class]) == 2:
|
||||
npc = sharps[pitch_class]
|
||||
else:
|
||||
npc = sharps[pitch_class]
|
||||
|
||||
if len(npc) > 1:
|
||||
return {
|
||||
"text": npc,
|
||||
"pitch_class": int(npc[1]),
|
||||
"octave": octave,
|
||||
"modifier": 1 if (npc[0] == "#") else -1,
|
||||
}
|
||||
|
||||
return {"text": npc, "pitch_class": int(npc), "octave": octave}
|
||||
|
||||
|
||||
def chord_from_roman_numeral(
|
||||
roman: str, name: str = "major", num_octaves: int = 1
|
||||
) -> list[int]:
|
||||
"""Generates chord from given roman numeral and chord name
|
||||
|
||||
Args:
|
||||
roman (str): Roman numeral
|
||||
name (str, optional): Chord name. Defaults to "major".
|
||||
num_octaves (int, optional): Number of octaves for the chord. Defaults to 1.
|
||||
|
||||
Returns:
|
||||
list[int]: _description_
|
||||
"""
|
||||
root = parse_roman(roman) - 1
|
||||
tonic = (DEFAULT_OCTAVE * 12) + root + 12
|
||||
intervals = CHORDS.get(name, CHORDS["major"])
|
||||
notes = []
|
||||
for cur_oct in range(num_octaves):
|
||||
for iterval in intervals:
|
||||
notes.append(tonic + iterval + (cur_oct * 12))
|
||||
return notes
|
||||
81
ziffers/ziffers.lark
Normal file
81
ziffers/ziffers.lark
Normal file
@ -0,0 +1,81 @@
|
||||
// Root for the rules
|
||||
?root: sequence -> start
|
||||
sequence: (pitch_class | dur_change | oct_mod | oct_change | WS | chord | named_roman | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)*
|
||||
|
||||
// Pitch classes
|
||||
pitch_class: prefix* pitch
|
||||
prefix: (octave | duration_chars | escaped_decimal | escaped_octave | modifier)
|
||||
pitch: /-?[0-9TE]/
|
||||
escaped_decimal: "<" decimal ">"
|
||||
escaped_octave: /<-?[0-9]>/
|
||||
octave: /[_^]+/
|
||||
modifier: /[#b]/
|
||||
|
||||
// Chords
|
||||
chord: pitch_class pitch_class+
|
||||
named_roman: roman_number (("^" chord_name))? // TODO: Add | ("+" number)
|
||||
chord_name: /[a-zA-Z0-9]+/
|
||||
?roman_number: /iv|v|v?i{1,3}/
|
||||
|
||||
// Valid as integer
|
||||
number: NUMBER | random_integer | cyclic_number
|
||||
cyclic_number: "<" number (WS number)* ">"
|
||||
|
||||
// Repeats
|
||||
repeat: "[:" sequence ":" [number] "]"
|
||||
|
||||
// List
|
||||
list: prefix* "(" sequence ")"
|
||||
repeated_list: prefix* "(:" sequence ":" [number] ")"
|
||||
|
||||
// Right recursive list operation
|
||||
list_op: list (operator right_op)+
|
||||
right_op: list | number
|
||||
operator: /([\+\-\*\/%]|<<|>>)/
|
||||
|
||||
// Euclidean cycles
|
||||
euclid: list euclid_operator list?
|
||||
?euclid_operator: /<[0-9]+,[0-9]+(,[0-9])?>/
|
||||
|
||||
// TODO: Support randomization etc.
|
||||
//euclid_operator: (">" | "<") number "," number ["," number] (">" | "<")
|
||||
|
||||
// Lisp like list operation
|
||||
lisp_operation: "(" operator WS sequence ")"
|
||||
|
||||
// Durations
|
||||
duration_chars: dotted_dur+
|
||||
dotted_dur: dchar dot*
|
||||
decimal: /-?[0-9]+\.[0-9]+/
|
||||
dchar: /[mklpdcwyhnqaefsxtgujzo]/
|
||||
dot: "."
|
||||
|
||||
// Subdivision
|
||||
subdivision: "[" subitems "]"
|
||||
subitems: (pitch_class | WS | chord | cycle | subdivision)*
|
||||
|
||||
// Control characters modifying future events
|
||||
oct_mod: octave WS
|
||||
oct_change: escaped_octave WS
|
||||
dur_change: (decimal | char_change)
|
||||
char_change: dchar_not_prefix+
|
||||
dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?!\d)/
|
||||
|
||||
// Generative rules
|
||||
random_integer: /\(-?[0-9]+,-?[0-9]+\)/
|
||||
range: /-?[0-9]\.\.-?[0-9]/
|
||||
cycle: "<" sequence ">"
|
||||
random_pitch: /(\?)(?!\d)/
|
||||
random_percent: /(%)(?!\d)/
|
||||
|
||||
// Rules for evaluating clauses inside {}
|
||||
// TODO: Support for parenthesis?
|
||||
eval: "{" operation+ "}"
|
||||
operation: atom (operator (sub_operations | operation))*
|
||||
sub_operations: "(" operation ")"
|
||||
atom: (NUMBER | DECIMAL)
|
||||
|
||||
%import common.NUMBER
|
||||
//%import common.SIGNED_NUMBER
|
||||
%import common.DECIMAL
|
||||
%import common.WS
|
||||
Reference in New Issue
Block a user