pychord
1from pychord.const import * 2from pychord.interval import * 3from pychord.mode import * 4from pychord.note import * 5from pychord.ratio import * 6from pychord.tone import * 7 8__all__ = [ 9 # Classes 10 "Tone", 11 "Ratio", 12 "Interval", 13 "Note", 14 "Mode", 15 "Scale", 16 # Constants 17 "OCTAVE_RATIO", 18 "SEMITONE_RATIO", 19 "C0_FREQUENCY", 20 "SEMITONE", 21 "MINOR_SECOND", 22 "WHOLETONE", 23 "MAJOR_SECOND", 24 "MINOR_THIRD", 25 "MAJOR_THIRD", 26 "PERFECT_FOURTH", 27 "AUGMENTED_FOURTH", 28 "TRITONE", 29 "DIMINISHED_FIFTH", 30 "PERFECT_FIFTH", 31 "MINOR_SIXTH", 32 "MAJOR_SIXTH", 33 "MINOR_SEVENTH", 34 "MAJOR_SEVENTH", 35 "OCTAVE", 36 "IONIAN", 37 "PHRYGIAN", 38 "DORIAN", 39 "LYDIAN", 40 "MIXOLYDIAN", 41 "AEOLIAN", 42 "LOCRIAN", 43]
7class Tone: 8 """ 9 Describes an abstract musical frequency 10 """ 11 12 frequency: Union[int, float] 13 "The frequency of the tone in hertz" 14 15 def __init__(self, frequency: Union[int, float]): 16 self.frequency = frequency 17 18 def __repr__(self) -> str: 19 return f"[Tone ({self.frequency:.4f})]" 20 21 def __str__(self) -> str: 22 return self.__repr__() 23 24 def __add__(self, other: Ratio) -> "Tone": 25 if not isinstance(other, Ratio): 26 return NotImplemented 27 return self.transposed(other) 28 29 def __sub__(self, other: Union[Ratio, "Tone"]) -> Union[Ratio, "Tone"]: 30 if isinstance(other, Ratio): 31 return self.transposed(-other) 32 elif isinstance(other, Tone): 33 return Ratio(self.frequency / other.frequency) 34 35 return NotImplemented 36 37 def __eq__(self, other: "Tone"): 38 return isinstance(other, Tone) and self.frequency == other.frequency 39 40 def __ne__(self, other: "Tone"): 41 return not isinstance(other, Tone) or self.frequency != other.frequency 42 43 def __ge__(self, other: "Tone") -> bool: 44 if not isinstance(other, Tone): 45 return NotImplemented 46 return self.frequency >= other.frequency 47 48 def __gt__(self, other: "Tone") -> bool: 49 if not isinstance(other, Tone): 50 return NotImplemented 51 return self.frequency > other.frequency 52 53 def __le__(self, other: "Tone") -> bool: 54 if not isinstance(other, Tone): 55 return NotImplemented 56 return self.frequency <= other.frequency 57 58 def __lt__(self, other: "Tone") -> bool: 59 if not isinstance(other, Tone): 60 return NotImplemented 61 return self.frequency < other.frequency 62 63 def transposed(self, ratio: "Ratio") -> "Tone": 64 """ 65 Multiply the tone frequency by the input ratio 66 """ 67 68 if not isinstance(ratio, Ratio): 69 raise TypeError() 70 return Tone(self.frequency * ratio.ratio)
Describes an abstract musical frequency
8class Ratio: 9 """ 10 Describes an abstract interval between two `Tone`s, the ratio between their frequencies 11 """ 12 13 ratio: Union[float, Fraction] 14 "Simple mathematical ratio between `Tone`s" 15 16 def __init__(self, ratio: Union[float, Fraction]): 17 self.ratio = ratio 18 19 def __repr__(self): 20 return f"[Ratio {self.ratio:.4f}]" 21 22 def __str__(self): 23 return self.__repr__() 24 25 def __neg__(self) -> "Ratio": 26 return self.inversion() 27 28 def __add__(self, other: "Ratio"): 29 if not isinstance(other, Ratio): 30 return NotImplemented 31 return Ratio(self.ratio * other.ratio) 32 33 def __sub__(self, other: "Ratio"): 34 if not isinstance(other, Ratio): 35 return NotImplemented 36 return Ratio(self.ratio / other.ratio) 37 38 def __mul__(self, other: Union[int, float, Fraction]) -> "Ratio": 39 if not isinstance(other, (int, float, Fraction)): 40 return NotImplemented 41 return Ratio(self.ratio**other) 42 43 def __eq__(self, other: "Ratio"): 44 return isinstance(other, Ratio) and self.ratio == other.ratio 45 46 def __ne__(self, other: "Ratio"): 47 return not isinstance(other, Ratio) or self.ratio != other.ratio 48 49 def __ge__(self, other: "Ratio") -> bool: 50 if not isinstance(other, (Ratio)): 51 return NotImplemented 52 return self.ratio >= other.ratio 53 54 def __gt__(self, other: "Ratio") -> bool: 55 if not isinstance(other, (Ratio)): 56 return NotImplemented 57 return self.ratio > other.ratio 58 59 def __le__(self, other: "Ratio") -> bool: 60 if not isinstance(other, (Ratio)): 61 return NotImplemented 62 return self.ratio <= other.ratio 63 64 def __lt__(self, other: "Ratio") -> bool: 65 if not isinstance(other, (Ratio)): 66 return NotImplemented 67 return self.ratio < other.ratio 68 69 def compliment(self) -> "Ratio": 70 """ 71 Compliment of `Ratio`, when added to the original `Ratio` will equal an octave 72 """ 73 74 return -(self - OCTAVE_RATIO) 75 76 def inversion(self) -> "Ratio": 77 """ 78 Inversion of `Ratio` e.g. up a fifth becomes down a fifth 79 """ 80 81 return Ratio(1.0 / self.ratio)
Describes an abstract interval between two Tone
s, the ratio between their frequencies
9class Interval(Ratio): 10 """ 11 Describes a musical interval as a ratio quantized to a 12TET semitone 12 """ 13 14 semitones: int 15 "Integer number of 12TET semitones" 16 17 quality: str 18 "Quality of `Interval`, 'M' for major, 'm' for minor, 'P' for perfect, 'A' for augmented, 'd' for diminished" 19 20 quantity: int 21 "Quantity of `Interval` as an integer number of major scale steps, for example M10 quality is 10" 22 23 def __init__(self, interval: Union[int, str]): 24 """ 25 `interval` can be either an interval name like "M5" "m2" "d5" "A2" "P5" or an integer number of semitones 26 """ 27 28 assert isinstance(interval, (int, str)) 29 30 if isinstance(interval, int): 31 self.semitones = interval 32 33 abs_semitones = abs(self.semitones) 34 35 self.quality = INTERVAL_VALUE_TO_COMPONENTS[abs_semitones % SEMITONES_PER_OCTAVE][0] 36 37 self.quantity = INTERVAL_VALUE_TO_COMPONENTS[abs_semitones % SEMITONES_PER_OCTAVE][1] + ( 38 7 * (abs_semitones // SEMITONES_PER_OCTAVE) 39 ) 40 41 elif isinstance(interval, str): 42 m = INTERVAL_NAME_RE.match(interval) 43 44 assert m is not None, f"Invalid 12TET interval name: '{interval}'!" 45 46 self.quality = m.group(1) 47 self.quantity = int(m.group(2)) 48 octave = 0 49 offset_quantity = self.quantity 50 51 if offset_quantity > 7: 52 octave = (self.quantity - 1) // 7 53 offset_quantity = ((self.quantity - 1) % 7) + 1 54 55 offset_name = f"{self.quality}{offset_quantity}" 56 57 if offset_name not in INTERVAL_NAME_TO_VALUE: 58 raise ValueError(f"Invalid 12TET interval name: '{interval}'!") 59 60 self.semitones = octave * SEMITONES_PER_OCTAVE + INTERVAL_NAME_TO_VALUE[offset_name] 61 62 interval = (SEMITONE_RATIO * (abs(self.semitones) % SEMITONES_PER_OCTAVE)) + ( 63 OCTAVE_RATIO * (abs(self.semitones) // SEMITONES_PER_OCTAVE) 64 ) 65 super().__init__((interval if self.semitones >= 0 else -interval).ratio) 66 67 def __repr__(self) -> str: 68 return f"[Interval {self.name()} ({self.ratio:.4f})]" 69 70 def __str__(self) -> str: 71 return self.__repr__() 72 73 def __add__(self, other: Union["Interval", Ratio]) -> Union["Interval", Ratio]: 74 if not isinstance(other, (Interval, Ratio)): 75 return NotImplemented 76 77 if isinstance(other, Interval): 78 return Interval(self.semitones + other.semitones) 79 elif isinstance(other, Ratio): 80 return Ratio(self.ratio) + other 81 82 def __sub__(self, other: Union["Interval", Ratio]) -> Union["Interval", Ratio]: 83 if not isinstance(other, (Interval, Ratio)): 84 return NotImplemented 85 86 if isinstance(other, Interval): 87 return Interval(self.semitones - other.semitones) 88 elif isinstance(other, Ratio): 89 return Ratio(self.ratio) - other 90 91 def __mul__(self, other: Union[int, float, Fraction]) -> Union["Interval", Ratio]: 92 if not isinstance(other, (int, float, Fraction)): 93 return NotImplemented 94 95 if isinstance(other, int): 96 return Interval(self.semitones * other) 97 else: 98 return Ratio(self.ratio) * other 99 100 def __neg__(self) -> "Ratio": 101 """ 102 Inversion of `Interval` e.g. up an octave becomes down an octave 103 """ 104 return Interval(-self.semitones) 105 106 def compliment(self) -> "Interval": 107 """ 108 Compliment of `Interval`, when added to the original interval will equal an octave 109 """ 110 return -(self - Interval(SEMITONES_PER_OCTAVE)) 111 112 def name(self) -> str: 113 """ 114 Return name of `Interval` like P5 or m7 or -M3 115 """ 116 return f"{'-' if self.semitones < 0 else ''}{self.quality}{self.quantity}" 117 118 def decompound(self) -> "Interval": 119 """ 120 Returns the same `Interval` without any octave offset 121 """ 122 return Interval(self.semitones % SEMITONES_PER_OCTAVE)
Describes a musical interval as a ratio quantized to a 12TET semitone
23 def __init__(self, interval: Union[int, str]): 24 """ 25 `interval` can be either an interval name like "M5" "m2" "d5" "A2" "P5" or an integer number of semitones 26 """ 27 28 assert isinstance(interval, (int, str)) 29 30 if isinstance(interval, int): 31 self.semitones = interval 32 33 abs_semitones = abs(self.semitones) 34 35 self.quality = INTERVAL_VALUE_TO_COMPONENTS[abs_semitones % SEMITONES_PER_OCTAVE][0] 36 37 self.quantity = INTERVAL_VALUE_TO_COMPONENTS[abs_semitones % SEMITONES_PER_OCTAVE][1] + ( 38 7 * (abs_semitones // SEMITONES_PER_OCTAVE) 39 ) 40 41 elif isinstance(interval, str): 42 m = INTERVAL_NAME_RE.match(interval) 43 44 assert m is not None, f"Invalid 12TET interval name: '{interval}'!" 45 46 self.quality = m.group(1) 47 self.quantity = int(m.group(2)) 48 octave = 0 49 offset_quantity = self.quantity 50 51 if offset_quantity > 7: 52 octave = (self.quantity - 1) // 7 53 offset_quantity = ((self.quantity - 1) % 7) + 1 54 55 offset_name = f"{self.quality}{offset_quantity}" 56 57 if offset_name not in INTERVAL_NAME_TO_VALUE: 58 raise ValueError(f"Invalid 12TET interval name: '{interval}'!") 59 60 self.semitones = octave * SEMITONES_PER_OCTAVE + INTERVAL_NAME_TO_VALUE[offset_name] 61 62 interval = (SEMITONE_RATIO * (abs(self.semitones) % SEMITONES_PER_OCTAVE)) + ( 63 OCTAVE_RATIO * (abs(self.semitones) // SEMITONES_PER_OCTAVE) 64 ) 65 super().__init__((interval if self.semitones >= 0 else -interval).ratio)
interval
can be either an interval name like "M5" "m2" "d5" "A2" "P5" or an integer number of semitones
Quality of Interval
, 'M' for major, 'm' for minor, 'P' for perfect, 'A' for augmented, 'd' for diminished
Quantity of Interval
as an integer number of major scale steps, for example M10 quality is 10
106 def compliment(self) -> "Interval": 107 """ 108 Compliment of `Interval`, when added to the original interval will equal an octave 109 """ 110 return -(self - Interval(SEMITONES_PER_OCTAVE))
Compliment of Interval
, when added to the original interval will equal an octave
10class Note(Tone): 11 """ 12 Describes a musical note as a Tone quantized to 12TET with A4 = 440Hz, where note 0 = C0 13 """ 14 15 letter: str 16 "The alphabet letter of the note, A-G" 17 18 octave: int 19 "The octave of the note, octaves start at C and end at B" 20 21 accidental: int 22 "The accidental semitone value, 0 for natural, 1 for sharp, -1 for flat, 2 for double sharp, -2 for double flat" 23 24 semitone: int 25 "The absolute semitone of the note starting at C0=0" 26 27 def __init__(self, note: Union[int, str]): 28 """ 29 `note` can either be a note name like "Ab4" "G2" "C#6" "F" or an integer number of semitones from C0 30 """ 31 32 if not isinstance(note, (int, str)): 33 raise TypeError() 34 35 if isinstance(note, int): 36 self.semitone = note 37 self.octave = self.semitone // SEMITONES_PER_OCTAVE 38 self.letter = NOTE_SEMITONE_TO_COMPONENTS[self.semitone % SEMITONES_PER_OCTAVE][0] 39 self.accidental = NOTE_SEMITONE_TO_COMPONENTS[self.semitone % SEMITONES_PER_OCTAVE][1] 40 41 elif isinstance(note, str): 42 name = note 43 m = NOTE_NAME_RE.match(name) 44 45 if m is None: 46 raise ValueError(f"Invalid note name '{name}'!") 47 48 self.letter = m.group(1) 49 self.accidental = ACCIDENTAL_NAME_TO_VALUE[m.group(2)] 50 self.octave = int(m.group(3) or NOTE_DEFAULT_OCTAVE) 51 52 c_based_note_semitone = NOTE_NAME_TO_SEMITONE[self.letter] 53 54 self.semitone = c_based_note_semitone + self.accidental + SEMITONES_PER_OCTAVE * self.octave 55 56 super().__init__( 57 ( 58 Tone(C0_FREQUENCY) 59 + Interval(self.semitone % SEMITONES_PER_OCTAVE) 60 + (OCTAVE * (self.semitone // SEMITONES_PER_OCTAVE)) 61 ).frequency 62 ) 63 64 def __repr__(self) -> str: 65 return f"[Note {self.name()} ({self.frequency:.4f})]" 66 67 def __str__(self) -> str: 68 return self.__repr__() 69 70 def __add__(self, other: Union[Interval, Ratio]): 71 if not isinstance(other, (Ratio, Interval)): 72 return NotImplemented 73 return self.transposed(other) 74 75 def __sub__(self, other: Union[Interval, Ratio, "Note"]): 76 if isinstance(other, Ratio): 77 return self.transposed(-other) 78 elif isinstance(other, Note): 79 return Interval(self.semitone - other.semitone) 80 else: 81 return NotImplemented 82 83 def name(self) -> str: 84 """ 85 Return name of note like Ab4 or G6 86 """ 87 88 return f"{self.letter}{ACCIDENTAL_VALUE_TO_NAME[self.accidental]}{self.octave}" 89 90 def transposed(self, interval: Union[Ratio, Interval]) -> Union["Note", "Tone"]: 91 """ 92 Transpose a note by an `Interval` or `Ratio`. Passing in an `Interval` will return a `Note` while passing in a `Ratio` will return a `Tone` 93 """ 94 95 if not isinstance(interval, (Ratio, Interval)): 96 raise TypeError() 97 98 if isinstance(interval, Interval): 99 return Note(self.semitone + interval.semitones) 100 else: 101 return Tone(self.frequency) + interval 102 103 def following(self, note: "Note") -> "Note": 104 """ 105 Return the higher octave of this `Note` following `note` 106 """ 107 108 i = (self - note).decompound() 109 110 i = Interval("P8") if i.semitones == 0 else i 111 112 return note + i 113 114 def preceding(self, note: "Note") -> "Note": 115 """ 116 Return the lower octave of this `Note` preceding `note` 117 """ 118 119 i = -(note - self).decompound() 120 121 i = -Interval("P8") if i.semitones == 0 else i 122 123 return note + i
Describes a musical note as a Tone quantized to 12TET with A4 = 440Hz, where note 0 = C0
27 def __init__(self, note: Union[int, str]): 28 """ 29 `note` can either be a note name like "Ab4" "G2" "C#6" "F" or an integer number of semitones from C0 30 """ 31 32 if not isinstance(note, (int, str)): 33 raise TypeError() 34 35 if isinstance(note, int): 36 self.semitone = note 37 self.octave = self.semitone // SEMITONES_PER_OCTAVE 38 self.letter = NOTE_SEMITONE_TO_COMPONENTS[self.semitone % SEMITONES_PER_OCTAVE][0] 39 self.accidental = NOTE_SEMITONE_TO_COMPONENTS[self.semitone % SEMITONES_PER_OCTAVE][1] 40 41 elif isinstance(note, str): 42 name = note 43 m = NOTE_NAME_RE.match(name) 44 45 if m is None: 46 raise ValueError(f"Invalid note name '{name}'!") 47 48 self.letter = m.group(1) 49 self.accidental = ACCIDENTAL_NAME_TO_VALUE[m.group(2)] 50 self.octave = int(m.group(3) or NOTE_DEFAULT_OCTAVE) 51 52 c_based_note_semitone = NOTE_NAME_TO_SEMITONE[self.letter] 53 54 self.semitone = c_based_note_semitone + self.accidental + SEMITONES_PER_OCTAVE * self.octave 55 56 super().__init__( 57 ( 58 Tone(C0_FREQUENCY) 59 + Interval(self.semitone % SEMITONES_PER_OCTAVE) 60 + (OCTAVE * (self.semitone // SEMITONES_PER_OCTAVE)) 61 ).frequency 62 )
note
can either be a note name like "Ab4" "G2" "C#6" "F" or an integer number of semitones from C0
The accidental semitone value, 0 for natural, 1 for sharp, -1 for flat, 2 for double sharp, -2 for double flat
83 def name(self) -> str: 84 """ 85 Return name of note like Ab4 or G6 86 """ 87 88 return f"{self.letter}{ACCIDENTAL_VALUE_TO_NAME[self.accidental]}{self.octave}"
Return name of note like Ab4 or G6
90 def transposed(self, interval: Union[Ratio, Interval]) -> Union["Note", "Tone"]: 91 """ 92 Transpose a note by an `Interval` or `Ratio`. Passing in an `Interval` will return a `Note` while passing in a `Ratio` will return a `Tone` 93 """ 94 95 if not isinstance(interval, (Ratio, Interval)): 96 raise TypeError() 97 98 if isinstance(interval, Interval): 99 return Note(self.semitone + interval.semitones) 100 else: 101 return Tone(self.frequency) + interval
103 def following(self, note: "Note") -> "Note": 104 """ 105 Return the higher octave of this `Note` following `note` 106 """ 107 108 i = (self - note).decompound() 109 110 i = Interval("P8") if i.semitones == 0 else i 111 112 return note + i
Return the higher octave of this Note
following note
7class Mode: 8 """ 9 A musical `Mode` consisting of intervals making up an abstract tonicless `Scale` 10 """ 11 12 intervals: list[Interval] 13 "List of `Interval`'s from the tonic of the `Mode`" 14 15 def __init__(self, intervals: list[Interval]): 16 if not isinstance(intervals, list): 17 raise TypeError() 18 self.intervals = intervals 19 20 def __repr__(self) -> str: 21 return f"[Mode {' '.join([i.name() for i in self.intervals])}]" 22 23 def __str__(self) -> str: 24 return self.__repr__() 25 26 def __eq__(self, other: "Mode"): 27 return ( 28 isinstance(other, Mode) 29 and len(self.intervals) == len(other.intervals) 30 and all(x == y for x, y in zip(self.intervals, other.intervals)) 31 ) 32 33 def __ne__(self, other: "Mode"): 34 return ( 35 not isinstance(other, Mode) 36 or len(self.intervals) != len(other.intervals) 37 or any(x != y for x, y in zip(self.intervals, other.intervals)) 38 ) 39 40 def __lshift__(self, other: int) -> "Mode": 41 if not isinstance(other, int): 42 return NotImplemented 43 return self.shifted(other) 44 45 def __rshift__(self, other: int) -> "Mode": 46 if not isinstance(other, int): 47 return NotImplemented 48 return self.shifted(-other) 49 50 def shifted(self, steps: int) -> "Mode": 51 """ 52 Return a shifted version of this `Mode` essentially the same `Mode` but starting on step `steps` 53 """ 54 55 if not isinstance(steps, int): 56 raise TypeError() 57 58 steps = steps % len(self.intervals) 59 60 shifted_intervals = [] 61 62 for i in range(len(self.intervals)): 63 shifted_intervals.append( 64 (self.intervals[(i + steps) % len(self.intervals)] - self.intervals[steps]).decompound() 65 ) 66 67 return Mode(shifted_intervals) 68 69 def to_scale(self, tonic: Note) -> "Scale": 70 return Scale([tonic + interval for interval in self.intervals])
50 def shifted(self, steps: int) -> "Mode": 51 """ 52 Return a shifted version of this `Mode` essentially the same `Mode` but starting on step `steps` 53 """ 54 55 if not isinstance(steps, int): 56 raise TypeError() 57 58 steps = steps % len(self.intervals) 59 60 shifted_intervals = [] 61 62 for i in range(len(self.intervals)): 63 shifted_intervals.append( 64 (self.intervals[(i + steps) % len(self.intervals)] - self.intervals[steps]).decompound() 65 ) 66 67 return Mode(shifted_intervals)
5class Scale: 6 """ 7 A musical `Scale` consisting of a list of ordered `Note`s 8 """ 9 10 notes: list[Note] 11 "List of `Note`s in the `Scale`" 12 13 def __init__(self, notes: list[Note]): 14 if not isinstance(notes, list): 15 raise TypeError() 16 self.notes = notes 17 18 def __repr__(self) -> str: 19 return f"[Scale {' '.join([n.name() for n in self.notes])}]" 20 21 def __str__(self) -> str: 22 return self.__repr__() 23 24 def __eq__(self, other: "Scale"): 25 return ( 26 isinstance(other, Scale) 27 and len(self.notes) == len(other.notes) 28 and all(x == y for x, y in zip(self.notes, other.notes)) 29 ) 30 31 def __ne__(self, other: "Scale"): 32 return ( 33 not isinstance(other, Scale) 34 or len(self.notes) != len(other.notes) 35 or any(x != y for x, y in zip(self.notes, other.notes)) 36 ) 37 38 def __lshift__(self, other: int) -> "Scale": 39 if not isinstance(other, int): 40 return NotImplemented 41 return self.shifted(other) 42 43 def __rshift__(self, other: int) -> "Scale": 44 if not isinstance(other, int): 45 return NotImplemented 46 return self.shifted(-other) 47 48 def shifted(self, steps: int) -> "Scale": 49 """ 50 Return a shifted version of this `Scale` essentially the same `Scale` but starting on step `steps` 51 """ 52 53 if not isinstance(steps, int): 54 raise TypeError() 55 56 shifted_notes = self.notes.copy() 57 58 for _ in range(abs(steps)): 59 if steps < 0: 60 shifted_notes.insert(0, shifted_notes.pop(-1).preceding(shifted_notes[0])) 61 elif steps > 0: 62 shifted_notes.append(shifted_notes.pop(0).following(shifted_notes[-1])) 63 64 return Scale(shifted_notes) 65 66 def tonic(self) -> Note: 67 """ 68 Return the tonic of the scale as a `Note` 69 """ 70 return self.notes[0]
48 def shifted(self, steps: int) -> "Scale": 49 """ 50 Return a shifted version of this `Scale` essentially the same `Scale` but starting on step `steps` 51 """ 52 53 if not isinstance(steps, int): 54 raise TypeError() 55 56 shifted_notes = self.notes.copy() 57 58 for _ in range(abs(steps)): 59 if steps < 0: 60 shifted_notes.insert(0, shifted_notes.pop(-1).preceding(shifted_notes[0])) 61 elif steps > 0: 62 shifted_notes.append(shifted_notes.pop(0).following(shifted_notes[-1])) 63 64 return Scale(shifted_notes)