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:
2023-03-16 22:29:24 +02:00
parent 882a9a7b4b
commit 5d122a90e0
11 changed files with 284 additions and 102 deletions

View File

@ -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):

View File

@ -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"""

View File

@ -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:

View File

@ -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):
@ -343,7 +346,7 @@ class Subdivision(Sequence):
if isinstance(item, Event): if isinstance(item, Event):
if duration is not None: if duration is not None:
item.duration = new_d item.duration = new_d
item.beat = new_d*4 item.beat = new_d * 4
yield item yield item
@ -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):

View File

@ -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,14 +24,16 @@ 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:
return pos*times return pos * times
if times<0: if times < 0:
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
View 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

View File

@ -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,44 +498,67 @@ 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):
ratio = items[0]/items[1] """Get ration as fraction"""
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):
ratio = pow(2,items[0]/items[1]) """Get EDO ratio"""
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):
if len(items)>3: """Get EDJI ratio"""
power = items[2]/items[3] if len(items) > 3:
power = items[2] / items[3]
else: else:
power = items[2] power = items[2]
ratio = pow(power,items[0]/items[1]) ratio = pow(power, items[0] / items[1])
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))

View File

@ -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,8 +144,10 @@ 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
@ -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

View File

@ -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

View File

@ -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