Added experimental scala scale parser
This commit is contained in:
@ -47,5 +47,5 @@ def test_notenames(name: str, expected: int):
|
|||||||
)
|
)
|
||||||
def test_note_to_midi(pitch_classes: str, expected: int):
|
def test_note_to_midi(pitch_classes: str, expected: int):
|
||||||
assert [
|
assert [
|
||||||
scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pitch_classes
|
scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian")[0] for val in pitch_classes
|
||||||
] == expected
|
] == expected
|
||||||
|
|||||||
@ -5,3 +5,4 @@ from .defaults import *
|
|||||||
from .scale import *
|
from .scale import *
|
||||||
from .converters import *
|
from .converters import *
|
||||||
from .spec import *
|
from .spec import *
|
||||||
|
from .classes import *
|
||||||
@ -1,3 +1,3 @@
|
|||||||
from .items import *
|
from .items import *
|
||||||
from .sequences import *
|
|
||||||
from .root import *
|
from .root import *
|
||||||
|
from .sequences import *
|
||||||
@ -1,5 +1,6 @@
|
|||||||
""" Ziffers item classes """
|
""" Ziffers item classes """
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from math import floor
|
||||||
import operator
|
import operator
|
||||||
import random
|
import random
|
||||||
from ..scale import (
|
from ..scale import (
|
||||||
@ -171,6 +172,7 @@ class Pitch(Event):
|
|||||||
"""Class for pitch in time"""
|
"""Class for pitch in time"""
|
||||||
|
|
||||||
pitch_class: int
|
pitch_class: int
|
||||||
|
pitch_bend: float = field(default=None)
|
||||||
octave: int = field(default=None)
|
octave: int = field(default=None)
|
||||||
modifier: int = field(default=None)
|
modifier: int = field(default=None)
|
||||||
note: int = field(default=None)
|
note: int = field(default=None)
|
||||||
@ -206,6 +208,10 @@ class Pitch(Event):
|
|||||||
def get_pitch_class(self):
|
def get_pitch_class(self):
|
||||||
"""Getter for pitche"""
|
"""Getter for pitche"""
|
||||||
return self.pitch_class
|
return self.pitch_class
|
||||||
|
|
||||||
|
def get_pitch_bend(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return self.pitch_bend
|
||||||
|
|
||||||
def update_note(self, force: bool = False):
|
def update_note(self, force: bool = False):
|
||||||
"""Update note if Key, Scale and Pitch-class are present"""
|
"""Update note if Key, Scale and Pitch-class are present"""
|
||||||
@ -215,15 +221,16 @@ class Pitch(Event):
|
|||||||
and (self.pitch_class is not None)
|
and (self.pitch_class is not None)
|
||||||
and (self.note is None or force)
|
and (self.note is None or force)
|
||||||
):
|
):
|
||||||
note = note_from_pc(
|
note, pitch_bend = note_from_pc(
|
||||||
root=self.key,
|
root=self.key,
|
||||||
pitch_class=self.pitch_class,
|
pitch_class=self.pitch_class,
|
||||||
intervals=self.scale,
|
intervals=self.scale,
|
||||||
modifier=self.modifier if self.modifier is not None else 0,
|
modifier=self.modifier if self.modifier is not None else 0,
|
||||||
octave=self.octave if self.octave is not None else 0,
|
octave=self.octave if self.octave is not None else 0,
|
||||||
)
|
)
|
||||||
|
self.pitch_bend = pitch_bend
|
||||||
self.freq = midi_to_freq(note)
|
self.freq = midi_to_freq(note)
|
||||||
self.note = note
|
self.note = floor(note)
|
||||||
if self.duration is not None:
|
if self.duration is not None:
|
||||||
self.beat = self.duration * 4
|
self.beat = self.duration * 4
|
||||||
|
|
||||||
@ -296,6 +303,7 @@ class Chord(Event):
|
|||||||
notes: list[int] = field(default=None)
|
notes: list[int] = field(default=None)
|
||||||
inversions: int = field(default=None)
|
inversions: int = field(default=None)
|
||||||
pitches: list[int] = field(default=None, init=False)
|
pitches: list[int] = field(default=None, init=False)
|
||||||
|
pitch_bends: list[int] = field(default=None, init=False)
|
||||||
freqs: list[float] = field(default=None, init=False)
|
freqs: list[float] = field(default=None, init=False)
|
||||||
octaves: list[int] = field(default=None, init=False)
|
octaves: list[int] = field(default=None, init=False)
|
||||||
durations: list[float] = field(default=None, init=False)
|
durations: list[float] = field(default=None, init=False)
|
||||||
@ -333,6 +341,10 @@ class Chord(Event):
|
|||||||
def get_pitch_class(self):
|
def get_pitch_class(self):
|
||||||
"""Getter for pitches"""
|
"""Getter for pitches"""
|
||||||
return self.pitches
|
return self.pitches
|
||||||
|
|
||||||
|
def get_pitch_bend(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return self.pitch_bends
|
||||||
|
|
||||||
def get_duration(self):
|
def get_duration(self):
|
||||||
"""Getter for durations"""
|
"""Getter for durations"""
|
||||||
@ -353,7 +365,7 @@ class Chord(Event):
|
|||||||
|
|
||||||
def update_notes(self, options=None):
|
def update_notes(self, options=None):
|
||||||
"""Update notes"""
|
"""Update notes"""
|
||||||
pitches, notes, freqs, octaves, durations, beats = ([] for _ in range(6))
|
pitches, pitch_bends, notes, freqs, octaves, durations, beats = ([] for _ in range(7))
|
||||||
|
|
||||||
# Update notes
|
# Update notes
|
||||||
for pitch in self.pitch_classes:
|
for pitch in self.pitch_classes:
|
||||||
@ -367,6 +379,7 @@ class Chord(Event):
|
|||||||
# Create helper lists
|
# Create helper lists
|
||||||
for pitch in self.pitch_classes:
|
for pitch in self.pitch_classes:
|
||||||
pitches.append(pitch.pitch_class)
|
pitches.append(pitch.pitch_class)
|
||||||
|
pitch_bends.append(pitch.pitch_bend)
|
||||||
notes.append(pitch.note)
|
notes.append(pitch.note)
|
||||||
freqs.append(pitch.freq)
|
freqs.append(pitch.freq)
|
||||||
octaves.append(pitch.octave)
|
octaves.append(pitch.octave)
|
||||||
@ -374,6 +387,7 @@ class Chord(Event):
|
|||||||
beats.append(pitch.beat)
|
beats.append(pitch.beat)
|
||||||
|
|
||||||
self.pitches = pitches
|
self.pitches = pitches
|
||||||
|
self.pitch_bends = pitch_bends
|
||||||
self.notes = notes
|
self.notes = notes
|
||||||
self.freqs = freqs
|
self.freqs = freqs
|
||||||
self.octaves = octaves
|
self.octaves = octaves
|
||||||
|
|||||||
@ -109,6 +109,14 @@ class Ziffers(Sequence):
|
|||||||
for val in self.evaluated_values
|
for val in self.evaluated_values
|
||||||
if isinstance(val, (Pitch, Chord))
|
if isinstance(val, (Pitch, Chord))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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))
|
||||||
|
]
|
||||||
|
|
||||||
def notes(self) -> list[int]:
|
def notes(self) -> list[int]:
|
||||||
"""Return list of midi notes"""
|
"""Return list of midi notes"""
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
""" Sequence classes for Ziffers """
|
""" Sequence classes for Ziffers """
|
||||||
from dataclasses import dataclass, field, replace
|
from dataclasses import dataclass, field, replace
|
||||||
from itertools import product
|
from itertools import product
|
||||||
|
from math import floor
|
||||||
from types import LambdaType
|
from types import LambdaType
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from ..defaults import DEFAULT_OPTIONS
|
from ..defaults import DEFAULT_OPTIONS
|
||||||
@ -190,7 +191,7 @@ def create_pitch(current: Item, options: dict) -> Pitch:
|
|||||||
|
|
||||||
current_value = current.get_value(merged_options)
|
current_value = current.get_value(merged_options)
|
||||||
|
|
||||||
note = note_from_pc(
|
note, pitch_bend = note_from_pc(
|
||||||
root=merged_options["key"],
|
root=merged_options["key"],
|
||||||
pitch_class=current_value,
|
pitch_class=current_value,
|
||||||
intervals=merged_options["scale"],
|
intervals=merged_options["scale"],
|
||||||
@ -200,8 +201,9 @@ def create_pitch(current: Item, options: dict) -> Pitch:
|
|||||||
new_pitch = Pitch(
|
new_pitch = Pitch(
|
||||||
pitch_class=current_value,
|
pitch_class=current_value,
|
||||||
text=str(current_value),
|
text=str(current_value),
|
||||||
note=note,
|
|
||||||
freq=midi_to_freq(note),
|
freq=midi_to_freq(note),
|
||||||
|
note=floor(note),
|
||||||
|
pitch_bend=pitch_bend,
|
||||||
octave=c_octave,
|
octave=c_octave,
|
||||||
modifier=c_modifier,
|
modifier=c_modifier,
|
||||||
kwargs=merged_options,
|
kwargs=merged_options,
|
||||||
|
|||||||
@ -110,4 +110,4 @@ def cyclic_zip(first: list, second: list) -> list:
|
|||||||
f_length = len(first)
|
f_length = len(first)
|
||||||
for i in range(s_length):
|
for i in range(s_length):
|
||||||
result.append([first[i % f_length], second[i]])
|
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]
|
||||||
3114
ziffers/defaults.py
3114
ziffers/defaults.py
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
|||||||
""" Lark transformer for mapping Lark tokens to Ziffers objects """
|
""" Lark transformer for mapping Lark tokens to Ziffers objects """
|
||||||
|
import random
|
||||||
|
from math import log
|
||||||
from lark import Transformer, Token
|
from lark import Transformer, Token
|
||||||
|
from .scale import cents_to_semitones
|
||||||
from .classes.root import Ziffers
|
from .classes.root import Ziffers
|
||||||
from .classes.sequences import (
|
from .classes.sequences import (
|
||||||
Sequence,
|
Sequence,
|
||||||
@ -337,15 +340,17 @@ class ZiffersTransformer(Transformer):
|
|||||||
return items[0].value
|
return items[0].value
|
||||||
|
|
||||||
def variable(self, items):
|
def variable(self, items):
|
||||||
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")
|
||||||
return Variable(name=items[-1], text=text_prefix+items[-1], local_options=prefixes)
|
return Variable(
|
||||||
|
name=items[-1], text=text_prefix + items[-1], local_options=prefixes
|
||||||
|
)
|
||||||
return Variable(name=items[0], text=items[0])
|
return Variable(name=items[0], text=items[0])
|
||||||
|
|
||||||
def variable_char(self, items):
|
def variable_char(self, items):
|
||||||
"""Return parsed variable name"""
|
"""Return parsed variable name"""
|
||||||
return items[0].value #Variable(name=items[0].value, text=items[0].value)
|
return items[0].value # Variable(name=items[0].value, text=items[0].value)
|
||||||
|
|
||||||
def variablelist(self, items):
|
def variablelist(self, items):
|
||||||
"""Return list of variables"""
|
"""Return list of variables"""
|
||||||
@ -474,3 +479,41 @@ class ZiffersTransformer(Transformer):
|
|||||||
wrap_start="",
|
wrap_start="",
|
||||||
wrap_end=":" + items[1].text,
|
wrap_end=":" + items[1].text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name
|
||||||
|
class ScalaTransformer(Transformer):
|
||||||
|
def lines(self, items):
|
||||||
|
return cents_to_semitones(items)
|
||||||
|
|
||||||
|
def operation(self, items):
|
||||||
|
val = eval("".join(str(item) for item in items))
|
||||||
|
return 1200.0 * log(float(val), 2)
|
||||||
|
|
||||||
|
def operator(self, items):
|
||||||
|
return items[0].value
|
||||||
|
|
||||||
|
def sub_operations(self, items):
|
||||||
|
return "(" + items[0] + ")"
|
||||||
|
|
||||||
|
def number(self, items):
|
||||||
|
val = items[0]
|
||||||
|
return float(val.value)
|
||||||
|
|
||||||
|
def random(self, items):
|
||||||
|
def _parse_type(val):
|
||||||
|
if "." in val:
|
||||||
|
return float(val)
|
||||||
|
else:
|
||||||
|
return int(val)
|
||||||
|
|
||||||
|
def _rand_between(start, end):
|
||||||
|
if isinstance(start, float) or isinstance(end, float):
|
||||||
|
return random.uniform(min(start, end), max(start, end))
|
||||||
|
elif isinstance(start, int) and isinstance(end, int):
|
||||||
|
return random.randint(min(start, end), max(start, end))
|
||||||
|
|
||||||
|
start = _parse_type(items[0].value)
|
||||||
|
end = _parse_type(items[1].value)
|
||||||
|
rand_val = _rand_between(start, end)
|
||||||
|
return 1200.0 * log(float(rand_val), 2)
|
||||||
|
|||||||
@ -3,21 +3,43 @@ from pathlib import Path
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from lark import Lark
|
from lark import Lark
|
||||||
from .classes.root import Ziffers
|
from .classes.root import Ziffers
|
||||||
from .mapper import ZiffersTransformer
|
from .mapper import ZiffersTransformer, ScalaTransformer
|
||||||
|
|
||||||
|
|
||||||
grammar_path = Path(__file__).parent
|
grammar_path = Path(__file__).parent
|
||||||
grammar_folder = Path.joinpath(grammar_path, "spec")
|
grammar_folder = Path.joinpath(grammar_path, "spec")
|
||||||
grammar_file = Path.joinpath(grammar_folder, "ziffers.lark")
|
ziffers_grammar = Path.joinpath(grammar_folder, "ziffers.lark")
|
||||||
|
scala_grammar = Path.joinpath(grammar_folder, "scala.lark")
|
||||||
|
|
||||||
ziffers_parser = Lark.open(
|
ziffers_parser = Lark.open(
|
||||||
str(grammar_file),
|
str(ziffers_grammar),
|
||||||
rel_to=__file__,
|
rel_to=__file__,
|
||||||
start="root",
|
start="root",
|
||||||
parser="lalr",
|
parser="lalr",
|
||||||
transformer=ZiffersTransformer(),
|
transformer=ZiffersTransformer(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
scala_parser = Lark.open(
|
||||||
|
str(scala_grammar),
|
||||||
|
rel_to=__file__,
|
||||||
|
start="root",
|
||||||
|
parser="lalr",
|
||||||
|
transformer=ScalaTransformer(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_scala(expr: str):
|
||||||
|
"""Parse an expression using the Ziffers parser
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (str): Ziffers expression as a string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ziffers: Reutrns Ziffers iterable
|
||||||
|
"""
|
||||||
|
# Ignore everything before last comment "!"
|
||||||
|
values = expr.split("!")[-1]
|
||||||
|
return scala_parser.parse(values)
|
||||||
|
|
||||||
|
|
||||||
def parse_expression(expr: str) -> Ziffers:
|
def parse_expression(expr: str) -> Ziffers:
|
||||||
"""Parse an expression using the Ziffers parser
|
"""Parse an expression using the Ziffers parser
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#!/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 floor
|
from math import log2, floor
|
||||||
from .common import repeat_text
|
from .common import repeat_text
|
||||||
from .defaults import (
|
from .defaults import (
|
||||||
SCALES,
|
SCALES,
|
||||||
@ -138,8 +138,7 @@ def get_scale_length(name: str) -> int:
|
|||||||
def note_from_pc(
|
def note_from_pc(
|
||||||
root: int | str,
|
root: int | str,
|
||||||
pitch_class: int,
|
pitch_class: int,
|
||||||
intervals: str | list[int | float],
|
intervals: str | tuple[int | float],
|
||||||
cents: bool = False,
|
|
||||||
octave: int = 0,
|
octave: int = 0,
|
||||||
modifier: int = 0,
|
modifier: int = 0,
|
||||||
) -> int:
|
) -> int:
|
||||||
@ -160,12 +159,11 @@ def note_from_pc(
|
|||||||
# Initialization
|
# Initialization
|
||||||
root = note_name_to_midi(root) if isinstance(root, str) else root
|
root = note_name_to_midi(root) if isinstance(root, str) else root
|
||||||
intervals = get_scale(intervals) if isinstance(intervals, str) else intervals
|
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)
|
scale_length = len(intervals)
|
||||||
|
|
||||||
# Resolve pitch classes to the scale and calculate octave
|
# Resolve pitch classes to the scale and calculate octave
|
||||||
if pitch_class >= scale_length or pitch_class < 0:
|
if pitch_class >= scale_length or pitch_class < 0:
|
||||||
octave += floor(pitch_class / scale_length)
|
octave += pitch_class // scale_length
|
||||||
pitch_class = (
|
pitch_class = (
|
||||||
scale_length - (abs(pitch_class) % scale_length)
|
scale_length - (abs(pitch_class) % scale_length)
|
||||||
if pitch_class < 0
|
if pitch_class < 0
|
||||||
@ -177,7 +175,9 @@ def note_from_pc(
|
|||||||
# Computing the result
|
# Computing the result
|
||||||
note = root + sum(intervals[0:pitch_class])
|
note = root + sum(intervals[0:pitch_class])
|
||||||
|
|
||||||
return note + (octave * sum(intervals)) + modifier
|
note = note + (octave * sum(intervals)) + modifier
|
||||||
|
|
||||||
|
return resolve_pitch_bend(note)
|
||||||
|
|
||||||
|
|
||||||
def parse_roman(numeral: str) -> int:
|
def parse_roman(numeral: str) -> int:
|
||||||
@ -254,7 +254,7 @@ def midi_to_octave(note: int) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
int: Returns default octave in Ziffers where C4 is in octave 0
|
int: Returns default octave in Ziffers where C4 is in octave 0
|
||||||
"""
|
"""
|
||||||
return 0 if note <= 0 else floor(note / 12)
|
return 0 if note <= 0 else note // 12
|
||||||
|
|
||||||
|
|
||||||
def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
||||||
@ -345,3 +345,34 @@ def named_chord_from_degree(
|
|||||||
for interval in intervals:
|
for interval in intervals:
|
||||||
notes.append(scale_degree + interval + (cur_oct * 12))
|
notes.append(scale_degree + interval + (cur_oct * 12))
|
||||||
return notes
|
return notes
|
||||||
|
|
||||||
|
def resolve_pitch_bend(note_value: float, semitones: int=1) -> int:
|
||||||
|
"""Resolves pitch bend value from float midi note
|
||||||
|
|
||||||
|
Args:
|
||||||
|
note_value (float): Note value as float, eg. 60.41123
|
||||||
|
semitones (int, optional): Number of semitones to scale the pitch bend. Defaults to 1.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Returns pitch bend value ranging from 0 to 16383. 8192 means no bend.
|
||||||
|
"""
|
||||||
|
# TODO: None or 8192
|
||||||
|
midi_bend_value = None
|
||||||
|
if isinstance(note_value, float) and note_value % 1 != 0.0:
|
||||||
|
start_value = note_value if note_value > round(note_value) else round(note_value)
|
||||||
|
end_value = round(note_value) if note_value > round(note_value) else note_value
|
||||||
|
bend_diff = midi_to_freq(start_value) / midi_to_freq(end_value)
|
||||||
|
bend_target = 1200 * log2(bend_diff)
|
||||||
|
# https://www.cs.cmu.edu/~rbd/doc/cmt/part7.html
|
||||||
|
midi_bend_value = 8192 + int(8191 * (bend_target/(100*semitones)))
|
||||||
|
return (note_value, midi_bend_value)
|
||||||
|
|
||||||
|
|
||||||
|
def cents_to_semitones(cents):
|
||||||
|
if cents[0] != 0.0:
|
||||||
|
cents = [0.0]+cents
|
||||||
|
semitone_scale = []
|
||||||
|
for i, cent in enumerate(cents[:-1]):
|
||||||
|
semitone_interval = (cents[i+1] - cent) / 100
|
||||||
|
semitone_scale.append(semitone_interval)
|
||||||
|
return tuple(semitone_scale)
|
||||||
|
|||||||
18
ziffers/spec/scala.lark
Normal file
18
ziffers/spec/scala.lark
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
?root: lines
|
||||||
|
|
||||||
|
lines: (number | operation)+
|
||||||
|
random: "(" SIGNED_NUMBER "," SIGNED_NUMBER ")"
|
||||||
|
operation: number (operator (number | sub_operations | operation))+
|
||||||
|
!operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" | "/"
|
||||||
|
sub_operations: "(" operation ")"
|
||||||
|
number: SIGNED_NUMBER | random
|
||||||
|
|
||||||
|
%import common.SIGNED_NUMBER
|
||||||
|
%import common.FLOAT
|
||||||
|
%import common.INT
|
||||||
|
%import common.WORD
|
||||||
|
%import common.CNAME
|
||||||
|
%import common.WS
|
||||||
|
%import common.NEWLINE
|
||||||
|
|
||||||
|
%ignore WS
|
||||||
Reference in New Issue
Block a user