Source code for musicaiz.rhythm.timing

from __future__ import annotations
from abc import ABCMeta
from enum import Enum
from typing import Tuple, List, Dict, Union, Optional
import numpy as np
import math


[docs]class TimingConsts(Enum): """ This Enum contains the default values for timing parameters. RESOLUTION: Sequencer Ticks per quarter note (TPQN) or Pulses per quarter note (PPQ) used in Logic Pro. DEFAULT_BPM: Default Tempo or bpm if no bpm value is known in advance. DEFAULT_TIME_SIGNATURE: Default time signature if it is not known in advance. """ RESOLUTION = 96 TICKS_PER_SEC = 192 DEFAULT_BPM = 120 DEFAULT_TIME_SIGNATURE = "4/4"
[docs]class NoteLengths(Enum): """ This Enum contains Note durations in fractions of the whole note. The Enum names correspond to the American notation. From https://www.fransabsil.nl/htm/ticktime.htm Value: Relative value """ DOTTED_WHOLE = 3 / 2 WHOLE = 1 DOTTED_HALF = 3 / 4 HALF = 1 / 2 DOTTED_QUARTER = 3 / 8 QUARTER = 1 / 4 DOTTED_EIGHT = 3 / 16 EIGHT = 1 / 8 DOTTED_SIXTEENTH = 3 / 32 SIXTEENTH = 1 / 16 DOTTED_THIRTY_SECOND = 3 / 64 THIRTY_SECOND = 1 / 32 DOTTED_SIXTY_FOUR = 3 / 128 SIXTY_FOUR = 1 / 64 DOTTED_HUNDRED_TWENTY_EIGHT = 3 / 256 HUNDRED_TWENTY_EIGHT = 1 / 128 # triplets HALF_TRIPLET = 1 / 3 QUARTER_TRIPLET = 1 / 6 EIGHT_TRIPLET = 1 / 12 SIXTEENTH_TRIPLET = 1 / 24 @property def fraction(self) -> float: return self.value def ticks( self, resolution: int = TimingConsts.RESOLUTION.value, ) -> int: return round(resolution * self.fraction / NoteLengths.QUARTER.fraction) def ms( self, bpm: int = TimingConsts.DEFAULT_BPM.value, resolution: int = TimingConsts.RESOLUTION.value, ) -> float: return ms_per_tick(bpm, resolution) * self.ticks(resolution) @classmethod def get_note_ticks_mapping( cls, triplets: bool = False, resolution: int = TimingConsts.RESOLUTION.value, ) -> Dict[str, int]: dict_notes = {} for note_dur in list(cls.__members__.keys()): if not triplets: # remove triplet durations (optional) if "TRIPLET" in note_dur: continue dict_notes.update({note_dur: cls[note_dur].ticks(resolution)}) return dict_notes @classmethod def get_note_with_fraction(cls, fraction: float) -> NoteLengths: for note in cls.__members__: if math.isclose(round(cls[note].value, 3), round(fraction, 3)): return cls[note]
class SymbolicNoteLengths(Enum): """ This Enum contains Note symbols as unicode strings. """ DOTTED_WHOLE = u"\U0001D15D" + "." WHOLE = u"\U0001D15D" DOTTED_HALF = u"\U0001D15E" + "." HALF = u"\U0001D15E" DOTTED_QUARTER = u"\U0001D15F" + "." QUARTER = u"\U0001D15F" DOTTED_EIGHT = u"\U0001D160" + "." EIGHT = u"\U0001D160" DOTTED_SIXTEENTH = u"\U0001D161" + "." SIXTEENTH = u"\U0001D161" DOTTED_THIRTY_SECOND = u"\U0001D162" + "." THIRTY_SECOND = u"\U0001D162" DOTTED_SIXTY_FOUR = u"\U0001D163" + "." SIXTY_FOUR = u"\U0001D163" DOTTED_HUNDRED_TWENTY_EIGHT = u"\U0001D164" + "." HUNDRED_TWENTY_EIGHT = u"\U0001D164" # triplets HALF_TRIPLET = u"\U0001D15E\U000000B3" QUARTER_TRIPLET = u"\U0001D15F\U000000B3" EIGHT_TRIPLET = u"\U0001D160\U000000B3" SIXTEENTH_TRIPLET = u"\U0001D161\U000000B3" class TimeSigDenominators(Enum): """This Enum contains the possible time signature denominators.""" WHOLE = 1 HALF = 2 QUARTER = 4 EIGHT = 8 SIXTEENTH = 16 THIRTY_SECOND = 32 SIXTY_FOUR = 64 HUNDRED_TWENTY_EIGHTH = 128 @classmethod def get_note_length(cls, denominator: int) -> TimeSigDenominators: got = None for note in cls.__members__.values(): if note.value == denominator: got = note return got if got is None: raise ValueError(f"Not note found for {denominator} denominator.")
[docs]class TimeSignature:
[docs] def __init__(self, time_sig: Union[Tuple[int, int], str]): """ TimeSignature class for representing time signatures. Parameters ---------- time_sig: str The time signature in the format num/den """ if isinstance(time_sig, str): if "/" not in time_sig: raise ValueError(f"Time signature {time_sig} is not in the correct format.") self.time_sig = time_sig self.num, self.denom = self.time_sig.split("/") self.num = int(self.num) self.denom = int(self.denom) elif isinstance(time_sig, tuple): self.time_sig = str(time_sig[0]) + "/" + str(time_sig[1]) self.num = int(time_sig[0]) self.denom = int(time_sig[1]) else: raise ValueError(f"Time signature {time_sig} is not in the correct format.")
@property def beats_per_bar(self) -> int: return self.num @property def beat_type(self) -> str: return TimeSigDenominators.get_note_length(self.denom).name def _notes_per_bar(self, note_name: str) -> int: return (1 / NoteLengths[note_name].value) * self.num * (1 / self.denom) @property def quarters(self) -> int: # Get the name of the denominator note: NoteLengths(1 / self.denom).name return self._notes_per_bar("QUARTER") @property def eights(self) -> int: return self._notes_per_bar("EIGHT") @property def sixteenths(self) -> int: return self._notes_per_bar("SIXTEENTH") def __repr__(self): return "TimeSig(num={}, den={})".format( self.num, self.denom, )
class Timing(metaclass=ABCMeta): def __init__( self, bpm: float, resolution: int, start: Union[int, float], end: Union[int, float], ): self.ms_tick = ms_per_tick(bpm, resolution) timings = self._initialize_timing_attributes( start, end, self.ms_tick ) self.start_ticks = timings["start_ticks"] self.end_ticks = timings["end_ticks"] self.start_sec = timings["start_sec"] self.end_sec = timings["end_sec"] self.bpm = bpm self.resolution = resolution @staticmethod def _initialize_timing_attributes( start: Union[int, float], end: Union[int, float], ms_tick: Union[int, float], ) -> Dict[str, Union[int, float]]: # inital checks if start < 0 or end <= 0: raise ValueError("Start and end must be positive.") elif start >= end: raise ValueError("Start time must be lower than the end time.") # ticks must be int, secs must be float if isinstance(start, int) and isinstance(end, int): start_ticks = start end_ticks = end start_sec = start_ticks * ms_tick / 1000 end_sec = end_ticks * ms_tick / 1000 elif isinstance(start, float) and isinstance(end, float): start_sec = start end_sec = end start_ticks = int(start_sec * (1 / (ms_tick / 1000))) end_ticks = int(end_sec * (1 / (ms_tick / 1000))) timings = { "start_ticks": start_ticks, "end_ticks": end_ticks, "start_sec": start_sec, "end_sec": end_sec, } return timings class Beat(Timing): def __init__( self, bpm: float, resolution: int, start: Union[int, float], end: Union[int, float], time_sig: Optional[TimeSignature] = None, global_idx: Optional[int] = None, bar_idx: Optional[int] = None, ): super().__init__(bpm, resolution, start, end) self.time_sig = time_sig self.global_idx = global_idx self.bar_idx = bar_idx self.symbolic = TimeSigDenominators.get_note_length( time_sig.denom ).name.lower() def __repr__(self): return "Beat(time_signature={}, " \ "bpm={}, " \ "start_ticks={} " \ "end_ticks={} " \ "start_sec={} " \ "end_sec={} " \ "global_idx={} " \ "bar_idx={} " \ "symbolic={})".format( self.time_sig, self.bpm, self.start_ticks, self.end_ticks, self.start_sec, self.end_sec, self.global_idx, self.bar_idx, self.symbolic, ) class Subdivision(Timing): def __init__( self, bpm: float, resolution: int, start: Union[int, float], end: Union[int, float], time_sig: Optional[TimeSignature] = None, global_idx: Optional[int] = None, bar_idx: Optional[int] = None, beat_idx: Optional[int] = None, ): super().__init__(bpm, resolution, start, end) self.time_sig = time_sig self.global_idx = global_idx self.bar_idx = bar_idx self.beat_idx = beat_idx def __repr__(self): return "Subdivision(time_signature={}, " \ "bpm={}, " \ "start_ticks={} " \ "end_ticks={} " \ "start_sec={} " \ "end_sec={} " \ "global_idx={} " \ "bar_idx={} " \ "beat_idx={})".format( self.time_sig, self.bpm, self.start_ticks, self.end_ticks, self.start_sec, self.end_sec, self.global_idx, self.bar_idx, self.beat_idx, )
[docs]def ms_per_tick( bpm: int = TimingConsts.DEFAULT_BPM.value, resolution: int = TimingConsts.RESOLUTION.value, ) -> float: """This function calculates the miliseconds that correspond to one tick. Parameters ---------- bpm: int the tempo or bpms. resolution: int the pulses o ticks per quarter note (PPQ or TPQN). Returns ------- float The miliseconds that correspond to a tick. """ return 60000 / (bpm * resolution)
[docs]def ms_per_note( note_length: str = "quarter", bpm: int = TimingConsts.DEFAULT_BPM.value, resolution: int = TimingConsts.RESOLUTION.value, ) -> float: """This function calculates the miliseconds that correspond to one note. Parameters ---------- note_length: str the name of the note length in american notation. bpm: int the tempo or bpms. Returns ------- float The miliseconds that correspond to a note length. """ return NoteLengths[note_length.upper()].ms(bpm, resolution)
[docs]def ticks_per_bar( time_sig: str = TimingConsts.DEFAULT_TIME_SIGNATURE.value, resolution: int = TimingConsts.RESOLUTION.value, ) -> Tuple[int, int]: """This function calculates the ticks that correspond to one beat and bar. Returns ------- ticks_beat: int number of ticks in a beat of the bar. ticks_bar: int number of ticks in a bar. """ # There are measures with beats or subdivisions lower or higher than a quarter note. # The number of beats per measure is given by the 1st value of the `measure` tuple. # The beat figure (quarter note, 16th note...) is given by the 2nd value of the `measure` tuple. n, d = _bar_str_to_tuple(time_sig) # Find the note corresponding note to the value of the time sig. denominator note_length = TimeSigDenominators.get_note_length(d) # That note is the beat, so we get now the ticks in that beat ticks_beat = NoteLengths[note_length.name].ticks(resolution) # The numerator gives us how many beats are in the bar ticks_bar = n * ticks_beat return ticks_beat, ticks_bar
def ms_per_bar( time_sig: str = TimingConsts.DEFAULT_TIME_SIGNATURE.value, bpm: int = TimingConsts.DEFAULT_BPM.value, resolution: int = TimingConsts.RESOLUTION.value, ) -> float: """This function calculates the miliseconds that correspond to one bar. Parameters ---------- time_sig: str the time signature as a fraction. bpm: int the tempo or bpms. resolution: int the pulses o ticks per quarter note (PPQ or TPQN). Returns ------- ms_beat: float number of miliseconds in a beat of the bar. ms_bar: float number of miliseconds in a bar. """ ticks_beat, ticks_bar = ticks_per_bar(time_sig, resolution) ms_tick = ms_per_tick(bpm, resolution) ms_beat, ms_bar = ticks_beat * ms_tick, ticks_bar * ms_tick return ms_beat, ms_bar
[docs]def _bar_str_to_tuple( time_sig: str = "4/4", ) -> Tuple[int, int]: """This function converts the time signature str to a tuple of 2 values: numerator and denominator.""" if "/" not in time_sig: raise ValueError("Not valid time signature format. Time sig must have '/'") numerator, denominator = time_sig.split("/") return int(numerator), int(denominator)
[docs]def get_subdivisions( total_bars: int, subdivision: str, # TODO: give possible values here time_sig: str = TimingConsts.DEFAULT_TIME_SIGNATURE.value, bpm: int = TimingConsts.DEFAULT_BPM.value, resolution: int = TimingConsts.RESOLUTION.value, absolute_timing: bool = True ) -> List[Dict[str, Union[int, float]]]: """ This method returns the grid (vertical lines) for quantizing notes of a determined number of measures given by `total_bars`. The lines are plotted in each beat subdivision. Each line has the format m.b.s.: m: measure b: beat s: subdivision Ex.: subdivision = 8 is an 8th note Parameters ---------- total_bars: int the number of bars. subdivision: str the note subdivision. Ex.: quarter, eight... Note that we cannot have a subdivision greater than the beat nor a non value of the beat note. If the beat corresponds to a quarter note (X/4 time sigs.), the subdivision has to be a note shorter than the quarter note but half or 4 times less than the beat (eight, sixteenth note...). time_sig: str the time signature as a fraction. bpm: int the tempo or bpms. resolution: int the pulses o ticks per quarter note (PPQ or TPQN). absolute_timing: bool default is True. This allows to initialize note time arguments in absolute (True) or relative time units (False). Relative units means that each bar will start at 0 seconds and ticks, so the note timing attributes will be relative to the bar start equals to 0. Returns ------- beat_subdivs: List[Dict[str, Union[int, float]]] each element in the list is a subdivision. The subdivision dict key's are: `bar`: the bar index in the total_bars of the subdivision. `piece_beat`: the beat index corresponding to the subdivision in the total_bars or piece. `piece_subdivision`: the subdivision index corresponding to the total number of subdivisions in the total_bars or piece. `bar_beat`: the beat index corresponding to the subdivision in its bar. `bar_subdivision`: the subdivision index corresponding to its bar. `ticks`: the ticks value of where the subdivision starts. `sec`: the secs value of where the subdivision starts. """ if total_bars <= 0 or isinstance(total_bars, float): raise ValueError("Total number of bars must be a positive integer.") beats_bar, denominator = _bar_str_to_tuple(time_sig) # Get the note length note_length = TimeSigDenominators.get_note_length(denominator) # Get the ticks in a beat ticks_beat = NoteLengths[note_length.name].ticks(resolution) ms_beat = ms_per_note(note_length.name.lower(), bpm) # Get the subdivision note subdiv_note = TimeSigDenominators[subdivision.upper()] # Subdivision note has to be 2, 4, 8...times less than the beat # Subdivision must be in the subdivisions possible values (strs) if subdivision.upper() not in TimeSigDenominators.__members__.keys(): raise ValueError(f"Subdivision {subdivision} does not exist.") if subdiv_note.value % note_length.value != 0: raise ValueError(f"Input subdivision {subdivision} is not valid for beat note {note_length.name}.") # Get how many of these subdivision notes are in a beat subdivs_beat = subdiv_note.value // note_length.value subdivs_bar = beats_bar * subdivs_beat ticks_subdiv = ticks_beat // subdivs_beat ms_subdiv = ms_beat / subdivs_beat # total beats in the grid (vertical lines) are the measures * number of beats per measure total_beats = total_bars * beats_bar total_beat_subdivs = total_beats * subdivs_beat # Calculate the ticks where each subdivision starts. # That will be the horizontal position of the vertical or grid lines. beat_subdivs = [] measure_index = 1 beat_index = 1 beat_bar_index = 1 for beat_subdiv_index in range(total_beat_subdivs): beat_subdivision = {} measure_div = beat_subdiv_index % subdivs_bar beat_div = beat_subdiv_index % subdivs_beat if measure_div == 0 and not beat_subdiv_index == 0: measure_index += 1 beat_bar_index = 0 if beat_div == 0 and not beat_subdiv_index == 0: beat_index += 1 beat_bar_index += 1 beat_subdivision["bar"] = measure_index beat_subdivision["piece_beat"] = beat_index beat_subdivision["piece_subdivision"] = beat_subdiv_index + 1 beat_subdivision["bar_beat"] = beat_bar_index beat_subdivision["bar_subdivision"] = measure_div + 1 beat_subdivision["ticks"] = int(ticks_subdiv * beat_subdiv_index) beat_subdivision["sec"] = ms_subdiv * beat_subdiv_index / 1000 beat_subdivs.append(beat_subdivision) # update ticks and sec values if absolute_timing is not True if not absolute_timing: # for the convention used, the 1st element in the dict `beat_subdivs` # has a "bar" value of 1 (not 0), so we'll start in the else statement prev_bar = 0 for subdiv in beat_subdivs: if subdiv["bar"] == prev_bar: subdiv["ticks"] = subdiv["ticks"] - bar_start_ticks subdiv["sec"] = subdiv["sec"] - bar_start_sec else: bar_start_ticks, bar_start_sec = subdiv["ticks"], subdiv["sec"] subdiv["ticks"], subdiv["sec"] = 0, 0 prev_bar = subdiv["bar"] return beat_subdivs
def get_symbolic_duration( duration: int, triplets: bool = False, resolution: int = TimingConsts.RESOLUTION.value, ) -> str: """Given a note duration in ticks it calculates its symbolic duration: half, quarter, dotted_half... Parameters ----------- triplets: bool default is False. It takes (True) or not (False) into account triplets durations. """ all_notes_ticks = NoteLengths.get_note_ticks_mapping(triplets, resolution) notes_ticks = all_notes_ticks # look for the closest note in the notes ticks dict # the closest note will be the selected note even if its # duration in ticks is not exactly equal to the theoretical note ticks arr = np.asarray(list(notes_ticks.values())) i = (np.abs(arr - duration)).argmin() symbolic_duration = list(notes_ticks.keys())[i] return symbolic_duration