Numuse Documentation

numuse.notation

Source code for numuse.notation

"""Different notations for specifying notes to be played"""

from __future__ import annotations
from .tools import I, ranged_modulus_operator
from typing import Set, Dict
from .musical_system import RBMS_Approximation
from .constants import JUST_INTONATION_RATIOS, JAZZ_INTERVAL_COLLECTIONS


# TODO Somehow specify that this is immutable
[docs]class Note: """A Note from a musical system Every number corresponds to a unique note or equivalently an octave it lives in and a note name. In other words the note 1 13 25 37 are all identical, in terms of their true note name. We will set 9 to 440hz, and we will consider the octave zero to contain the set of notes 0 to 11, octave 1 represents the notes 12 to 23 and so on. Because of this we will represent notes by an octave offset and a note name :param note: The absolute note :type note: int :param musical_system: The underlying musical system :type musical_system: RBMS_Approximation """
[docs] def __init__( self, note: int, musical_system=RBMS_Approximation( 440, JUST_INTONATION_RATIOS, 2, 2 ** (1 / 12), 12 ), ): self.note = note self.musical_system = musical_system self.octave_offset, self.note_name = divmod( self.note, self.musical_system.num_notes )
def octave_equivalent(self, other: Note): return self.note_name == other.note_name def __eq__(self, other: Note): return self.note == other.note def __hash__(self): return hash(self.note) def __add__(self, other: int): return Note(self.note + other) def __sub__(self, other: int): return Note(self.note - other)
[docs]class NoteCollection: """A collection of notes from a musical system If a duration is specified then this represents all notes being played at once. Otherwise it just represents a conceptual set of notes :param notes: The notes in this note collection :type notes: List[int] :param duration: How long this note collection is held for :type duration: List[int] :param musical_system: The underlying musical system :type musical_system: RBMS_Approximation """
[docs] def __init__( self, notes: Set[Note], duration=0, musical_system=RBMS_Approximation( 440, JUST_INTONATION_RATIOS, 2, 2 ** (1 / 12), 12 ), ): self.notes = notes self.duration = duration self.musical_system = musical_system
def __eq__(self, other_NC): """True if they contain the same notes""" return self.notes == other_NC.notes def __repr__(self): return str(self.__class__) + ": " + str(self.__dict__) def __str__(self): """Human readable representation of a note collection""" return str(self.notes)
[docs] def generate_wave_function(self): """Generates the wave function determined by the current musical system""" raise NotImplementedError
[docs] def compute_diatonic_distance(self, other_NC: NoteCollection) -> float: """Return how many notes the two note collections differ by dividided by the number of notes it has""" raise NotImplementedError
[docs]class RootedIntervalCollection(NoteCollection): """A note collection instantiated in a special way A rooted interval collection is a way to define a set of notes of a musical system. It does so specifying a note (denoted by root) from the system and a set of intervals measured with respect to the root. :param root: The root tone :type root: int :param intervals: The intervals above the root :type intervals: Set[int] """
[docs] def __init__( self, root: Note, interval_collection: Set[int], duration=0, musical_system=RBMS_Approximation( 440, JUST_INTONATION_RATIOS, 2, 2 ** (1 / 12), 12 ), ): """ durations is measured in seconds, it is by default set to 0 seconds to represent no duration """ self.root = root self.interval_collection = interval_collection super().__init__(self.generate_notes(), duration, musical_system)
def __str__(self): """Human readable representation of a RIC""" return str(self.root) + " | " + str(self.interval_collection)
[docs] def generate_notes(self) -> Set[Note]: """Generate the notes that are defined by taking the root note and adding the notes in the interval collection :param :py:attr:`~root`: The root tone :param intervals: The intervals above the root :type intervals: Set[int] :return: A list of notes :rtype: Set[int] :Example: >>> ric = RootedIntervalCollection(5, {0, 4, 7, 11}) >>> ric.generate_notes() TODO """ return {self.root + x for x in self.interval_collection}
[docs] def compute_intervallic_complexity(self) -> float: """Computes the intervallic complexity of this rooted interval collection The intervallic complexity of a rooted interval collection is computed by considering all the possible intervals in the interval collection, assigning a complexity cost (derived from the ratios that the system approximates) and then summing all of the complexity costs. For example, if we consider the interval collection {0, 4, 7, 11}, we clearly have the intervals 0, 4, 7, 11, but additionally between 4 and 7, there is an interval of 3. and between 4 and 11 there is another interval of 7. :return: The intervallic complexity :rtype: float """ interval_to_occurence = self.generate_interval_to_occurence() intervallic_complexity = 0 for interval, occurence in interval_to_occurence.items(): # ratio = self.musical_system.interval_to_ratio[interval] # ratio = self.musical_system.interval_to_ratio[interval] # ratio_complexity = self.musical_system.ratios_to_complexity[ratio] * occurence interval_complexity = ( self.musical_system.interval_to_complexity[interval] * occurence ) intervallic_complexity += interval_complexity return intervallic_complexity
[docs] def generate_interval_to_occurence(self) -> Dict[int, int]: """Generate a dictionary that maps all possible intervals in this interval collection to the number of times it appears :return: A dictionary mapping intervals to occurence :rtype: Dict[int, int] """ num_intervals = len(self.interval_collection) fixed_order_interval_collection = sorted(list(self.interval_collection)) interval_to_occurence = {} for i in range(num_intervals): for j in range(i, num_intervals): if i < j: interval_a = fixed_order_interval_collection[i] interval_b = fixed_order_interval_collection[j] interval_between = I(interval_a, interval_b) fundamental_interval_between = ranged_modulus_operator( interval_between, self.musical_system.num_notes ) if interval_between not in interval_to_occurence: interval_to_occurence[fundamental_interval_between] = 1 else: interval_to_occurence[fundamental_interval_between] += 1 return interval_to_occurence
[docs] def get_fundamental_representation(self) -> RootedIntervalCollection: """Generate the fundamental representation of this interval collection The fundamental representation a rooted interval collection where the interval are within the range 0 ... num_notes - 1 where num_notes is defined by the musical system we are dealing with. In 12 tone equal temperament, num_notes is equal to 12. For example, if we have a rooted interval collection 13 | -3 1 2 24, then the fundamental representation would be 1 | 0 1 2 9 :return: The funamental representation of this interavl collection :rtype: RootedIntervalCollection """ fundamental_interval = { ranged_modulus_operator(i, self.musical_system.num_notes) for i in self.interval_collection } fundamental_root = Note( ranged_modulus_operator(self.root.note, self.musical_system.num_notes) ) return RootedIntervalCollection(fundamental_root, fundamental_interval)
[docs]class DoubleRootedIntervalCollection(NoteCollection): """A rooted interval collection where the note in the RIC is an interval above another note""" pass
Free document hosting provided by Read the Docs.