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):
|
||||
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
|
||||
|
||||
@ -5,3 +5,4 @@ from .defaults import *
|
||||
from .scale import *
|
||||
from .converters import *
|
||||
from .spec import *
|
||||
from .classes import *
|
||||
@ -1,3 +1,3 @@
|
||||
from .items import *
|
||||
from .sequences import *
|
||||
from .root import *
|
||||
from .sequences import *
|
||||
@ -1,5 +1,6 @@
|
||||
""" Ziffers item classes """
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from math import floor
|
||||
import operator
|
||||
import random
|
||||
from ..scale import (
|
||||
@ -171,6 +172,7 @@ class Pitch(Event):
|
||||
"""Class for pitch in time"""
|
||||
|
||||
pitch_class: int
|
||||
pitch_bend: float = field(default=None)
|
||||
octave: int = field(default=None)
|
||||
modifier: int = field(default=None)
|
||||
note: int = field(default=None)
|
||||
@ -207,6 +209,10 @@ class Pitch(Event):
|
||||
"""Getter for pitche"""
|
||||
return self.pitch_class
|
||||
|
||||
def get_pitch_bend(self):
|
||||
"""Getter for pitche"""
|
||||
return self.pitch_bend
|
||||
|
||||
def update_note(self, force: bool = False):
|
||||
"""Update note if Key, Scale and Pitch-class are present"""
|
||||
if (
|
||||
@ -215,15 +221,16 @@ class Pitch(Event):
|
||||
and (self.pitch_class is not None)
|
||||
and (self.note is None or force)
|
||||
):
|
||||
note = note_from_pc(
|
||||
note, pitch_bend = note_from_pc(
|
||||
root=self.key,
|
||||
pitch_class=self.pitch_class,
|
||||
intervals=self.scale,
|
||||
modifier=self.modifier if self.modifier is not None else 0,
|
||||
octave=self.octave if self.octave is not None else 0,
|
||||
)
|
||||
self.pitch_bend = pitch_bend
|
||||
self.freq = midi_to_freq(note)
|
||||
self.note = note
|
||||
self.note = floor(note)
|
||||
if self.duration is not None:
|
||||
self.beat = self.duration * 4
|
||||
|
||||
@ -296,6 +303,7 @@ class Chord(Event):
|
||||
notes: list[int] = field(default=None)
|
||||
inversions: int = field(default=None)
|
||||
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)
|
||||
octaves: list[int] = field(default=None, init=False)
|
||||
durations: list[float] = field(default=None, init=False)
|
||||
@ -334,6 +342,10 @@ class Chord(Event):
|
||||
"""Getter for pitches"""
|
||||
return self.pitches
|
||||
|
||||
def get_pitch_bend(self):
|
||||
"""Getter for pitche"""
|
||||
return self.pitch_bends
|
||||
|
||||
def get_duration(self):
|
||||
"""Getter for durations"""
|
||||
return self.durations
|
||||
@ -353,7 +365,7 @@ class Chord(Event):
|
||||
|
||||
def update_notes(self, options=None):
|
||||
"""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
|
||||
for pitch in self.pitch_classes:
|
||||
@ -367,6 +379,7 @@ class Chord(Event):
|
||||
# Create helper lists
|
||||
for pitch in self.pitch_classes:
|
||||
pitches.append(pitch.pitch_class)
|
||||
pitch_bends.append(pitch.pitch_bend)
|
||||
notes.append(pitch.note)
|
||||
freqs.append(pitch.freq)
|
||||
octaves.append(pitch.octave)
|
||||
@ -374,6 +387,7 @@ class Chord(Event):
|
||||
beats.append(pitch.beat)
|
||||
|
||||
self.pitches = pitches
|
||||
self.pitch_bends = pitch_bends
|
||||
self.notes = notes
|
||||
self.freqs = freqs
|
||||
self.octaves = octaves
|
||||
|
||||
@ -110,6 +110,14 @@ class Ziffers(Sequence):
|
||||
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]:
|
||||
"""Return list of midi notes"""
|
||||
return [
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
""" Sequence classes for Ziffers """
|
||||
from dataclasses import dataclass, field, replace
|
||||
from itertools import product
|
||||
from math import floor
|
||||
from types import LambdaType
|
||||
from copy import deepcopy
|
||||
from ..defaults import DEFAULT_OPTIONS
|
||||
@ -190,7 +191,7 @@ def create_pitch(current: Item, options: dict) -> Pitch:
|
||||
|
||||
current_value = current.get_value(merged_options)
|
||||
|
||||
note = note_from_pc(
|
||||
note, pitch_bend = note_from_pc(
|
||||
root=merged_options["key"],
|
||||
pitch_class=current_value,
|
||||
intervals=merged_options["scale"],
|
||||
@ -200,8 +201,9 @@ def create_pitch(current: Item, options: dict) -> Pitch:
|
||||
new_pitch = Pitch(
|
||||
pitch_class=current_value,
|
||||
text=str(current_value),
|
||||
note=note,
|
||||
freq=midi_to_freq(note),
|
||||
note=floor(note),
|
||||
pitch_bend=pitch_bend,
|
||||
octave=c_octave,
|
||||
modifier=c_modifier,
|
||||
kwargs=merged_options,
|
||||
|
||||
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 """
|
||||
import random
|
||||
from math import log
|
||||
from lark import Transformer, Token
|
||||
from .scale import cents_to_semitones
|
||||
from .classes.root import Ziffers
|
||||
from .classes.sequences import (
|
||||
Sequence,
|
||||
@ -337,15 +340,17 @@ class ZiffersTransformer(Transformer):
|
||||
return items[0].value
|
||||
|
||||
def variable(self, items):
|
||||
if len(items)>1:
|
||||
if len(items) > 1:
|
||||
prefixes = sum_dict(items[0:-1])
|
||||
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])
|
||||
|
||||
def variable_char(self, items):
|
||||
"""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):
|
||||
"""Return list of variables"""
|
||||
@ -474,3 +479,41 @@ class ZiffersTransformer(Transformer):
|
||||
wrap_start="",
|
||||
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 lark import Lark
|
||||
from .classes.root import Ziffers
|
||||
from .mapper import ZiffersTransformer
|
||||
from .mapper import ZiffersTransformer, ScalaTransformer
|
||||
|
||||
|
||||
grammar_path = Path(__file__).parent
|
||||
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(
|
||||
str(grammar_file),
|
||||
str(ziffers_grammar),
|
||||
rel_to=__file__,
|
||||
start="root",
|
||||
parser="lalr",
|
||||
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:
|
||||
"""Parse an expression using the Ziffers parser
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# pylint: disable=locally-disabled, no-name-in-module
|
||||
import re
|
||||
from math import floor
|
||||
from math import log2, floor
|
||||
from .common import repeat_text
|
||||
from .defaults import (
|
||||
SCALES,
|
||||
@ -138,8 +138,7 @@ def get_scale_length(name: str) -> int:
|
||||
def note_from_pc(
|
||||
root: int | str,
|
||||
pitch_class: int,
|
||||
intervals: str | list[int | float],
|
||||
cents: bool = False,
|
||||
intervals: str | tuple[int | float],
|
||||
octave: int = 0,
|
||||
modifier: int = 0,
|
||||
) -> int:
|
||||
@ -160,12 +159,11 @@ def note_from_pc(
|
||||
# Initialization
|
||||
root = note_name_to_midi(root) if isinstance(root, str) else root
|
||||
intervals = get_scale(intervals) if isinstance(intervals, str) else intervals
|
||||
intervals = list(map(lambda x: x / 100), intervals) if cents else intervals
|
||||
scale_length = len(intervals)
|
||||
|
||||
# Resolve pitch classes to the scale and calculate octave
|
||||
if pitch_class >= scale_length or pitch_class < 0:
|
||||
octave += floor(pitch_class / scale_length)
|
||||
octave += pitch_class // scale_length
|
||||
pitch_class = (
|
||||
scale_length - (abs(pitch_class) % scale_length)
|
||||
if pitch_class < 0
|
||||
@ -177,7 +175,9 @@ def note_from_pc(
|
||||
# Computing the result
|
||||
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:
|
||||
@ -254,7 +254,7 @@ def midi_to_octave(note: int) -> int:
|
||||
Returns:
|
||||
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:
|
||||
@ -345,3 +345,34 @@ def named_chord_from_degree(
|
||||
for interval in intervals:
|
||||
notes.append(scale_degree + interval + (cur_oct * 12))
|
||||
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