""" Methods for calculating notes from scales and list of all intervals in scales""" #!/usr/bin/env python3 # pylint: disable=locally-disabled, no-name-in-module import re from math import log2 from itertools import islice from .generators import gen_primes from .common import repeat_text from .defaults import ( SCALES, MODIFIERS, NOTES_TO_INTERVALS, INTERVALS_TO_NOTES, ROMANS, CIRCLE_OF_FIFTHS, CHORDS, ) def midi_to_note_name(midi: int) -> str: """Creates note name from midi number Args: midi (int): Mii number Returns: str: Note name """ return INTERVALS_TO_NOTES[midi % 12] def note_name_to_interval(name: str) -> int: """Parse note name to interval Args: name (str): Note name as: [a-gA-G][#bs] Returns: int: Interval of the note name [-1 - 11] """ items = re.match(r"^([a-gA-G])([#bs])?$", name) if items is None: return 0 values = items.groups() modifier = MODIFIERS[values[1]] if values[1] else 0 interval = NOTES_TO_INTERVALS[values[0].capitalize()] return interval + modifier def midi_to_freq(note: int) -> float: """Transform midi to frequency""" freq = 440 # Frequency of A return (freq / 32) * (2 ** ((note - 9) / 12)) def note_name_to_midi(name: str) -> int: """Parse note name to midi Args: name (str): Note name in scientific notation: [a-gA-G][#bs][1-9] Returns: int: Midi note """ items = re.match(r"^([a-gA-G])([#bs])?([1-9])?$", name) if items is None: return 60 values = items.groups() octave = int(values[2]) if values[2] else 4 modifier = MODIFIERS[values[1]] if values[1] else 0 interval = NOTES_TO_INTERVALS[values[0].capitalize()] return 12 + octave * 12 + interval + modifier def get_scale(scale: str) -> list[int]: """Get a scale from the global scale list Args: name (str): Name of the scale as named in https://allthescales.org/ Returns: list: List of intervals in the scale """ if isinstance(scale, (list, tuple)): return scale scale = SCALES.get(scale.lower().capitalize(), SCALES["Ionian"]) return scale def get_scale_notes(name: str, root: int = 60, num_octaves: int = 1) -> list[int]: """Return notes for the scale Args: name (str): Name of the scale root (int, optional): Root note. Defaults to 60. num_octaves (int, optional): Number of octaves. Defaults to 1. Returns: list[int]: List of notes """ scale = get_scale(name) scale_notes = [root] for _ in range(num_octaves): scale_notes = scale_notes + [root := root + semitone for semitone in scale] return scale_notes def get_chord_from_scale( 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 Args: degree (int): Degree of scale to start on root (int, optional): Root for the scale. Defaults to 60. scale (str, optional): Name of the scale. Defaults to "Major". num_notes (int, optional): Number of notes. Defaults to 3. skip (int, optional): Takes every n from the scale. Defaults to 2. Returns: list[int]: List of midi notes """ 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(scale: str) -> int: """Get length of the scale Args: name (str): Name of the scale Returns: int: Length of the 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 def note_from_pc( root: int | str, pitch_class: int, intervals: str | tuple[int | float], octave: int = 0, modifier: int = 0, degrees: bool = False ) -> int: """Resolve a pitch class into a note from a scale Args: root (int | str): Root of the scale in MIDI or scientific pitch notation pitch_class (int): Pitch class to be resolved intervals (str | list[int | float]): Name or Intervals for the scale cents (bool, optional): Flag for interpreting intervals as cents. Defaults to False. octave (int, optional): Default octave. Defaults to 0. modifier (int, optional): Modifier for the pitch class (#=1, b=-1). Defaults to 0. Returns: int: Resolved MIDI note """ # Initialization pitch_class = pitch_class-1 if degrees and pitch_class>0 else pitch_class root = note_name_to_midi(root) if isinstance(root, str) else root intervals = get_scale(intervals) if isinstance(intervals, str) 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 += pitch_class // scale_length pitch_class = ( scale_length - (abs(pitch_class) % scale_length) if pitch_class < 0 else pitch_class % scale_length ) if pitch_class == scale_length: pitch_class = 0 # Computing the result note = root + sum(intervals[0:pitch_class]) note = note + (octave * sum(intervals)) + modifier if isinstance(note, float): return resolve_pitch_bend(note) return (note, None) def parse_roman(numeral: str) -> int: """Parse roman numeral from string Args: numeral (str): Roman numeral as string Returns: int: Integer parsed from roman numeral """ values = [ROMANS[val] for val in numeral] result = 0 i = 0 while i < len(values): if i < len(values) - 1 and values[i + 1] > values[i]: result += values[i + 1] - values[i] i += 2 else: result += values[i] i += 1 return result def accidentals_from_note_name(name: str) -> int: """Generates number of accidentals from name of the note. Args: name (str): Name of the note Returns: int: Integer representing number of flats or sharps: -7 flat to 7 sharp. """ if name not in CIRCLE_OF_FIFTHS: name = midi_to_note_name(note_name_to_midi(name)) idx = CIRCLE_OF_FIFTHS.index(name) return idx - 6 def accidentals_from_midi_note(note: int) -> int: """Generates number of accidentals from name of the note. Args: note (int): Note as midi number Returns: int: Integer representing number of flats or sharps: -7 flat to 7 sharp. """ name = midi_to_note_name(note) return accidentals_from_note_name(name) def midi_to_tpc(note: int, key: str | int): """Return Tonal Pitch Class value for the note Args: note (int): MIDI note key (str | int): Key as a string (A-G) or a MIDI note. Returns: _type_: Tonal Pitch Class value for the note """ if isinstance(key, str): acc = accidentals_from_note_name(key[0]) else: acc = accidentals_from_midi_note(key) return (note * 7 + 26 - (11 + acc)) % 12 + (11 + acc) def midi_to_octave(note: int) -> int: """Return octave for the midi note Args: note (int): Note in midi Returns: int: Returns default octave in Ziffers where C4 is in octave 0 """ return 0 if note <= 0 else note // 12 def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict: """Return pitch class and octave from given midi note, key and scale Args: note (int): Note as MIDI number key (str | int): Used key scale (str): Used scale Returns: tuple: Returns dict containing pitch-class values """ pitch_class = int(note % 12) # Cast to int "fixes" microtonal scales octave = midi_to_octave(note) - 5 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"] flats = ["0", "b1", "1", "b2", "2", "3", "b4", "4", "b5", "5", "b6", "6"] tpc = midi_to_tpc(note, key) if tpc >= 6 and tpc <= 12 and len(flats[pitch_class]) == 2: npc = flats[pitch_class] elif tpc >= 20 and tpc <= 26 and len(sharps[pitch_class]) == 2: npc = sharps[pitch_class] else: npc = sharps[pitch_class] if len(npc) > 1: modifier = 1 if (npc[0] == "#") else -1 return { "text": repeat_text("^", "_", octave) + npc, "pitch_class": int(npc[1]), "octave": octave, "modifier": modifier, } return { "text": repeat_text("^", "_", octave) + npc, "pitch_class": int(npc), "octave": octave, } def chord_from_degree( degree: int, name: str, scale: str, root: str | int, num_octaves: int = 1 ) -> list[int]: """Generate chord from scale Args: degree (int): Chord degree name (str): Chord type scale (str): Scale name root (str | int): Root for the chord num_octaves (int, optional): Number of octaves. Defaults to 1. Returns: list[int]: Created chord as list of midi notes """ root = note_name_to_midi(root) if isinstance(root, str) else root if ( name is None and isinstance(scale, str) and scale.lower().capitalize() == "Chromatic" ): name = "major" if name: return named_chord_from_degree(degree, name, root, scale, num_octaves) else: return get_chord_from_scale(degree, root, scale) def named_chord_from_degree( 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 Args: roman (str): Roman numeral name (str, optional): Chord name. Defaults to "major". num_octaves (int, optional): Number of octaves for the chord. Defaults to 1. Returns: list[int]: _description_ """ intervals = CHORDS.get(name, CHORDS["major"]) 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: """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. """ 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) ) 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: list) -> tuple[float]: """Tranform cents to semitones""" 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) def ratio_to_cents(ratio: float) -> float: """Transform ratio to cents""" 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