diff --git a/ziffers/classes/items.py b/ziffers/classes/items.py index b249b5d..b4478f3 100644 --- a/ziffers/classes/items.py +++ b/ziffers/classes/items.py @@ -172,7 +172,7 @@ class Pitch(Event): """Class for pitch in time""" pitch_class: int - pitch_bend: float = field(default=None) + pitch_bend: int = field(default=None) octave: int = field(default=None) modifier: int = field(default=None) note: int = field(default=None) diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 46458d6..c8aa299 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,8 +1,8 @@ """ Lark transformer for mapping Lark tokens to Ziffers objects """ import random -from math import log +from math import log, pow from lark import Transformer, Token -from .scale import cents_to_semitones +from .scale import cents_to_semitones, ratio_to_cents from .classes.root import Ziffers from .classes.sequences import ( Sequence, @@ -484,36 +484,57 @@ class ZiffersTransformer(Transformer): # pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name class ScalaTransformer(Transformer): def lines(self, items): - return cents_to_semitones(items) - + cents = [ratio_to_cents(item) if isinstance(item,int) else item for item in items] + return cents_to_semitones(cents) + def operation(self, items): val = eval("".join(str(item) for item in items)) - return 1200.0 * log(float(val), 2) - + return val + 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 ratio(self, items): + ratio = items[0]/items[1] + return ratio_to_cents(ratio) + + def edo_ratio(self, items): + 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] + else: + power = items[2] + ratio = pow(power,items[0]/items[1]) + return ratio_to_cents(ratio) + + def int(self, items): + return int(items[0].value) + + def float(self, items): + return float(items[0].value) + + def random_int(self, items): 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)) + return random.randint(min(start, end), max(start, end)) - start = _parse_type(items[0].value) - end = _parse_type(items[1].value) + start = items[0] + end = items[1] rand_val = _rand_between(start, end) - return 1200.0 * log(float(rand_val), 2) + return rand_val + + def random_decimal(self, items): + + def _rand_between(start, end): + return random.uniform(min(start, end), max(start, end)) + + start = items[0] + end = items[1] + rand_val = _rand_between(start, end) + return rand_val diff --git a/ziffers/scale.py b/ziffers/scale.py index 6d4bca9..5bf359c 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -2,7 +2,7 @@ #!/usr/bin/env python3 # pylint: disable=locally-disabled, no-name-in-module import re -from math import log2, floor +from math import log2, floor, log from .common import repeat_text from .defaults import ( SCALES, @@ -177,7 +177,10 @@ def note_from_pc( note = note + (octave * sum(intervals)) + modifier - return resolve_pitch_bend(note) + if isinstance(note, float): + return resolve_pitch_bend(note) + + return (note, None) def parse_roman(numeral: str) -> int: @@ -286,14 +289,14 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict: if len(npc) > 1: modifier = 1 if (npc[0] == "#") else -1 return { - "text": repeat_text("^", "_", octave)+npc, + "text": repeat_text("^", "_", octave) + npc, "pitch_class": int(npc[1]), "octave": octave, "modifier": modifier, } return { - "text": repeat_text("^", "_", octave)+npc, + "text": repeat_text("^", "_", octave) + npc, "pitch_class": int(npc), "octave": octave, } @@ -326,7 +329,11 @@ def chord_from_degree( def named_chord_from_degree( - degree: int, name: str = "major", root: int = 60, scale: str="Major", num_octaves: int = 1 + degree: int, + name: str = "major", + root: int = 60, + scale: str = "Major", + num_octaves: int = 1, ) -> list[int]: """Generates chord from given roman numeral and chord name @@ -339,14 +346,15 @@ def named_chord_from_degree( list[int]: _description_ """ intervals = CHORDS.get(name, CHORDS["major"]) - scale_degree = get_scale_notes(scale, root)[degree-1] + scale_degree = get_scale_notes(scale, root)[degree - 1] notes = [] for cur_oct in range(num_octaves): 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: + +def resolve_pitch_bend(note_value: float, semitones: int = 1) -> int: """Resolves pitch bend value from float midi note Args: @@ -356,23 +364,29 @@ def resolve_pitch_bend(note_value: float, semitones: int=1) -> int: Returns: int: Returns pitch bend value ranging from 0 to 16383. 8192 means no bend. """ - # TODO: None or 8192 - midi_bend_value = None + midi_bend_value = 8192 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) + 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))) + midi_bend_value = 8192 + int(8191 * (bend_target / (100 * semitones))) return (note_value, midi_bend_value) -def cents_to_semitones(cents): +def cents_to_semitones(cents: list) -> tuple[float]: + """Tranform cents to semitones""" if cents[0] != 0.0: - cents = [0.0]+cents + cents = [0.0] + cents semitone_scale = [] for i, cent in enumerate(cents[:-1]): - semitone_interval = (cents[i+1] - cent) / 100 + semitone_interval = (cents[i + 1] - cent) / 100 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) diff --git a/ziffers/spec/scala.lark b/ziffers/spec/scala.lark index 2b5d667..7469606 100644 --- a/ziffers/spec/scala.lark +++ b/ziffers/spec/scala.lark @@ -1,18 +1,20 @@ ?root: lines -lines: (number | operation)+ -random: "(" SIGNED_NUMBER "," SIGNED_NUMBER ")" +lines: (number | operation | ratio | edo_ratio)+ + operation: number (operator (number | sub_operations | operation))+ -!operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" | "/" +ratio: (int | random_int) "/" (int | random_int) +edo_ratio: (int | random_int) "\\" (int | random_int) +edji_ratio: (int | random_int) "\\" (int | random_int) "<" int "/" int ">" +!operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" sub_operations: "(" operation ")" -number: SIGNED_NUMBER | random -%import common.SIGNED_NUMBER -%import common.FLOAT -%import common.INT -%import common.WORD -%import common.CNAME +// 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]+/ + %import common.WS -%import common.NEWLINE - %ignore WS \ No newline at end of file