diff --git a/README.md b/README.md index c5a4c89..ca12725 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,17 @@ True ## TODOs - ~~modes~~ -- ~~ midi note numbers (see https://musicinformationretrieval.com/midi_conversion_table.html and https://www.inspiredacoustics.com/en/MIDI_note_numbers_and_center_frequencies)~~ +- ~~midi note numbers (see https://musicinformationretrieval.com/midi_conversion_table.html and https://www.inspiredacoustics.com/en/MIDI_note_numbers_and_center_frequencies)~~ - ~~init note from midi number~~ - intervals - addition and subtraction in scale - finding qualities of interval in scale (notation - https://en.wikipedia.org/wiki/Interval_(music)#Shorthand_notation, quality - https://en.wikipedia.org/wiki/Interval_(music)#Main_intervals) -- add and subtract intervals within a context of a scale -- chords with scales, chord names and notes they consist of (don't forget octaves and root notes) - https://en.wikipedia.org/wiki/List_of_chords -- chords in given scales (similar to notes_in_scale method) + - check interval between two notes in scale +- consonance and dissonance of intervals +- chords with scales, chord names and notes they consist of (don't forget octaves and root notes) - https://en.wikipedia.org/wiki/List_of_chords - also https://en.wikipedia.org/wiki/Interval_(music)#Chord_qualities_and_interval_qualities +- consonance and dissonance of scales +- interval inversion +- chords in given scales (similar to notes_in_scale method) - chord transposing by adding or subtracting tonedeltas - init chord from given notes (check if matches any formula) - find nearest note to a given frequency diff --git a/pyscales/primitives.py b/pyscales/primitives.py index 732f0b2..8da633d 100644 --- a/pyscales/primitives.py +++ b/pyscales/primitives.py @@ -100,6 +100,14 @@ def __eq__(self, other): def __add__(self, other): + from pyscales import IntervalInScale + from pyscales import Interval + if isinstance(other, IntervalInScale): + return other.add_to_note(self) + elif isinstance(other, Interval): + return other.add_to_note(self) + + # assume other is ToneDelta # TODO: cache this somewhere notes_order = NoteArray(NoteArray.DEFAULT_NOTE_ORDER) @@ -118,6 +126,9 @@ def __sub__(self, other): this_note_index = notes_order.index(self) + from pyscales import IntervalInScale + + from pyscales import Interval if hasattr(other, 'semitones'): return notes_order[this_note_index-other.semitones] @@ -128,7 +139,14 @@ def __sub__(self, other): return ToneDelta(this_note_index-other_not_index) - raise ValueError("Can only subtract ToneDelta or other Note") + elif isinstance(other, IntervalInScale): + + return other.subtract_from_note(self) + + elif isinstance(other, Interval): + return other.subtract_from_note(self) + + raise ValueError("Can only subtract ToneDelta, IntervalInScale or other Note") def __str__(self): diff --git a/pyscales/scales.py b/pyscales/scales.py index cbe252a..085f61d 100644 --- a/pyscales/scales.py +++ b/pyscales/scales.py @@ -1,6 +1,9 @@ +import math from copy import copy +from enum import Enum +from typing import Optional -from .primitives import Note, NoteArray +from .primitives import Note, NoteArray, ToneDelta class ScaleFormula: @@ -48,6 +51,10 @@ def __init__(self, root_note:Note, formula: ScaleFormula): self._all_notes = NoteArray(self.DEFAULT_NOTE_ORDER) + def __eq__(self, other): + + return (self.root_note == other.root_note) and (self.formula == other.formula) + def notes_in_scale(self) -> NoteArray: # find root note index @@ -87,4 +94,479 @@ def scale_name(self): def __str__(self): # TODO: change flats and sharps so note names will be unique across scale - return str(self.notes_in_scale()) \ No newline at end of file + return str(self.notes_in_scale()) + + + +class IntervalQuality(Enum): + PERFECT = 0 + MINOR = 1 + MAJOR = 2 + DIMINISHED = 3 + AUGMENTED = 4 + + def name(self): + return INTERVAL_QUALITY_NAMES[self] + + def short_name(self): + return INTERVAL_QUALITY_SHORT_NAME[self] + + def notation(self): + return INTERVAL_QUALITY_NOTATION[self] + +# todo: also add scientific notations +INTERVAL_QUALITY_NAMES = { + IntervalQuality.PERFECT: "Perfect", + IntervalQuality.MINOR: "Minor", + IntervalQuality.MAJOR: "Major", + IntervalQuality.DIMINISHED: "Diminished", + IntervalQuality.AUGMENTED: "Augmented", +} + +INTERVAL_QUALITY_SHORT_NAME = { + IntervalQuality.PERFECT: "perf", + IntervalQuality.MINOR: "min", + IntervalQuality.MAJOR: "maj", + IntervalQuality.DIMINISHED: "dim", + IntervalQuality.AUGMENTED: "aug", +} + + +# see https://en.wikipedia.org/wiki/Interval_(music)#Shorthand_notation +INTERVAL_QUALITY_NOTATION = { + IntervalQuality.PERFECT: "P", + IntervalQuality.MINOR: "m", + IntervalQuality.MAJOR: "M", + IntervalQuality.DIMINISHED: "d", + IntervalQuality.AUGMENTED: "A", +} + +# interval quality map +# based on https://en.wikipedia.org/wiki/Interval_(music)#Main_intervals +# They depend on staff position and quality +# there is logic behind it but its kind of complicated so it's easier to use this map +# to determine quality from staff positions and semitone difference. + +# anyway, I would be glad if there was a more elegant way to determine interval quality. +# if an interval is compound, subtract 12 from semitones and 7 from staff position difference +# to use this map (test this!) +# map usage: +# INTERVAL_QUALITY_MAP[semitone_difference][staff_position_difference] -> IntervalQuality +INTERVAL_QUALITY_MAP = { + # semitone difference as main dictionary key + 0: { + # staff position _difference_, zero-based (0 for unison, 1 for second degree, 7 for octave, etc.) + 0: IntervalQuality.PERFECT, # P1 - perfect unison + 1: IntervalQuality.DIMINISHED # d2 - diminished second + }, + 1: { + 1: IntervalQuality.MINOR, # m2 - minor second + 0: IntervalQuality.AUGMENTED # A1 - augmented unison (not sure how it's possible) + }, + 2: { + 1: IntervalQuality.MAJOR, # M2 - major second + 2: IntervalQuality.DIMINISHED # d3 - diminished third + }, + 3: { + 2: IntervalQuality.MINOR, # m3 - minor third + 1: IntervalQuality.AUGMENTED # A2 - augmented second + }, + 4: { + 2: IntervalQuality.MAJOR, # M3 - major third, + 3: IntervalQuality.DIMINISHED, # d4 - diminished fourth + }, + 5: { + 3: IntervalQuality.PERFECT, # P4 - perfect fourth + 2: IntervalQuality.AUGMENTED # A3 - augmented third + }, + 6: { + 4: IntervalQuality.DIMINISHED, # d5 - diminished 5th, tritone + 3: IntervalQuality.AUGMENTED, # A4 - augmented fourth, tritone + }, + 7: { + 4: IntervalQuality.PERFECT, #P5 - perfect fifth + 5: IntervalQuality.DIMINISHED, #d6 - diminished 6th + }, + 8: { + 5: IntervalQuality.MINOR, #m6 - minor sixth + 4: IntervalQuality.AUGMENTED, #A5 - augmented fifth + }, + 9: { + 5: IntervalQuality.MAJOR, #M6 - major sixth + 6: IntervalQuality.DIMINISHED, #d7 - diminished 7th + }, + 10: { + 6: IntervalQuality.MINOR, #m7 - minor seventh + 5: IntervalQuality.AUGMENTED #A6 - augented sixth + }, + 11: { + 6: IntervalQuality.MAJOR, #M7 - major seventh + 7: IntervalQuality.DIMINISHED #d8 - diminished octave + }, + 12: { + 7: IntervalQuality.PERFECT, #P8 - perfect octave + 6: IntervalQuality.AUGMENTED, #A7 - augmented seventh + } +} + +# reverse map +INTERVAL_QUALITY_TO_SEMITONE_MAP = { + # quality + IntervalQuality.PERFECT: { + # staff positions # semitones + 0: 0, + 3: 5, + 4: 7, + 7: 12, + }, + IntervalQuality.MAJOR: { + # staff positions # semitones + 1: 2, + 2: 4, + 5: 9, + 6: 11, + }, + IntervalQuality.MINOR: { + # staff positions # semitones + 1: 1, + 2: 3, + 5: 8, + 6: 10, + }, + IntervalQuality.DIMINISHED: { + # staff positions # semitones + 1: 0, + 2: 2, + 3: 4, + 4: 6, + 5: 7, + 6: 9, + 7: 11, + }, + IntervalQuality.AUGMENTED: { + # staff positions # semitones + 0: 1, + 1: 3, + 2: 5, + 3: 6, + 4: 8, + 5: 10, + 6: 12, + } +} + + +class IntervalInScale: + """ + Use this to move up and down a scale by given number of staff positions. + Add or subtract this to your notes. + + Not sure what's the correct name for this - degree? Interval in scale? Step? + + This entity is not quality-aware + """ + + def __init__(self, staff_positions: int, scale: Scale): + self.staff_positions: int = staff_positions + self.scale: Scale = scale + + def __add__(self, other): + + if isinstance(other, Note): + + try: + note_index = self.scale.notes_in_scale().index(other) + + # return corresponding note from scale + return copy(self.scale.notes_in_scale()[note_index+self.staff_positions]) + + except ValueError: + raise ValueError(f"Note {other} is not in scale {self.scale}") + + elif isinstance(other, IntervalInScale): + if other.scale == self.scale: + return IntervalInScale(staff_positions=self.staff_positions+other.staff_positions, scale=self.scale) + + raise ValueError("Cannot add two IntervalInScale objects with different scales") + + else: + raise ValueError("Can add IntervalInScale only to Note and other IntervalInScale") + + + def __sub__(self, other): + + if isinstance(other, IntervalInScale): + if other.scale == self.scale: + return IntervalInScale(staff_positions=self.staff_positions - other.staff_positions, scale=self.scale) + + raise ValueError("Cannot add two IntervalInScale objects with different scales") + + else: + raise ValueError("Can only subtract other IntervalInScale from IntervalInScale") + + + def subtract_from_note(self, note: Note): + + interval_in_scale: IntervalInScale = copy(self) + # invert for subtraction + interval_in_scale.staff_positions = -interval_in_scale.staff_positions + + return interval_in_scale.__add__(note) + + def add_to_note(self, note): + + return self.__add__(note) + + + # todo: add __sub__ method to note that calls this one with negative value + + # there is no __eq__ overload as it does not seem to make a lot of sense to compare this entities + + +class Interval: + """ + Interval between notes. + Characterized by quality. Not aware of scale. + """ + + # see https://en.wikipedia.org/wiki/Interval_(music)#Main_intervals + # also https://en.wikipedia.org/wiki/Interval_(music)#Compound_intervals + + def __init__(self, staff_positions: int, quality: Optional[IntervalQuality] = None, + semitones: Optional[int] = None): + """ + @staff_positions - difference in staff positions between notes. Zero-based. + @quality - interval quality, depends on relation between staff positions and + semitone difference between two notes + E.g. to get a Fifth, set this to 4 + see. https://en.wikipedia.org/wiki/Staff_(music)#Staff_positions + """ + self.staff_positions: int = staff_positions + + if not quality and not semitones: + raise ValueError("Provide either semitones or quality value") + + if quality and semitones: + # if for some reason we were provided with both, check them instead of failing: + assessed_quality = Interval.assess_quality(staff_positions, semitones) + if assessed_quality != quality: + raise ValueError(f"You passed both quality and semitones when defining Interval," + f"but quality {quality} does not correspond to {staff_positions} staff positions " + f"with {semitones} semitones difference. Please provide either quality, or" + f"semitones difference.") + + if quality: + self.quality: IntervalQuality = quality + self.semitones: int = semitones or Interval.calculate_semitones_difference(staff_positions, quality) + + elif semitones: + self.semitones: int = semitones + self.quality: IntervalQuality = quality or Interval.assess_quality(staff_positions, semitones) + + + def get_tone_delta(self): + """ + Returns a ToneDelta with the same number of semitones. + Useful for adding or subtracting operations with Notes. + """ + return ToneDelta(semitones=self.semitones) + + def __repr__(self): + + return f"{super(Interval, self).__repr__()[:-1]} ({self.__str__()})>" + + def __add__(self, other): + if isinstance(other, Note): + return self.add_to_note(other) + + # TODO: I am not sure that adding intervals together makes sense. Or does it? + # Do we add both semitones and staff positions? + raise ValueError("You can only add Intervals to notes.") + + def add_to_note(self, note: Note) -> Note: + """ + Adds this interval to a Note to get a new note + """ + return note + self.get_tone_delta() + + def subtract_from_note(self, note: Note) -> Note: + """ + Subtracts this interval from a Note to get a new note + """ + #FIXME: does it actually makes sense to subtract intervals? + return note - self.get_tone_delta() + + # def __sub__(self, other): + # # FIXME: should it be in Note?.. should we use some method in this class when calling it + # # from note so logic will live here? should we just call __add__ with negative value from this class? + # raise NotImplementedError() + # pass + + def __eq__(self, other): + + if (other.staff_positions == self.staff_positions) and (other.quality == self.quality): + return True + + return False + + + def get_quantitative_name(self) -> str: + + # note that the interval name is 1-based, but the actual interval is 0-based + # e.g. unison is called Perfect Unison and is written as P1 although notes are + # 0 staff positions (and semitones) apart + + + return str(self.staff_positions+1) + + # +1 because 1 staff position above something is 2nd e.g. "minor 2nd" + + def __str__(self): + # TODO: make long notation option? + return f"{self.quality.notation() if self.quality else '?'}{self.get_quantitative_name()}" + + + @staticmethod + def calculate_semitones_difference(staff_position_difference: int, quality: IntervalQuality) -> int: + + + shift_octaves = 0 + + # if more than an octave, remove octaves as INTERVAL_QUALITY_TO_SEMITONE_MAP goes only within 1 octave + # but save number of octaves for correct semitone calculation + if staff_position_difference > 7: + + shift_octaves = math.floor(staff_position_difference / 7) + + staff_position_difference %= 7 # FIXME: should not we add 1? + + try: + semitones = INTERVAL_QUALITY_TO_SEMITONE_MAP[quality][staff_position_difference] + 12*shift_octaves + except KeyError: + raise ValueError(f"Cannot find semitones difference corresponding " + f"to {quality.short_name()}{staff_position_difference+1} interval") + + return semitones + + @staticmethod + def assess_quality(staff_position_difference: int, semitone_difference: int) -> IntervalQuality: + """ + @staff_positions - staff positions between notes in scale + @semitones - semitones between these notes + """ + + # basically it boils down to these rules: + # https://music.utk.edu/theorycomp/courses/murphy/documents/Intervals.pdf + # (thank you Dr. Barbara Murphy for a comprehensive explanation unlike + # most of you find on the internet) + + # Intervals are: + + # - perfect if: (1) the top note is in the major key of the bottom note AND (2) the + # - bottom note is in the major key of the top note. + # - major if the top note is in the major key of the bottom note. + # - minor if it is a half step smaller than major. + # - diminished if it is a half step smaller than minor or perfect. + # - augmented if it is a half step larger than major or perfect. + + # TODO: there should probably be an algorithmic way to do this without using a map that is easily computable + + if semitone_difference > 12: + semitone_difference %= 12 # FIXME: should not we add 1 here too? or not? + staff_position_difference %= 7 # FIXME: should not we add 1 here since we have both 0 and 1 in map? + + try: + + quality = INTERVAL_QUALITY_MAP[semitone_difference][staff_position_difference] + + except KeyError: + raise ValueError(f"Cannot find interval quality corresponding to {semitone_difference} semitone " + f"difference and {staff_position_difference} staff (scale) position difference") + + return quality + + @classmethod + def between_notes(cls, note1: Note, note2: Note, scale: Scale): + """ + Returns an interval between two notes. + The returned interval has quality info. + """ + semitone_interval = abs((note1 - note2).semitones) + staff_interval = abs(scale.notes_in_scale().index(note1) - scale.notes_in_scale().index(note2)) + + quality = cls.assess_quality(staff_interval, semitone_interval) + result = Interval(staff_interval, quality) + + return result + + def is_consonant(self): + return self in CONSONANT_INTERVALS + + def is_perfect_consonant(self): + return self in PERFECT_CONSONANT_INTERVALS + + def is_imperfect_consonant(self): + return self in IMPERFECT_CONSONANT_INTERVALS + + def is_dissonant(self): + return self in DISSONANT_INTERVALS + + # TODO: inversions + + +# main (non-compound, less than octave) intervals constants +class Intervals: + P1 = Interval(0, IntervalQuality.PERFECT) + P8 = Interval(7, IntervalQuality.PERFECT) + P4 = Interval(3, IntervalQuality.PERFECT) + P5 = Interval(4, IntervalQuality.PERFECT) + + m2 = Interval(1, IntervalQuality.MINOR) + m3 = Interval(2, IntervalQuality.MINOR) + m6 = Interval(5, IntervalQuality.MINOR) + m7 = Interval(6, IntervalQuality.MINOR) + + M2 = Interval(1, IntervalQuality.MAJOR) + M3 = Interval(2, IntervalQuality.MAJOR) + M6 = Interval(5, IntervalQuality.MAJOR) + M7 = Interval(6, IntervalQuality.MAJOR) + + d5 = Interval(4, IntervalQuality.DIMINISHED) + + A4 = Interval(3, IntervalQuality.AUGMENTED) + + +# https://music.utk.edu/theorycomp/courses/murphy/documents/Intervals.pdf +PERFECT_CONSONANT_INTERVALS = ( + Intervals.P1, # P1 # TODO: add constants for all these intervals for easier reference? + Intervals.P8, + Intervals.P5, # P5 + # P4 is weird so it's not added here: + # The P4 is sometimes consonant and sometimes dissonant. + # In early music, P4 was a consonance and with other perfect intervals made up + # most of music compositions. + # Later, when using complete triads, the 4th tended to lose some of its stability and + # consonance (sounded like active tone between 5th and 3rd). If it appeared + # near a 5th (its inversion), then it was stable. + # SO -- the P4 is a consonant when it functions as an inverted 5th (as part of the + # chord); otherwise, it is dissonant. +) + +IMPERFECT_CONSONANT_INTERVALS = ( + Intervals.m3, # m3 + Intervals.M3, # M3 + Intervals.m6, # m6 + Intervals.M6, # M6 +) + +CONSONANT_INTERVALS = PERFECT_CONSONANT_INTERVALS + IMPERFECT_CONSONANT_INTERVALS + +DISSONANT_INTERVALS = ( + Intervals.m2, # m2 + Intervals.M2, # M2 + Intervals.m7, # m7 + Intervals.M7, # M7 + # tritones + Intervals.d5, # d5 - diminished 5th, tritone + Intervals.A4, # A4 - augmented 4th, tritone +) diff --git a/tests.py b/tests.py index 86581f0..8276c04 100644 --- a/tests.py +++ b/tests.py @@ -1,8 +1,8 @@ import unittest from pyscales import constants, scaleformulas -from pyscales.primitives import Note, NoteArray -from pyscales.scales import Scale +from pyscales.primitives import Note, NoteArray, ToneDelta +from pyscales.scales import Scale, IntervalInScale, Interval, IntervalQuality, Intervals class TestNoteFrequencies(unittest.TestCase): @@ -110,5 +110,246 @@ def test_initializing_notes_from_midi_values(self): self.assertEqual(note.octave_number, line[1]) +class IntervalInScaleTestCase(unittest.TestCase): + + def test_interval_in_scale_addition_to_each_other(self): + a = IntervalInScale(staff_positions=2, scale=Scale(Note("C", 4), scaleformulas.MINOR_FORMULA)) + b = IntervalInScale(staff_positions=3, scale=Scale(Note("C", 4), scaleformulas.MINOR_FORMULA)) + + c = a+b + + self.assertEqual(c.staff_positions, 5) + + a = IntervalInScale(staff_positions=5, scale=Scale(Note("C", 4), scaleformulas.MINOR_FORMULA)) + b = IntervalInScale(staff_positions=23, scale=Scale(Note("C", 4), scaleformulas.MINOR_FORMULA)) + + c = a + b + + self.assertEqual(c.staff_positions, 28) + + def test_interval_in_scale_subtraction_from_each_other(self): + + a = IntervalInScale(staff_positions=4, scale=Scale(Note("C", 4), scaleformulas.MAJOR_FORMULA)) + b = IntervalInScale(staff_positions=1, scale=Scale(Note("C", 4), scaleformulas.MAJOR_FORMULA)) + + c = a - b + + self.assertEqual(c.staff_positions, 3) + + a = IntervalInScale(staff_positions=2, scale=Scale(Note("C", 4), scaleformulas.MINOR_FORMULA)) + b = IntervalInScale(staff_positions=3, scale=Scale(Note("C", 4), scaleformulas.MINOR_FORMULA)) + + c = a-b + + self.assertEqual(c.staff_positions, -1) + + a = IntervalInScale(staff_positions=23, scale=Scale(Note("C", 4), scaleformulas.MINOR_FORMULA)) + b = IntervalInScale(staff_positions=5, scale=Scale(Note("C", 4), scaleformulas.MINOR_FORMULA)) + + c = a - b + + self.assertEqual(c.staff_positions, 18) + + def test_interval_in_scale_addition_to_note(self): + + note = Note("C", 4) + scale = Scale(Note("C", 4), scaleformulas.MAJOR_FORMULA) + + interval_in_scale = IntervalInScale(staff_positions=0, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("C", 4)) + + interval_in_scale = IntervalInScale(staff_positions=1, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("D", 4)) + + interval_in_scale = IntervalInScale(staff_positions=2, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("E", 4)) + + interval_in_scale = IntervalInScale(staff_positions=7, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("C", 5)) + + interval_in_scale = IntervalInScale(staff_positions=8, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("D", 5)) + + interval_in_scale = IntervalInScale(staff_positions=-1, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("B", 3)) + + + note = Note("A", 4) + scale = Scale(Note("F#", 4), scaleformulas.MINOR_FORMULA) + + interval_in_scale = IntervalInScale(staff_positions=2, scale=scale) + result = note + interval_in_scale + self.assertEqual(Note("C#", 5), result) # octaves numbers are absolute, not dependant on scales. C marks new octave start + + interval_in_scale = IntervalInScale(staff_positions=-1, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("G#", 4)) + + note = Note("C", 4) + scale = Scale(Note("A", 4), scaleformulas.MINOR_FORMULA) + + interval_in_scale = IntervalInScale(staff_positions=3, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("F", 4)) + + interval_in_scale = IntervalInScale(staff_positions=1, scale=scale) + result = note + interval_in_scale + self.assertEqual(result, Note("D", 4)) + + + def test_interval_in_scale_subtraction_from_note(self): + + note = Note("C", 4) + scale = Scale(Note("C", 4), scaleformulas.MAJOR_FORMULA) + + interval_in_scale = IntervalInScale(staff_positions=0, scale=scale) + result = note - interval_in_scale + self.assertEqual(result, Note("C", 4)) + + interval_in_scale = IntervalInScale(staff_positions=2, scale=scale) + result = note - interval_in_scale + self.assertEqual(Note("A", 3), result) + + note = Note("F", 4) + scale = Scale(Note("Eb", 4), scaleformulas.MAJOR_FORMULA) + + interval_in_scale = IntervalInScale(staff_positions=0, scale=scale) + result = note - interval_in_scale + self.assertEqual(result, Note("F", 4)) + + interval_in_scale = IntervalInScale(staff_positions=1, scale=scale) + result = note - interval_in_scale + self.assertEqual(result, Note("Eb", 4)) + self.assertEqual(result, Note("D#", 4)) # same thing + + interval_in_scale = IntervalInScale(staff_positions=2, scale=scale) + result = note - interval_in_scale + self.assertEqual(result, Note("D", 4)) + + interval_in_scale = IntervalInScale(staff_positions=4, scale=scale) + result = note - interval_in_scale + self.assertEqual(result, Note("Bb", 3)) + + + + +class IntervalsTestCase(unittest.TestCase): + + def test_interval_between_two_notes_in_scale(self): + + c_major = Scale(Note("C", 4), scaleformulas.MAJOR_FORMULA) + + interval = Interval.between_notes(Note("D", 4), Note("C", 4), c_major) + + self.assertEqual(Intervals.M2, interval) + self.assertTrue(interval.is_dissonant()) + self.assertEqual(IntervalQuality.MAJOR, interval.quality) + self.assertEqual(2, interval.semitones) + self.assertEqual(1, interval.staff_positions) + + + interval = Interval.between_notes(Note("C", 4), Note("F", 4), c_major) + + self.assertEqual(Intervals.P4, interval) + # self.assertTrue(interval.is_perfect_consonant()) + # self.assertTrue(interval.is_consonant()) + self.assertEqual(IntervalQuality.PERFECT, interval.quality) + self.assertEqual(5, interval.semitones) + self.assertEqual(3, interval.staff_positions) + + # switch notes positions - should have same results + interval = Interval.between_notes(Note("F", 4), Note("C", 4), c_major) + + self.assertEqual(Intervals.P4, interval) + # self.assertTrue(interval.is_perfect_consonant()) # this interval is weird, not sure if its coonsonant + # self.assertTrue(interval.is_consonant()) + self.assertEqual(IntervalQuality.PERFECT, interval.quality) + self.assertEqual(5, interval.semitones) + self.assertEqual(3, interval.staff_positions) + + a_minor = Scale(Note("A", 4), scaleformulas.MINOR_FORMULA) + interval = Interval.between_notes(Note("A", 4), Note("G", 5), a_minor) + + self.assertEqual(Intervals.m7, interval) + self.assertTrue(interval.is_dissonant()) + self.assertEqual(IntervalQuality.MINOR, interval.quality) + self.assertEqual(10, interval.semitones) + self.assertEqual(6, interval.staff_positions) + + # check again in major just to be sure it's the same + interval = Interval.between_notes(Note("A", 4), Note("G", 5), c_major) + + self.assertEqual(Intervals.m7, interval) + self.assertTrue(interval.is_dissonant()) + self.assertEqual(IntervalQuality.MINOR, interval.quality) + self.assertEqual(10, interval.semitones) + self.assertEqual(6, interval.staff_positions) + + interval = Interval.between_notes(Note("F", 4), Note("B", 4), c_major) + + self.assertEqual(Intervals.A4, interval) + self.assertTrue(interval.is_dissonant()) + self.assertEqual(IntervalQuality.AUGMENTED, interval.quality) + self.assertEqual(6, interval.semitones) + self.assertEqual(3, interval.staff_positions) + + + + + def test_compound_interval_between_two_notes_in_scale(self): + + # check https://en.wikipedia.org/wiki/Interval_(music)#Main_compound_intervals + + # raise NotImplementedError() + + + c_major = Scale(Note("C", 4), scaleformulas.MAJOR_FORMULA) + + interval = Interval.between_notes(Note("C", 4), Note("D", 4), c_major) + + self.assertEqual(Intervals.M2, interval) + self.assertTrue(interval.is_dissonant()) + self.assertEqual(IntervalQuality.MAJOR, interval.quality) + self.assertEqual(2, interval.semitones) + self.assertEqual(1, interval.staff_positions) + + interval = Interval.between_notes(Note("C", 4), Note("D", 4) + ToneDelta(12), c_major) + + # self.assertEqual(Intervals.M2, interval) + # self.assertTrue(interval.is_dissonant()) + self.assertEqual(IntervalQuality.MAJOR, interval.quality) + self.assertEqual(2+12, interval.semitones) + self.assertEqual(1+7, interval.staff_positions) + + + interval = Interval.between_notes(Note("A", 4), Note("G", 5) + ToneDelta(12), c_major) + + # self.assertEqual(Intervals.m7, interval) + # self.assertTrue(interval.is_dissonant()) + self.assertEqual(IntervalQuality.MINOR, interval.quality) + self.assertEqual(10+12, interval.semitones) + self.assertEqual(6+7, interval.staff_positions) + + # todo: add more compound cases here!! + # todo: add main compound cases from wiki here + # https://en.wikipedia.org/wiki/Interval_(music)#Main_compound_intervals + + + + def test_interval_addition_to_note(self): + raise NotImplementedError() + + def test_compound_interval_addition_to_note(self): + raise NotImplementedError() + + # compound intervals + + if __name__ == '__main__': unittest.main()