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]]),
|
||||
("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):
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
from .items import *
|
||||
from .root import *
|
||||
from .sequences import *
|
||||
from .sequences import *
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
""" Ziffers item classes """
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from math import floor
|
||||
import operator
|
||||
import random
|
||||
from ..scale import (
|
||||
note_from_pc,
|
||||
@ -149,6 +148,26 @@ class Event(Item):
|
||||
class Rest(Event):
|
||||
"""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
|
||||
class Measure(Item):
|
||||
@ -283,9 +302,16 @@ class RandomPitch(Event):
|
||||
Returns:
|
||||
int: Returns random pitch
|
||||
"""
|
||||
return random.randint(
|
||||
0, get_scale_length(options.get("scale", "Major")) if options else 9
|
||||
)
|
||||
if options:
|
||||
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)
|
||||
@ -604,14 +630,6 @@ class Operator(Item):
|
||||
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)
|
||||
class Atom(Item):
|
||||
"""Class for evaluable atoms"""
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
from itertools import islice, cycle
|
||||
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
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ class Ziffers(Sequence):
|
||||
|
||||
def __len__(self):
|
||||
return len(self.evaluated_values)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
@ -107,15 +107,15 @@ class Ziffers(Sequence):
|
||||
return [
|
||||
val.get_pitch_class()
|
||||
for val in self.evaluated_values
|
||||
if isinstance(val, (Pitch, Chord))
|
||||
if isinstance(val, (Pitch, Chord, Rest))
|
||||
]
|
||||
|
||||
|
||||
def pitch_bends(self) -> list[int]:
|
||||
"""Return list of pitch bend values"""
|
||||
return [
|
||||
val.get_pitch_bend()
|
||||
for val in self.evaluated_values
|
||||
if isinstance(val, (Pitch, Chord))
|
||||
if isinstance(val, (Pitch, Chord, Rest))
|
||||
]
|
||||
|
||||
def notes(self) -> list[int]:
|
||||
@ -123,7 +123,7 @@ class Ziffers(Sequence):
|
||||
return [
|
||||
val.get_note()
|
||||
for val in self.evaluated_values
|
||||
if isinstance(val, (Pitch, Chord))
|
||||
if isinstance(val, (Pitch, Chord, Rest))
|
||||
]
|
||||
|
||||
def durations(self) -> list[float]:
|
||||
@ -133,10 +133,12 @@ class Ziffers(Sequence):
|
||||
for val in self.evaluated_values
|
||||
if isinstance(val, Event)
|
||||
]
|
||||
|
||||
|
||||
def total_duration(self) -> float:
|
||||
"""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:
|
||||
"""Return total beats"""
|
||||
@ -161,7 +163,7 @@ class Ziffers(Sequence):
|
||||
return [
|
||||
val.get_octave()
|
||||
for val in self.evaluated_values
|
||||
if isinstance(val, (Pitch, Chord))
|
||||
if isinstance(val, (Pitch, Chord, Rest))
|
||||
]
|
||||
|
||||
def freqs(self) -> list[int]:
|
||||
@ -169,9 +171,9 @@ class Ziffers(Sequence):
|
||||
return [
|
||||
val.get_freq()
|
||||
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:
|
||||
"""Collect n items from parsed Ziffers"""
|
||||
if num is None:
|
||||
|
||||
@ -4,8 +4,9 @@ from itertools import product
|
||||
from math import floor
|
||||
from types import LambdaType
|
||||
from copy import deepcopy
|
||||
import operator
|
||||
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 .items import (
|
||||
Meta,
|
||||
@ -31,7 +32,7 @@ from .items import (
|
||||
Modification,
|
||||
Whitespace,
|
||||
Sample,
|
||||
SampleList
|
||||
SampleList,
|
||||
)
|
||||
|
||||
|
||||
@ -46,6 +47,8 @@ def resolve_item(item: Meta, options: dict):
|
||||
elif isinstance(item, Subdivision):
|
||||
item.evaluate_values(options)
|
||||
yield item
|
||||
elif isinstance(item, Eval):
|
||||
yield from item.evaluate_values(options)
|
||||
else:
|
||||
yield from item.evaluate_tree(options)
|
||||
elif isinstance(item, VariableAssignment):
|
||||
@ -66,14 +69,14 @@ def resolve_item(item: Meta, options: dict):
|
||||
run=opt_item,
|
||||
text=item.text,
|
||||
kwargs=(options | item.local_options),
|
||||
local_options=item.local_options
|
||||
local_options=item.local_options,
|
||||
)
|
||||
elif isinstance(opt_item, str):
|
||||
yield Sample(
|
||||
name=opt_item,
|
||||
text=item.text,
|
||||
kwargs=(options | item.local_options),
|
||||
local_options=item.local_options
|
||||
local_options=item.local_options,
|
||||
)
|
||||
variable = deepcopy(opt_item)
|
||||
yield from resolve_item(variable, options)
|
||||
@ -90,7 +93,7 @@ def resolve_item(item: Meta, options: dict):
|
||||
run=opt_item,
|
||||
text=var.text,
|
||||
kwargs=(options | var.local_options),
|
||||
local_options=var.local_options
|
||||
local_options=var.local_options,
|
||||
)
|
||||
)
|
||||
elif isinstance(opt_item, str):
|
||||
@ -99,7 +102,7 @@ def resolve_item(item: Meta, options: dict):
|
||||
name=opt_item,
|
||||
text=var.text,
|
||||
kwargs=(options | var.local_options),
|
||||
local_options=var.local_options
|
||||
local_options=var.local_options,
|
||||
)
|
||||
)
|
||||
elif isinstance(opt_item, Sequence):
|
||||
@ -343,7 +346,7 @@ class Subdivision(Sequence):
|
||||
if isinstance(item, Event):
|
||||
if duration is not None:
|
||||
item.duration = new_d
|
||||
item.beat = new_d*4
|
||||
item.beat = new_d * 4
|
||||
yield item
|
||||
|
||||
|
||||
@ -373,6 +376,8 @@ class ListOperation(Sequence):
|
||||
flattened_list.extend(list(item.evaluate_durations()))
|
||||
elif isinstance(item, RepeatedListSequence):
|
||||
flattened_list.extend(list(item.resolve_repeat(options)))
|
||||
elif isinstance(item, Eval):
|
||||
flattened_list.extend(item.evaluate_values(options))
|
||||
else:
|
||||
flattened_list.append(_filter_operation(item, options))
|
||||
elif isinstance(item, Cyclic):
|
||||
@ -543,13 +548,43 @@ class ListOperation(Sequence):
|
||||
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)
|
||||
# def __post_init__(self):
|
||||
# self.text = "".join([val.text for val in flatten(self.values)])
|
||||
# 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)
|
||||
@ -588,6 +623,8 @@ class RepeatedSequence(Sequence):
|
||||
elif isinstance(item, Subdivision):
|
||||
item.evaluate_values(options)
|
||||
yield item
|
||||
elif isinstance(item, Eval):
|
||||
yield from item.evaluate_values(options)
|
||||
else:
|
||||
yield from item
|
||||
elif isinstance(item, Cyclic):
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import re
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
def flatten(arr: list) -> list:
|
||||
"""Flattens array"""
|
||||
return (
|
||||
@ -23,14 +24,16 @@ def rotate(arr, k):
|
||||
arr = arr[-k:] + arr[:-k]
|
||||
return arr
|
||||
|
||||
def repeat_text(pos,neg,times):
|
||||
|
||||
def repeat_text(pos, neg, times):
|
||||
"""Helper to repeat text"""
|
||||
if times>0:
|
||||
return pos*times
|
||||
if times<0:
|
||||
return neg*abs(times)
|
||||
if times > 0:
|
||||
return pos * times
|
||||
if times < 0:
|
||||
return neg * abs(times)
|
||||
return ""
|
||||
|
||||
|
||||
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]
|
||||
@ -110,4 +113,4 @@ def cyclic_zip(first: list, second: list) -> list:
|
||||
f_length = len(first)
|
||||
for i in range(s_length):
|
||||
result.append([first[i % f_length], second[i]])
|
||||
return [deepcopy(item) for sublist in result for item in sublist]
|
||||
return [deepcopy(item) for sublist in result for item in sublist]
|
||||
|
||||
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 """
|
||||
import random
|
||||
from math import log, pow
|
||||
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.sequences import (
|
||||
Sequence,
|
||||
@ -13,6 +12,8 @@ from .classes.sequences import (
|
||||
Euclid,
|
||||
Subdivision,
|
||||
Eval,
|
||||
Operation,
|
||||
LispOperation,
|
||||
)
|
||||
from .classes.items import (
|
||||
Whitespace,
|
||||
@ -29,7 +30,6 @@ from .classes.items import (
|
||||
RandomInteger,
|
||||
Range,
|
||||
Operator,
|
||||
Operation,
|
||||
Atom,
|
||||
Integer,
|
||||
VariableAssignment,
|
||||
@ -138,6 +138,11 @@ class ZiffersTransformer(Transformer):
|
||||
text_value = items[0].value.replace("T", "10").replace("E", "11")
|
||||
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):
|
||||
"""Return prefix"""
|
||||
return items[0]
|
||||
@ -305,8 +310,7 @@ class ZiffersTransformer(Transformer):
|
||||
|
||||
def eval(self, items):
|
||||
"""Parse eval"""
|
||||
val = items[0]
|
||||
return Eval(values=val)
|
||||
return Eval(values=items)
|
||||
|
||||
def sub_operations(self, items):
|
||||
"""Returns list of operations"""
|
||||
@ -314,12 +318,16 @@ class ZiffersTransformer(Transformer):
|
||||
|
||||
def operation(self, items):
|
||||
"""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):
|
||||
"""Return partial eval item"""
|
||||
val = token[0].value
|
||||
return Atom(value=val, text=val)
|
||||
return Atom(value=val, text=str(val))
|
||||
|
||||
# Variable assignment
|
||||
|
||||
@ -340,6 +348,7 @@ class ZiffersTransformer(Transformer):
|
||||
return items[0].value
|
||||
|
||||
def variable(self, items):
|
||||
"""Return variable"""
|
||||
if len(items) > 1:
|
||||
prefixes = sum_dict(items[0:-1])
|
||||
text_prefix = prefixes.pop("text")
|
||||
@ -405,9 +414,10 @@ class ZiffersTransformer(Transformer):
|
||||
)
|
||||
return seq
|
||||
|
||||
def NUMBER(self, token):
|
||||
"""Parse integer"""
|
||||
val = token.value
|
||||
def integer(self, items):
|
||||
"""Parses integer from single ints"""
|
||||
concatted = sum_dict(items)
|
||||
val = concatted["text"]
|
||||
return Integer(text=val, value=int(val))
|
||||
|
||||
def number(self, item):
|
||||
@ -422,7 +432,7 @@ class ZiffersTransformer(Transformer):
|
||||
"""Parse lisp like list operation"""
|
||||
op = items[0]
|
||||
values = items[1:]
|
||||
return Operation(
|
||||
return LispOperation(
|
||||
operator=op,
|
||||
values=values,
|
||||
text="(+" + "".join([v.text for v in values]) + ")",
|
||||
@ -433,6 +443,11 @@ class ZiffersTransformer(Transformer):
|
||||
val = token[0].value
|
||||
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):
|
||||
"""Parse sequence"""
|
||||
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
|
||||
class ScalaTransformer(Transformer):
|
||||
"""Transformer for scala scales"""
|
||||
|
||||
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)
|
||||
|
||||
def operation(self, items):
|
||||
"""Get operation"""
|
||||
# Safe eval. Items are pre-parsed.
|
||||
val = eval("".join(str(item) for item in items))
|
||||
return val
|
||||
|
||||
def operator(self, items):
|
||||
"""Get operator"""
|
||||
return items[0].value
|
||||
|
||||
def sub_operations(self, items):
|
||||
"""Get sub-operation"""
|
||||
return "(" + items[0] + ")"
|
||||
|
||||
def ratio(self, items):
|
||||
ratio = items[0]/items[1]
|
||||
def frac_ratio(self, items):
|
||||
"""Get ration as fraction"""
|
||||
ratio = items[0] / items[1]
|
||||
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):
|
||||
ratio = pow(2,items[0]/items[1])
|
||||
"""Get EDO ratio"""
|
||||
ratio = pow(2, items[0] / items[1])
|
||||
return ratio_to_cents(ratio)
|
||||
|
||||
|
||||
def edji_ratio(self, items):
|
||||
if len(items)>3:
|
||||
power = items[2]/items[3]
|
||||
"""Get EDJI ratio"""
|
||||
if len(items) > 3:
|
||||
power = items[2] / items[3]
|
||||
else:
|
||||
power = items[2]
|
||||
ratio = pow(power,items[0]/items[1])
|
||||
ratio = pow(power, items[0] / items[1])
|
||||
return ratio_to_cents(ratio)
|
||||
|
||||
|
||||
def int(self, items):
|
||||
"""Get integer"""
|
||||
return int(items[0].value)
|
||||
|
||||
|
||||
def float(self, items):
|
||||
"""Get float"""
|
||||
return float(items[0].value)
|
||||
|
||||
|
||||
def random_int(self, items):
|
||||
"""Get random integer"""
|
||||
|
||||
def _rand_between(start, end):
|
||||
return random.randint(min(start, end), max(start, end))
|
||||
@ -529,8 +567,9 @@ class ScalaTransformer(Transformer):
|
||||
end = items[1]
|
||||
rand_val = _rand_between(start, end)
|
||||
return rand_val
|
||||
|
||||
|
||||
def random_decimal(self, items):
|
||||
"""Get random decimal"""
|
||||
|
||||
def _rand_between(start, end):
|
||||
return random.uniform(min(start, end), max(start, end))
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# pylint: disable=locally-disabled, no-name-in-module
|
||||
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 .defaults import (
|
||||
SCALES,
|
||||
@ -70,7 +72,7 @@ def note_name_to_midi(name: str) -> int:
|
||||
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
|
||||
|
||||
Args:
|
||||
@ -79,7 +81,10 @@ def get_scale(name: str) -> list[int]:
|
||||
Returns:
|
||||
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
|
||||
|
||||
|
||||
@ -102,7 +107,11 @@ def get_scale_notes(name: str, root: int = 60, num_octaves: int = 1) -> list[int
|
||||
|
||||
|
||||
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]:
|
||||
"""Generate chord from the scale by skipping notes
|
||||
|
||||
@ -116,12 +125,17 @@ def get_chord_from_scale(
|
||||
Returns:
|
||||
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)
|
||||
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
|
||||
|
||||
Args:
|
||||
@ -130,8 +144,10 @@ def get_scale_length(name: str) -> int:
|
||||
Returns:
|
||||
int: Length of the scale
|
||||
"""
|
||||
scale = SCALES.get(name.lower().capitalize(), SCALES["Ionian"])
|
||||
return len(scale)
|
||||
if isinstance(scale, (list, tuple)):
|
||||
return len(scale)
|
||||
|
||||
return len(SCALES.get(scale.lower().capitalize(), SCALES["Ionian"]))
|
||||
|
||||
|
||||
# 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:
|
||||
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
|
||||
if scale.upper() == "CHROMATIC":
|
||||
if isinstance(scale, str) and scale.upper() == "CHROMATIC":
|
||||
return {"text": str(pitch_class), "pitch_class": pitch_class, "octave": octave}
|
||||
|
||||
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
|
||||
|
||||
if name is None and scale.lower().capitalize() == "Chromatic":
|
||||
if (
|
||||
name is None
|
||||
and isinstance(scale, str)
|
||||
and scale.lower().capitalize() == "Chromatic"
|
||||
):
|
||||
name = "major"
|
||||
|
||||
if name:
|
||||
@ -387,6 +407,32 @@ def cents_to_semitones(cents: list) -> tuple[float]:
|
||||
semitone_scale.append(semitone_interval)
|
||||
return tuple(semitone_scale)
|
||||
|
||||
|
||||
def ratio_to_cents(ratio: float) -> float:
|
||||
"""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
|
||||
|
||||
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
|
||||
random_int: "(" int "," int ")"
|
||||
random_float: "(" float "," float ")"
|
||||
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
|
||||
%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)*
|
||||
|
||||
// Pitch classes
|
||||
pitch_class: prefix* pitch
|
||||
pitch_class: prefix* (pitch | escaped_pitch)
|
||||
prefix: (octave | duration_chars | escaped_decimal | escaped_octave | modifier)
|
||||
pitch: /-?[0-9TE]/
|
||||
escaped_decimal: "<" decimal ">"
|
||||
@ -39,10 +39,9 @@
|
||||
invert: /%-?[0-9][0-9]*/
|
||||
|
||||
// Valid as integer
|
||||
number: NUMBER | random_integer | cycle
|
||||
|
||||
// CYCLIC NUMBERS NOT IN USE. NUMBERS MUST BE VALIDATED FROM FULL CYCLES!
|
||||
// cyclic_number: "<" number (WS number)* ">"
|
||||
number: integer | random_integer | cycle
|
||||
integer: pitch+
|
||||
escaped_pitch: /{-?[0-9]+}/
|
||||
|
||||
// Repeats
|
||||
repeat: "[:" sequence ":" [number] "]"
|
||||
@ -53,10 +52,12 @@
|
||||
repeated_list: prefix* "(:" sequence ":" [number] ")"
|
||||
|
||||
// Right recursive list operation
|
||||
list_op: list (operator right_op)+
|
||||
list_op: list ((operator | list_operator) right_op)+
|
||||
right_op: list | number
|
||||
//operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" | "@" | "#"
|
||||
operator: /([\+\-\*\/%\|\&]|<<|>>|@|#|<>)/
|
||||
// Operators that work only on lists: | << >>
|
||||
list_operator: /(\||<<|>>|<>|#|@)(?=[(\d])/
|
||||
// Common operators that works with numbers 3+5 3-5 etc.
|
||||
operator: /([\+\-\*\/%\&])/
|
||||
|
||||
// Euclidean cycles
|
||||
// TODO: Support randomization etc.
|
||||
@ -88,13 +89,9 @@
|
||||
random_percent: /(%)(?!\d)/
|
||||
|
||||
// Rules for evaluating clauses inside {}
|
||||
// TODO: Support for parenthesis?
|
||||
eval: "{" operation+ "}"
|
||||
operation: atom (operator (sub_operations | operation))*
|
||||
eval: "{" ((operation | rest) WS?)+ "}"
|
||||
operation: prefix? atom (operator (sub_operations | operation))*
|
||||
sub_operations: "(" operation ")"
|
||||
atom: (NUMBER | DECIMAL)
|
||||
atom: number
|
||||
|
||||
%import common.NUMBER
|
||||
//%import common.SIGNED_NUMBER
|
||||
%import common.DECIMAL
|
||||
%import common.WS
|
||||
Reference in New Issue
Block a user