Added parsing for monzos and support for escaped pitch_classes
Syntax for monzos supported in scala scales: [-1 1> etc.
Support for escaped pitches: {q12 e23 26}
This commit is contained in:
@ -7,6 +7,7 @@ from ziffers import zparse
|
|||||||
[
|
[
|
||||||
("1 2 3", [[1, 2, 3], [0.25,0.25,0.25]]),
|
("1 2 3", [[1, 2, 3], [0.25,0.25,0.25]]),
|
||||||
("q2 eq3 e.4", [[2, 3, 4], [0.25,0.375,0.1875]]),
|
("q2 eq3 e.4", [[2, 3, 4], [0.25,0.375,0.1875]]),
|
||||||
|
("{q9 e10 23}", [[9,10,23],[0.25,0.125,0.25]])
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_multi_var(pattern: str, expected: list):
|
def test_multi_var(pattern: str, expected: list):
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
""" Ziffers item classes """
|
""" Ziffers item classes """
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import dataclass, field, asdict
|
||||||
from math import floor
|
from math import floor
|
||||||
import operator
|
|
||||||
import random
|
import random
|
||||||
from ..scale import (
|
from ..scale import (
|
||||||
note_from_pc,
|
note_from_pc,
|
||||||
@ -149,6 +148,26 @@ class Event(Item):
|
|||||||
class Rest(Event):
|
class Rest(Event):
|
||||||
"""Class for rests"""
|
"""Class for rests"""
|
||||||
|
|
||||||
|
def get_note(self):
|
||||||
|
"""Getter for note"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_freq(self):
|
||||||
|
"""Getter for freq"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_octave(self):
|
||||||
|
"""Getter for octave"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_pitch_class(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_pitch_bend(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Measure(Item):
|
class Measure(Item):
|
||||||
@ -283,9 +302,16 @@ class RandomPitch(Event):
|
|||||||
Returns:
|
Returns:
|
||||||
int: Returns random pitch
|
int: Returns random pitch
|
||||||
"""
|
"""
|
||||||
return random.randint(
|
if options:
|
||||||
0, get_scale_length(options.get("scale", "Major")) if options else 9
|
scale = options["scale"]
|
||||||
)
|
if isinstance(scale, str):
|
||||||
|
scale_length = get_scale_length(options.get("scale", "Major"))
|
||||||
|
else:
|
||||||
|
scale_length = len(scale)
|
||||||
|
else:
|
||||||
|
scale_length = 9
|
||||||
|
|
||||||
|
return random.randint(0, scale_length)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
@ -604,14 +630,6 @@ class Operator(Item):
|
|||||||
value: ...
|
value: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Operation(Item):
|
|
||||||
"""Class for lisp-like operations: (+ 1 2 3) etc."""
|
|
||||||
|
|
||||||
values: list
|
|
||||||
operator: operator
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
class Atom(Item):
|
class Atom(Item):
|
||||||
"""Class for evaluable atoms"""
|
"""Class for evaluable atoms"""
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from itertools import islice, cycle
|
from itertools import islice, cycle
|
||||||
from ..defaults import DEFAULT_OPTIONS
|
from ..defaults import DEFAULT_OPTIONS
|
||||||
from .items import Item, Pitch, Chord, Event
|
from .items import Item, Pitch, Chord, Event, Rest
|
||||||
from .sequences import Sequence, Subdivision
|
from .sequences import Sequence, Subdivision
|
||||||
|
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ class Ziffers(Sequence):
|
|||||||
return [
|
return [
|
||||||
val.get_pitch_class()
|
val.get_pitch_class()
|
||||||
for val in self.evaluated_values
|
for val in self.evaluated_values
|
||||||
if isinstance(val, (Pitch, Chord))
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
]
|
]
|
||||||
|
|
||||||
def pitch_bends(self) -> list[int]:
|
def pitch_bends(self) -> list[int]:
|
||||||
@ -115,7 +115,7 @@ class Ziffers(Sequence):
|
|||||||
return [
|
return [
|
||||||
val.get_pitch_bend()
|
val.get_pitch_bend()
|
||||||
for val in self.evaluated_values
|
for val in self.evaluated_values
|
||||||
if isinstance(val, (Pitch, Chord))
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
]
|
]
|
||||||
|
|
||||||
def notes(self) -> list[int]:
|
def notes(self) -> list[int]:
|
||||||
@ -123,7 +123,7 @@ class Ziffers(Sequence):
|
|||||||
return [
|
return [
|
||||||
val.get_note()
|
val.get_note()
|
||||||
for val in self.evaluated_values
|
for val in self.evaluated_values
|
||||||
if isinstance(val, (Pitch, Chord))
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
]
|
]
|
||||||
|
|
||||||
def durations(self) -> list[float]:
|
def durations(self) -> list[float]:
|
||||||
@ -136,7 +136,9 @@ class Ziffers(Sequence):
|
|||||||
|
|
||||||
def total_duration(self) -> float:
|
def total_duration(self) -> float:
|
||||||
"""Return total duration"""
|
"""Return total duration"""
|
||||||
return sum([val.duration for val in self.evaluated_values if isinstance(val, Event)])
|
return sum(
|
||||||
|
[val.duration for val in self.evaluated_values if isinstance(val, Event)]
|
||||||
|
)
|
||||||
|
|
||||||
def total_beats(self) -> float:
|
def total_beats(self) -> float:
|
||||||
"""Return total beats"""
|
"""Return total beats"""
|
||||||
@ -161,7 +163,7 @@ class Ziffers(Sequence):
|
|||||||
return [
|
return [
|
||||||
val.get_octave()
|
val.get_octave()
|
||||||
for val in self.evaluated_values
|
for val in self.evaluated_values
|
||||||
if isinstance(val, (Pitch, Chord))
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
]
|
]
|
||||||
|
|
||||||
def freqs(self) -> list[int]:
|
def freqs(self) -> list[int]:
|
||||||
@ -169,7 +171,7 @@ class Ziffers(Sequence):
|
|||||||
return [
|
return [
|
||||||
val.get_freq()
|
val.get_freq()
|
||||||
for val in self.evaluated_values
|
for val in self.evaluated_values
|
||||||
if isinstance(val, (Pitch, Chord))
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
]
|
]
|
||||||
|
|
||||||
def collect(self, num: int = None, keys: str | list = None) -> list:
|
def collect(self, num: int = None, keys: str | list = None) -> list:
|
||||||
|
|||||||
@ -4,8 +4,9 @@ from itertools import product
|
|||||||
from math import floor
|
from math import floor
|
||||||
from types import LambdaType
|
from types import LambdaType
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
import operator
|
||||||
from ..defaults import DEFAULT_OPTIONS
|
from ..defaults import DEFAULT_OPTIONS
|
||||||
from ..common import cyclic_zip, euclidian_rhythm
|
from ..common import cyclic_zip, euclidian_rhythm, flatten
|
||||||
from ..scale import note_from_pc, midi_to_freq
|
from ..scale import note_from_pc, midi_to_freq
|
||||||
from .items import (
|
from .items import (
|
||||||
Meta,
|
Meta,
|
||||||
@ -31,7 +32,7 @@ from .items import (
|
|||||||
Modification,
|
Modification,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Sample,
|
Sample,
|
||||||
SampleList
|
SampleList,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -46,6 +47,8 @@ def resolve_item(item: Meta, options: dict):
|
|||||||
elif isinstance(item, Subdivision):
|
elif isinstance(item, Subdivision):
|
||||||
item.evaluate_values(options)
|
item.evaluate_values(options)
|
||||||
yield item
|
yield item
|
||||||
|
elif isinstance(item, Eval):
|
||||||
|
yield from item.evaluate_values(options)
|
||||||
else:
|
else:
|
||||||
yield from item.evaluate_tree(options)
|
yield from item.evaluate_tree(options)
|
||||||
elif isinstance(item, VariableAssignment):
|
elif isinstance(item, VariableAssignment):
|
||||||
@ -66,14 +69,14 @@ def resolve_item(item: Meta, options: dict):
|
|||||||
run=opt_item,
|
run=opt_item,
|
||||||
text=item.text,
|
text=item.text,
|
||||||
kwargs=(options | item.local_options),
|
kwargs=(options | item.local_options),
|
||||||
local_options=item.local_options
|
local_options=item.local_options,
|
||||||
)
|
)
|
||||||
elif isinstance(opt_item, str):
|
elif isinstance(opt_item, str):
|
||||||
yield Sample(
|
yield Sample(
|
||||||
name=opt_item,
|
name=opt_item,
|
||||||
text=item.text,
|
text=item.text,
|
||||||
kwargs=(options | item.local_options),
|
kwargs=(options | item.local_options),
|
||||||
local_options=item.local_options
|
local_options=item.local_options,
|
||||||
)
|
)
|
||||||
variable = deepcopy(opt_item)
|
variable = deepcopy(opt_item)
|
||||||
yield from resolve_item(variable, options)
|
yield from resolve_item(variable, options)
|
||||||
@ -90,7 +93,7 @@ def resolve_item(item: Meta, options: dict):
|
|||||||
run=opt_item,
|
run=opt_item,
|
||||||
text=var.text,
|
text=var.text,
|
||||||
kwargs=(options | var.local_options),
|
kwargs=(options | var.local_options),
|
||||||
local_options=var.local_options
|
local_options=var.local_options,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif isinstance(opt_item, str):
|
elif isinstance(opt_item, str):
|
||||||
@ -99,7 +102,7 @@ def resolve_item(item: Meta, options: dict):
|
|||||||
name=opt_item,
|
name=opt_item,
|
||||||
text=var.text,
|
text=var.text,
|
||||||
kwargs=(options | var.local_options),
|
kwargs=(options | var.local_options),
|
||||||
local_options=var.local_options
|
local_options=var.local_options,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif isinstance(opt_item, Sequence):
|
elif isinstance(opt_item, Sequence):
|
||||||
@ -373,6 +376,8 @@ class ListOperation(Sequence):
|
|||||||
flattened_list.extend(list(item.evaluate_durations()))
|
flattened_list.extend(list(item.evaluate_durations()))
|
||||||
elif isinstance(item, RepeatedListSequence):
|
elif isinstance(item, RepeatedListSequence):
|
||||||
flattened_list.extend(list(item.resolve_repeat(options)))
|
flattened_list.extend(list(item.resolve_repeat(options)))
|
||||||
|
elif isinstance(item, Eval):
|
||||||
|
flattened_list.extend(item.evaluate_values(options))
|
||||||
else:
|
else:
|
||||||
flattened_list.append(_filter_operation(item, options))
|
flattened_list.append(_filter_operation(item, options))
|
||||||
elif isinstance(item, Cyclic):
|
elif isinstance(item, Cyclic):
|
||||||
@ -543,13 +548,43 @@ class ListOperation(Sequence):
|
|||||||
class Eval(Sequence):
|
class Eval(Sequence):
|
||||||
"""Class for evaluation notation"""
|
"""Class for evaluation notation"""
|
||||||
|
|
||||||
result: ... = None
|
|
||||||
wrap_start: str = field(default="{", repr=False)
|
wrap_start: str = field(default="{", repr=False)
|
||||||
wrap_end: str = field(default="}", repr=False)
|
wrap_end: str = field(default="}", repr=False)
|
||||||
|
|
||||||
def __post_init__(self):
|
# def __post_init__(self):
|
||||||
super().__post_init__()
|
# self.text = "".join([val.text for val in flatten(self.values)])
|
||||||
self.result = eval(self.text)
|
# super().__post_init__()
|
||||||
|
|
||||||
|
def evaluate_values(self, options):
|
||||||
|
operations = [val for val in self.values if isinstance(val, (Operation, Rest))]
|
||||||
|
eval_values = []
|
||||||
|
for val in operations:
|
||||||
|
if isinstance(val,Operation):
|
||||||
|
eval_values.append(Pitch(pitch_class=val.evaluate(), kwargs=options | val.local_options))
|
||||||
|
else:
|
||||||
|
eval_values.append(val)
|
||||||
|
|
||||||
|
self.evaluated_values = eval_values
|
||||||
|
|
||||||
|
return self.evaluated_values
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class LispOperation(Sequence):
|
||||||
|
"""Class for lisp-like operations: (+ 1 2 3) etc."""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
operator: operator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Operation(Sequence):
|
||||||
|
"""Class for sequential operations"""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
|
||||||
|
def evaluate(self):
|
||||||
|
return eval(self.text)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
@ -588,6 +623,8 @@ class RepeatedSequence(Sequence):
|
|||||||
elif isinstance(item, Subdivision):
|
elif isinstance(item, Subdivision):
|
||||||
item.evaluate_values(options)
|
item.evaluate_values(options)
|
||||||
yield item
|
yield item
|
||||||
|
elif isinstance(item, Eval):
|
||||||
|
yield from item.evaluate_values(options)
|
||||||
else:
|
else:
|
||||||
yield from item
|
yield from item
|
||||||
elif isinstance(item, Cyclic):
|
elif isinstance(item, Cyclic):
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import re
|
import re
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
def flatten(arr: list) -> list:
|
def flatten(arr: list) -> list:
|
||||||
"""Flattens array"""
|
"""Flattens array"""
|
||||||
return (
|
return (
|
||||||
@ -23,6 +24,7 @@ def rotate(arr, k):
|
|||||||
arr = arr[-k:] + arr[:-k]
|
arr = arr[-k:] + arr[:-k]
|
||||||
return arr
|
return arr
|
||||||
|
|
||||||
|
|
||||||
def repeat_text(pos, neg, times):
|
def repeat_text(pos, neg, times):
|
||||||
"""Helper to repeat text"""
|
"""Helper to repeat text"""
|
||||||
if times > 0:
|
if times > 0:
|
||||||
@ -31,6 +33,7 @@ def repeat_text(pos,neg,times):
|
|||||||
return neg * abs(times)
|
return neg * abs(times)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def sum_dict(arr: list[dict]) -> dict:
|
def sum_dict(arr: list[dict]) -> dict:
|
||||||
"""Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}"""
|
"""Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}"""
|
||||||
result = arr[0]
|
result = arr[0]
|
||||||
|
|||||||
35
ziffers/generators.py
Normal file
35
ziffers/generators.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""Collection of generators"""
|
||||||
|
|
||||||
|
|
||||||
|
# Sieve of Eratosthenes
|
||||||
|
# Based on code by David Eppstein, UC Irvine, 28 Feb 2002
|
||||||
|
# http://code.activestate.com/recipes/117119/
|
||||||
|
def gen_primes():
|
||||||
|
"""Generate an infinite sequence of prime numbers."""
|
||||||
|
# Maps composites to primes witnessing their compositeness.
|
||||||
|
# This is memory efficient, as the sieve is not "run forward"
|
||||||
|
# indefinitely, but only as long as required by the current
|
||||||
|
# number being tested.
|
||||||
|
sieve = {}
|
||||||
|
|
||||||
|
# The running integer that's checked for primeness
|
||||||
|
current = 2
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if current not in sieve:
|
||||||
|
# current is a new prime.
|
||||||
|
# Yield it and mark its first multiple that isn't
|
||||||
|
# already marked in previous iterations
|
||||||
|
yield current
|
||||||
|
sieve[current * current] = [current]
|
||||||
|
else:
|
||||||
|
# current is composite. sieve[current] is the list of primes that
|
||||||
|
# divide it. Since we've reached current, we no longer
|
||||||
|
# need it in the map, but we'll mark the next
|
||||||
|
# multiples of its witnesses to prepare for larger
|
||||||
|
# numbers
|
||||||
|
for composite in sieve[current]:
|
||||||
|
sieve.setdefault(composite + current, []).append(composite)
|
||||||
|
del sieve[current]
|
||||||
|
|
||||||
|
current += 1
|
||||||
@ -1,8 +1,7 @@
|
|||||||
""" Lark transformer for mapping Lark tokens to Ziffers objects """
|
""" Lark transformer for mapping Lark tokens to Ziffers objects """
|
||||||
import random
|
import random
|
||||||
from math import log, pow
|
|
||||||
from lark import Transformer, Token
|
from lark import Transformer, Token
|
||||||
from .scale import cents_to_semitones, ratio_to_cents
|
from .scale import cents_to_semitones, ratio_to_cents, monzo_to_cents
|
||||||
from .classes.root import Ziffers
|
from .classes.root import Ziffers
|
||||||
from .classes.sequences import (
|
from .classes.sequences import (
|
||||||
Sequence,
|
Sequence,
|
||||||
@ -13,6 +12,8 @@ from .classes.sequences import (
|
|||||||
Euclid,
|
Euclid,
|
||||||
Subdivision,
|
Subdivision,
|
||||||
Eval,
|
Eval,
|
||||||
|
Operation,
|
||||||
|
LispOperation,
|
||||||
)
|
)
|
||||||
from .classes.items import (
|
from .classes.items import (
|
||||||
Whitespace,
|
Whitespace,
|
||||||
@ -29,7 +30,6 @@ from .classes.items import (
|
|||||||
RandomInteger,
|
RandomInteger,
|
||||||
Range,
|
Range,
|
||||||
Operator,
|
Operator,
|
||||||
Operation,
|
|
||||||
Atom,
|
Atom,
|
||||||
Integer,
|
Integer,
|
||||||
VariableAssignment,
|
VariableAssignment,
|
||||||
@ -138,6 +138,11 @@ class ZiffersTransformer(Transformer):
|
|||||||
text_value = items[0].value.replace("T", "10").replace("E", "11")
|
text_value = items[0].value.replace("T", "10").replace("E", "11")
|
||||||
return {"pitch_class": int(text_value), "text": items[0].value}
|
return {"pitch_class": int(text_value), "text": items[0].value}
|
||||||
|
|
||||||
|
def escaped_pitch(self, items):
|
||||||
|
"""Return escaped pitch"""
|
||||||
|
val = items[0].value[1:-1]
|
||||||
|
return {"pitch_class": int(val), "text": val}
|
||||||
|
|
||||||
def prefix(self, items):
|
def prefix(self, items):
|
||||||
"""Return prefix"""
|
"""Return prefix"""
|
||||||
return items[0]
|
return items[0]
|
||||||
@ -305,8 +310,7 @@ class ZiffersTransformer(Transformer):
|
|||||||
|
|
||||||
def eval(self, items):
|
def eval(self, items):
|
||||||
"""Parse eval"""
|
"""Parse eval"""
|
||||||
val = items[0]
|
return Eval(values=items)
|
||||||
return Eval(values=val)
|
|
||||||
|
|
||||||
def sub_operations(self, items):
|
def sub_operations(self, items):
|
||||||
"""Returns list of operations"""
|
"""Returns list of operations"""
|
||||||
@ -314,12 +318,16 @@ class ZiffersTransformer(Transformer):
|
|||||||
|
|
||||||
def operation(self, items):
|
def operation(self, items):
|
||||||
"""Return partial eval operations"""
|
"""Return partial eval operations"""
|
||||||
return flatten(items)
|
if isinstance(items[0], dict):
|
||||||
|
local_opts = items[0]
|
||||||
|
del local_opts["text"]
|
||||||
|
return Operation(values=flatten(items[1:]), local_options=items[0])
|
||||||
|
return Operation(values=flatten(items))
|
||||||
|
|
||||||
def atom(self, token):
|
def atom(self, token):
|
||||||
"""Return partial eval item"""
|
"""Return partial eval item"""
|
||||||
val = token[0].value
|
val = token[0].value
|
||||||
return Atom(value=val, text=val)
|
return Atom(value=val, text=str(val))
|
||||||
|
|
||||||
# Variable assignment
|
# Variable assignment
|
||||||
|
|
||||||
@ -340,6 +348,7 @@ class ZiffersTransformer(Transformer):
|
|||||||
return items[0].value
|
return items[0].value
|
||||||
|
|
||||||
def variable(self, items):
|
def variable(self, items):
|
||||||
|
"""Return variable"""
|
||||||
if len(items) > 1:
|
if len(items) > 1:
|
||||||
prefixes = sum_dict(items[0:-1])
|
prefixes = sum_dict(items[0:-1])
|
||||||
text_prefix = prefixes.pop("text")
|
text_prefix = prefixes.pop("text")
|
||||||
@ -405,9 +414,10 @@ class ZiffersTransformer(Transformer):
|
|||||||
)
|
)
|
||||||
return seq
|
return seq
|
||||||
|
|
||||||
def NUMBER(self, token):
|
def integer(self, items):
|
||||||
"""Parse integer"""
|
"""Parses integer from single ints"""
|
||||||
val = token.value
|
concatted = sum_dict(items)
|
||||||
|
val = concatted["text"]
|
||||||
return Integer(text=val, value=int(val))
|
return Integer(text=val, value=int(val))
|
||||||
|
|
||||||
def number(self, item):
|
def number(self, item):
|
||||||
@ -422,7 +432,7 @@ class ZiffersTransformer(Transformer):
|
|||||||
"""Parse lisp like list operation"""
|
"""Parse lisp like list operation"""
|
||||||
op = items[0]
|
op = items[0]
|
||||||
values = items[1:]
|
values = items[1:]
|
||||||
return Operation(
|
return LispOperation(
|
||||||
operator=op,
|
operator=op,
|
||||||
values=values,
|
values=values,
|
||||||
text="(+" + "".join([v.text for v in values]) + ")",
|
text="(+" + "".join([v.text for v in values]) + ")",
|
||||||
@ -433,6 +443,11 @@ class ZiffersTransformer(Transformer):
|
|||||||
val = token[0].value
|
val = token[0].value
|
||||||
return Operator(text=val, value=OPERATORS[val])
|
return Operator(text=val, value=OPERATORS[val])
|
||||||
|
|
||||||
|
def list_operator(self, token):
|
||||||
|
"""Parse list operator"""
|
||||||
|
val = token[0].value
|
||||||
|
return Operator(text=val, value=OPERATORS[val])
|
||||||
|
|
||||||
def list_items(self, items):
|
def list_items(self, items):
|
||||||
"""Parse sequence"""
|
"""Parse sequence"""
|
||||||
return Sequence(values=items)
|
return Sequence(values=items)
|
||||||
@ -483,30 +498,50 @@ class ZiffersTransformer(Transformer):
|
|||||||
|
|
||||||
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name, eval-used
|
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name, eval-used
|
||||||
class ScalaTransformer(Transformer):
|
class ScalaTransformer(Transformer):
|
||||||
|
"""Transformer for scala scales"""
|
||||||
|
|
||||||
def lines(self, items):
|
def lines(self, items):
|
||||||
cents = [ratio_to_cents(item) if isinstance(item,int) else item for item in items]
|
"""Transforms cents to semitones"""
|
||||||
|
cents = [
|
||||||
|
ratio_to_cents(item) if isinstance(item, int) else item for item in items
|
||||||
|
]
|
||||||
return cents_to_semitones(cents)
|
return cents_to_semitones(cents)
|
||||||
|
|
||||||
def operation(self, items):
|
def operation(self, items):
|
||||||
|
"""Get operation"""
|
||||||
# Safe eval. Items are pre-parsed.
|
# Safe eval. Items are pre-parsed.
|
||||||
val = eval("".join(str(item) for item in items))
|
val = eval("".join(str(item) for item in items))
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def operator(self, items):
|
def operator(self, items):
|
||||||
|
"""Get operator"""
|
||||||
return items[0].value
|
return items[0].value
|
||||||
|
|
||||||
def sub_operations(self, items):
|
def sub_operations(self, items):
|
||||||
|
"""Get sub-operation"""
|
||||||
return "(" + items[0] + ")"
|
return "(" + items[0] + ")"
|
||||||
|
|
||||||
def ratio(self, items):
|
def frac_ratio(self, items):
|
||||||
|
"""Get ration as fraction"""
|
||||||
ratio = items[0] / items[1]
|
ratio = items[0] / items[1]
|
||||||
return ratio_to_cents(ratio)
|
return ratio_to_cents(ratio)
|
||||||
|
|
||||||
|
def decimal_ratio(self, items):
|
||||||
|
"""Get ratio as decimal"""
|
||||||
|
ratio = float(str(items[0]) + "." + str(items[1]))
|
||||||
|
return ratio_to_cents(ratio)
|
||||||
|
|
||||||
|
def monzo(self, items):
|
||||||
|
"""Get monzo ratio"""
|
||||||
|
return monzo_to_cents(items)
|
||||||
|
|
||||||
def edo_ratio(self, items):
|
def edo_ratio(self, items):
|
||||||
|
"""Get EDO ratio"""
|
||||||
ratio = pow(2, items[0] / items[1])
|
ratio = pow(2, items[0] / items[1])
|
||||||
return ratio_to_cents(ratio)
|
return ratio_to_cents(ratio)
|
||||||
|
|
||||||
def edji_ratio(self, items):
|
def edji_ratio(self, items):
|
||||||
|
"""Get EDJI ratio"""
|
||||||
if len(items) > 3:
|
if len(items) > 3:
|
||||||
power = items[2] / items[3]
|
power = items[2] / items[3]
|
||||||
else:
|
else:
|
||||||
@ -515,12 +550,15 @@ class ScalaTransformer(Transformer):
|
|||||||
return ratio_to_cents(ratio)
|
return ratio_to_cents(ratio)
|
||||||
|
|
||||||
def int(self, items):
|
def int(self, items):
|
||||||
|
"""Get integer"""
|
||||||
return int(items[0].value)
|
return int(items[0].value)
|
||||||
|
|
||||||
def float(self, items):
|
def float(self, items):
|
||||||
|
"""Get float"""
|
||||||
return float(items[0].value)
|
return float(items[0].value)
|
||||||
|
|
||||||
def random_int(self, items):
|
def random_int(self, items):
|
||||||
|
"""Get random integer"""
|
||||||
|
|
||||||
def _rand_between(start, end):
|
def _rand_between(start, end):
|
||||||
return random.randint(min(start, end), max(start, end))
|
return random.randint(min(start, end), max(start, end))
|
||||||
@ -531,6 +569,7 @@ class ScalaTransformer(Transformer):
|
|||||||
return rand_val
|
return rand_val
|
||||||
|
|
||||||
def random_decimal(self, items):
|
def random_decimal(self, items):
|
||||||
|
"""Get random decimal"""
|
||||||
|
|
||||||
def _rand_between(start, end):
|
def _rand_between(start, end):
|
||||||
return random.uniform(min(start, end), max(start, end))
|
return random.uniform(min(start, end), max(start, end))
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# pylint: disable=locally-disabled, no-name-in-module
|
# pylint: disable=locally-disabled, no-name-in-module
|
||||||
import re
|
import re
|
||||||
from math import log2, floor, log
|
from math import log2
|
||||||
|
from itertools import islice
|
||||||
|
from .generators import gen_primes
|
||||||
from .common import repeat_text
|
from .common import repeat_text
|
||||||
from .defaults import (
|
from .defaults import (
|
||||||
SCALES,
|
SCALES,
|
||||||
@ -70,7 +72,7 @@ def note_name_to_midi(name: str) -> int:
|
|||||||
return 12 + octave * 12 + interval + modifier
|
return 12 + octave * 12 + interval + modifier
|
||||||
|
|
||||||
|
|
||||||
def get_scale(name: str) -> list[int]:
|
def get_scale(scale: str) -> list[int]:
|
||||||
"""Get a scale from the global scale list
|
"""Get a scale from the global scale list
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -79,7 +81,10 @@ def get_scale(name: str) -> list[int]:
|
|||||||
Returns:
|
Returns:
|
||||||
list: List of intervals in the scale
|
list: List of intervals in the scale
|
||||||
"""
|
"""
|
||||||
scale = SCALES.get(name.lower().capitalize(), SCALES["Ionian"])
|
if isinstance(scale, (list, tuple)):
|
||||||
|
return scale
|
||||||
|
|
||||||
|
scale = SCALES.get(scale.lower().capitalize(), SCALES["Ionian"])
|
||||||
return scale
|
return scale
|
||||||
|
|
||||||
|
|
||||||
@ -102,7 +107,11 @@ def get_scale_notes(name: str, root: int = 60, num_octaves: int = 1) -> list[int
|
|||||||
|
|
||||||
|
|
||||||
def get_chord_from_scale(
|
def get_chord_from_scale(
|
||||||
degree: int, root: int = 60, scale: str = "Major", num_notes: int = 3, skip: int = 2
|
degree: int,
|
||||||
|
root: int = 60,
|
||||||
|
scale: str | tuple = "Major",
|
||||||
|
num_notes: int = 3,
|
||||||
|
skip: int = 2,
|
||||||
) -> list[int]:
|
) -> list[int]:
|
||||||
"""Generate chord from the scale by skipping notes
|
"""Generate chord from the scale by skipping notes
|
||||||
|
|
||||||
@ -116,12 +125,17 @@ def get_chord_from_scale(
|
|||||||
Returns:
|
Returns:
|
||||||
list[int]: List of midi notes
|
list[int]: List of midi notes
|
||||||
"""
|
"""
|
||||||
num_of_octaves = ((num_notes * skip + degree) // get_scale_length(scale)) + 1
|
if isinstance(scale, str):
|
||||||
|
scale_length = get_scale_length(scale)
|
||||||
|
else:
|
||||||
|
scale_length = len(scale)
|
||||||
|
|
||||||
|
num_of_octaves = ((num_notes * skip + degree) // scale_length) + 1
|
||||||
scale_notes = get_scale_notes(scale, root, num_of_octaves)
|
scale_notes = get_scale_notes(scale, root, num_of_octaves)
|
||||||
return scale_notes[degree - 1 :: skip][:num_notes]
|
return scale_notes[degree - 1 :: skip][:num_notes]
|
||||||
|
|
||||||
|
|
||||||
def get_scale_length(name: str) -> int:
|
def get_scale_length(scale: str) -> int:
|
||||||
"""Get length of the scale
|
"""Get length of the scale
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -130,9 +144,11 @@ def get_scale_length(name: str) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
int: Length of the scale
|
int: Length of the scale
|
||||||
"""
|
"""
|
||||||
scale = SCALES.get(name.lower().capitalize(), SCALES["Ionian"])
|
if isinstance(scale, (list, tuple)):
|
||||||
return len(scale)
|
return len(scale)
|
||||||
|
|
||||||
|
return len(SCALES.get(scale.lower().capitalize(), SCALES["Ionian"]))
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=locally-disabled, too-many-arguments
|
# pylint: disable=locally-disabled, too-many-arguments
|
||||||
def note_from_pc(
|
def note_from_pc(
|
||||||
@ -271,9 +287,9 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple: Returns dict containing pitch-class values
|
tuple: Returns dict containing pitch-class values
|
||||||
"""
|
"""
|
||||||
pitch_class = note % 12
|
pitch_class = int(note % 12) # Cast to int "fixes" microtonal scales
|
||||||
octave = midi_to_octave(note) - 5
|
octave = midi_to_octave(note) - 5
|
||||||
if scale.upper() == "CHROMATIC":
|
if isinstance(scale, str) and scale.upper() == "CHROMATIC":
|
||||||
return {"text": str(pitch_class), "pitch_class": pitch_class, "octave": octave}
|
return {"text": str(pitch_class), "pitch_class": pitch_class, "octave": octave}
|
||||||
|
|
||||||
sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"]
|
sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"]
|
||||||
@ -319,7 +335,11 @@ def chord_from_degree(
|
|||||||
"""
|
"""
|
||||||
root = note_name_to_midi(root) if isinstance(root, str) else root
|
root = note_name_to_midi(root) if isinstance(root, str) else root
|
||||||
|
|
||||||
if name is None and scale.lower().capitalize() == "Chromatic":
|
if (
|
||||||
|
name is None
|
||||||
|
and isinstance(scale, str)
|
||||||
|
and scale.lower().capitalize() == "Chromatic"
|
||||||
|
):
|
||||||
name = "major"
|
name = "major"
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
@ -387,6 +407,32 @@ def cents_to_semitones(cents: list) -> tuple[float]:
|
|||||||
semitone_scale.append(semitone_interval)
|
semitone_scale.append(semitone_interval)
|
||||||
return tuple(semitone_scale)
|
return tuple(semitone_scale)
|
||||||
|
|
||||||
|
|
||||||
def ratio_to_cents(ratio: float) -> float:
|
def ratio_to_cents(ratio: float) -> float:
|
||||||
"""Transform ratio to cents"""
|
"""Transform ratio to cents"""
|
||||||
return 1200.0 * log(float(ratio), 2)
|
return 1200.0 * log2(float(ratio))
|
||||||
|
|
||||||
|
|
||||||
|
def monzo_to_cents(monzo) -> float:
|
||||||
|
"""
|
||||||
|
Convert a monzo to cents using the prime factorization method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
monzo (list): A list of integers representing the exponents of the prime factorization
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The value in cents
|
||||||
|
"""
|
||||||
|
# Calculate the prime factors of the indices in the monzo
|
||||||
|
max_index = len(monzo)
|
||||||
|
primes = list(islice(gen_primes(), max_index + 1))
|
||||||
|
|
||||||
|
# Product of the prime factors raised to the corresponding exponents
|
||||||
|
ratio = 1
|
||||||
|
for i in range(max_index):
|
||||||
|
ratio *= primes[i] ** monzo[i]
|
||||||
|
|
||||||
|
# Frequency ratio to cents
|
||||||
|
cents = 1200 * log2(ratio)
|
||||||
|
|
||||||
|
return cents
|
||||||
|
|||||||
@ -1,20 +1,24 @@
|
|||||||
?root: lines
|
?root: lines
|
||||||
|
|
||||||
lines: (number | operation | ratio | edo_ratio | edji_ratio)+
|
lines: (number | ratio | monzo | operation)+
|
||||||
|
|
||||||
operation: (number | ratio) (operator ((number | ratio) | sub_operations | operation))+
|
|
||||||
ratio: (int | random_int) "/" (int | random_int)
|
|
||||||
edo_ratio: (int | random_int) "\\" (int | random_int)
|
|
||||||
edji_ratio: (int | random_int) "\\" (int | random_int) "<" (int | random_int) "/"? (int | random_int)? ">"
|
|
||||||
!operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>"
|
|
||||||
sub_operations: "(" operation ")"
|
|
||||||
|
|
||||||
// Signed number without EXP
|
|
||||||
?number: float | int | random_int | random_float
|
?number: float | int | random_int | random_float
|
||||||
random_int: "(" int "," int ")"
|
random_int: "(" int "," int ")"
|
||||||
random_float: "(" float "," float ")"
|
random_float: "(" float "," float ")"
|
||||||
float: /(-?[0-9]+\.[0-9]*)|(\.[0-9]+)/
|
float: /(-?[0-9]+\.[0-9]*)|(\.[0-9]+)/
|
||||||
int: /[0-9]+/
|
int: /-?[0-9]+/
|
||||||
|
|
||||||
|
?ratio: frac_ratio | edo_ratio | edji_ratio | decimal_ratio
|
||||||
|
frac_ratio: (int | random_int) "/" (int | random_int)
|
||||||
|
edo_ratio: (int | random_int) "\\" (int | random_int)
|
||||||
|
edji_ratio: (int | random_int) "\\" (int | random_int) "<" (int | random_int) "/"? (int | random_int)? ">"
|
||||||
|
decimal_ratio: int "," int
|
||||||
|
|
||||||
|
monzo: "[" int+ ">"
|
||||||
|
|
||||||
|
operation: (number | ratio | monzo) (operator ((number | ratio | monzo) | sub_operations | operation))+
|
||||||
|
!operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>"
|
||||||
|
sub_operations: "(" operation ")"
|
||||||
|
|
||||||
%import common.WS
|
%import common.WS
|
||||||
%ignore WS
|
%ignore WS
|
||||||
@ -3,7 +3,7 @@
|
|||||||
sequence: (pitch_class | repeat_item | assignment | variable | variablelist | rest | dur_change | oct_mod | oct_change | WS | measure | chord | named_roman | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)*
|
sequence: (pitch_class | repeat_item | assignment | variable | variablelist | rest | dur_change | oct_mod | oct_change | WS | measure | 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 classes
|
||||||
pitch_class: prefix* pitch
|
pitch_class: prefix* (pitch | escaped_pitch)
|
||||||
prefix: (octave | duration_chars | escaped_decimal | escaped_octave | modifier)
|
prefix: (octave | duration_chars | escaped_decimal | escaped_octave | modifier)
|
||||||
pitch: /-?[0-9TE]/
|
pitch: /-?[0-9TE]/
|
||||||
escaped_decimal: "<" decimal ">"
|
escaped_decimal: "<" decimal ">"
|
||||||
@ -39,10 +39,9 @@
|
|||||||
invert: /%-?[0-9][0-9]*/
|
invert: /%-?[0-9][0-9]*/
|
||||||
|
|
||||||
// Valid as integer
|
// Valid as integer
|
||||||
number: NUMBER | random_integer | cycle
|
number: integer | random_integer | cycle
|
||||||
|
integer: pitch+
|
||||||
// CYCLIC NUMBERS NOT IN USE. NUMBERS MUST BE VALIDATED FROM FULL CYCLES!
|
escaped_pitch: /{-?[0-9]+}/
|
||||||
// cyclic_number: "<" number (WS number)* ">"
|
|
||||||
|
|
||||||
// Repeats
|
// Repeats
|
||||||
repeat: "[:" sequence ":" [number] "]"
|
repeat: "[:" sequence ":" [number] "]"
|
||||||
@ -53,10 +52,12 @@
|
|||||||
repeated_list: prefix* "(:" sequence ":" [number] ")"
|
repeated_list: prefix* "(:" sequence ":" [number] ")"
|
||||||
|
|
||||||
// Right recursive list operation
|
// Right recursive list operation
|
||||||
list_op: list (operator right_op)+
|
list_op: list ((operator | list_operator) right_op)+
|
||||||
right_op: list | number
|
right_op: list | number
|
||||||
//operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" | "@" | "#"
|
// Operators that work only on lists: | << >>
|
||||||
operator: /([\+\-\*\/%\|\&]|<<|>>|@|#|<>)/
|
list_operator: /(\||<<|>>|<>|#|@)(?=[(\d])/
|
||||||
|
// Common operators that works with numbers 3+5 3-5 etc.
|
||||||
|
operator: /([\+\-\*\/%\&])/
|
||||||
|
|
||||||
// Euclidean cycles
|
// Euclidean cycles
|
||||||
// TODO: Support randomization etc.
|
// TODO: Support randomization etc.
|
||||||
@ -88,13 +89,9 @@
|
|||||||
random_percent: /(%)(?!\d)/
|
random_percent: /(%)(?!\d)/
|
||||||
|
|
||||||
// Rules for evaluating clauses inside {}
|
// Rules for evaluating clauses inside {}
|
||||||
// TODO: Support for parenthesis?
|
eval: "{" ((operation | rest) WS?)+ "}"
|
||||||
eval: "{" operation+ "}"
|
operation: prefix? atom (operator (sub_operations | operation))*
|
||||||
operation: atom (operator (sub_operations | operation))*
|
|
||||||
sub_operations: "(" operation ")"
|
sub_operations: "(" operation ")"
|
||||||
atom: (NUMBER | DECIMAL)
|
atom: number
|
||||||
|
|
||||||
%import common.NUMBER
|
|
||||||
//%import common.SIGNED_NUMBER
|
|
||||||
%import common.DECIMAL
|
|
||||||
%import common.WS
|
%import common.WS
|
||||||
Reference in New Issue
Block a user