Пример #1
0
import pytest

from chordparser.analysers.chords_analyser import ChordAnalyser
from chordparser.editors.chords_editor import ChordEditor
from chordparser.editors.keys_editor import KeyEditor
from chordparser.editors.notes_editor import NoteEditor
from chordparser.editors.scales_editor import ScaleEditor


NE = NoteEditor()
KE = KeyEditor()
SE = ScaleEditor()
CE = ChordEditor()
CA = ChordAnalyser()


@pytest.mark.parametrize(
    "chord, mode, incl_submodes, result", [
        ("C", "major", True, [('I', 'major', None)]),
        ("C7", "major", False, [('I7', 'major', None)]),
        ("Cm", "minor", False, [('i', 'minor', 'natural')]),
        ("Cm", "minor", True, [('i', 'minor', 'natural'), ('i', 'minor', 'harmonic'), ('i', 'minor', 'melodic')]),
        ]
    )
def test_diatonic(chord, mode, incl_submodes, result):
    c = CE.create_chord(chord)
    s = SE.create_scale("C", mode)
    assert CA.analyse_diatonic(c, s, incl_submodes) == result


@pytest.mark.parametrize(
Пример #2
0
class ChordRomanConverter:
    """A `Chord`-`Roman` converter.

    The `ChordRomanConverter` can convert `Chords` to `Romans` based on a `Scale` or `Key`.

    """
    _symbols = {
        -1: '\u266d',
        -2: '\U0001D12B',
        +1: '\u266f',
        +2: '\U0001D12A',
        0: '',
    }
    _inversions = {
        3: (6, ),
        5: (6, 4),
    }
    _inversions_ext = {
        3: (6, 5),
        5: (4, 3),
        7: (4, 2),
    }
    _roman_deg = {
        1: 'I',
        2: 'II',
        3: 'III',
        4: 'IV',
        5: 'V',
        6: 'VI',
        7: 'VII',
    }
    _q_dict = {
        'diminished': '\u00B0',
        'augmented': '+',
        'half-diminished': '\u00f8',
    }
    _NE = NoteEditor()
    _CE = ChordEditor()
    _SE = ScaleEditor()

    def to_roman(self, chord, scale_key):
        """Converts a `Chord` to `Roman`.

        Creates the `Roman` based on the `Chord` and a `Scale` or `Key`.

        Parameters
        ----------
        chord : Chord
            The `Chord` to be converted.
        scale_key : Scale or Key
            The `Scale` or `Key` to base the `Roman` on.

        Returns
        -------
        Roman
            The `Roman` of the `Chord`.

        Warns
        -----
        UserWarning
            If the `Chord` is a power or sus chord.

        Examples
        --------
        >>> KE = KeyEditor()
        >>> SE = ScaleEditor()
        >>> CE = ChordEditor()
        >>> CRC = ChordRomanConverter()
        >>> c_key = KE.create_key("C")
        >>> c_scale = SE.create_scale(c_key)
        >>> d = CE.create_diatonic(c_scale, 2)
        >>> CRC.to_roman(d, c_key)
        ii roman chord
        >>> f = CE.create_diatonic(c_scale, 4)
        >>> CRC.to_roman(f, c_scale)
        IV roman chord

        """
        if chord.quality.value in {"power", "sus2", "sus4"}:
            warnings.warn(
                "Warning: Power and sus chords are defaulted to major chords",
                UserWarning)
            chord = self._CE.change_chord(chord, quality="Maj", inplace=False)
        if isinstance(scale_key, Scale):
            scale_root = scale_key.key.root
        else:
            scale_root = scale_key.root
        scale = self._SE.create_scale(scale_root, "major")
        root = self._get_roman_root(chord, scale)
        quality = self._get_roman_quality(chord)
        inversion = self._get_roman_inversion(chord)
        return Roman(root, quality, inversion)

    def _get_roman_root(self, chord, scale):
        """Get Roman notation of the chord root."""
        degree = min(
            scale.notes.index(x)
            for x in scale.notes if x.letter == chord.root.letter) + 1
        # lower/uppercase numeral
        if chord.quality.value in {"major", "augmented", "dominant"}:
            quality_fn = str.upper
        else:
            quality_fn = str.lower
        root = quality_fn(ChordRomanConverter._roman_deg[degree])
        (shift, ) = self._NE.get_min_intervals(scale.notes[degree - 1],
                                               chord.root)
        sym = ChordRomanConverter._symbols[shift]  # accidental of root
        return sym + root

    def _get_roman_inversion(self, chord):
        """Get Roman inversion notation of the chord."""
        notes = len(chord.base_notes)
        if chord.inversion and notes <= 4:  # get inversion notation
            if chord.quality.ext:
                inv_dict = ChordRomanConverter._inversions_ext
            else:
                inv_dict = ChordRomanConverter._inversions
            return inv_dict.get(chord.inversion, (notes * 2 - 1, ))
        elif notes > 3:
            return (notes * 2 - 1, )  # chord extension
        return ()

    def _get_roman_quality(self, chord):
        """Get Roman notation for chord quality."""
        q_str = ""
        if chord.quality.ext and re.match("major", chord.quality.ext):
            q_str += "M"
        q_str += ChordRomanConverter._q_dict.get(chord.quality.value, "")
        return q_str
Пример #3
0
class Chord:
    """A musical class representing a chord.

    The `Chord` is composed of a `root` `Note`, `quality`, optional `add` `Notes` and an optional `bass` `Note`. It automatically builds its `notes` from these components. When printed, a standardised short notation meant for chord sheets is displayed.

    Parameters
    ----------
    root : Note
        The root note.
    quality : Quality
        The `Chord` quality.
    add : list of (str, int), Optional
        List of added notes. The `str` is the accidental and the `int` is the scale degree of each added note.
    bass : Note, Optional
        Bass note.
    string : str, Optional
        The `Chord` notation string input.

    Attributes
    ----------
    root : Note
        The root note.
    quality : Quality
        The `Chord` quality.
    add : list of (str, int), Optional
        List of added notes.
    bass : Note, Optional
        Bass note.
    string : str, Optional
        The `Chord` notation string input.
    base_intervals : tuple of int
        The intervals of the `Chord` solely based on its `quality`.
    base_degrees : tuple of int
        The scale degrees of the `Chord` solely based on its `quality`.
    base_symbols : tuple of str
        The accidentals of the `Chord` solely based on its `quality`.
    intervals : tuple of int
        The intervals of the `Chord`.
    degrees : tuple of int
        The scale degrees of the `Chord`.
    symbols : tuple of str
        The accidentals of the `Chord`.
    notes : tuple of Note
        The tuple of `Notes` in the `Chord`.

    """

    _SE = ScaleEditor()
    _NE = NoteEditor()

    def __init__(self, root, quality, add=None, bass=None, string=None):
        self.root = root
        self.quality = quality
        self.add = add
        self.bass = bass
        self.string = string
        self.build()

    def build(self):
        """Build the `Chord` from its attributes.

        This method does not need to be used if `Chord` adjustments are done through the proper channels (i.e. `ChordEditor` or using other `Chord` methods), since those would build the `Chord` automatically.

        """
        self._build_base_chord()
        self._build_full_chord()
        self._build_notation()

    def _build_base_chord(self):
        """Build the chord without any added or bass notes."""
        self._base_scale = self._SE.create_scale(self.root.value)
        self.base_intervals = self.quality.intervals
        self.base_degrees = self.quality.degrees
        self.base_symbols = self.quality.symbols
        # get a copy of the root
        root = self._NE.create_note(self.root.value)
        notes = [root]
        idx = len(self.base_intervals)
        for i in range(idx):
            new = self._NE.create_note(notes[-1].value)
            new.transpose(
                self.base_intervals[i],
                self.base_degrees[i+1] - self.base_degrees[i]
                )
            notes.append(new)
        self.base_notes = tuple(notes)

    def _build_full_chord(self):
        self.notes = list(self.base_notes)
        self.degrees = list(self.base_degrees)
        self.symbols = list(self.base_symbols)
        self._build_add()
        self._build_bass_note()
        self.notes = tuple(self.notes)
        self.degrees = tuple(self.degrees)
        self.symbols = tuple(self.symbols)
        self.intervals = self._NE.get_intervals(*self.notes)

    def _build_add(self):
        """Add notes for chords with added notes."""
        if not self.add:
            return
        symbols = {
            '\u266d': -1, '\U0001D12B': -2,
            '\u266f': +1, '\U0001D12A': +2,
            '': 0,
            }
        for each in self.add:
            sym = each[0]
            tone = each[1]
            shift = symbols[sym]
            pos = max(
                self.degrees.index(i)
                for i in self.degrees
                if i < tone
                ) + 1
            self.symbols.insert(pos, sym)
            self.degrees.insert(pos, tone)
            # copy the note
            new_note = self._NE.create_note(self._base_scale.notes[tone-1].value)
            new_note.shift_s(shift)
            self.notes.insert(pos, new_note)

    def _build_bass_note(self):
        """Build the bass note."""
        self.inversion = None
        if not self.bass:
            return
        if self.bass in self.notes:
            idx = self.notes.index(self.bass)
            self.notes.insert(0, self.notes.pop(idx))
            self.symbols.insert(0, self.symbols.pop(idx))
            self.degrees.insert(0, self.degrees.pop(idx))
            self.inversion = self.degrees[0]
            return
        self.notes.insert(0, self.bass)
        degree = min(
            self._base_scale.notes.index(x)
            for x in self._base_scale.notes
            if x.letter == self.bass.letter
            ) + 1
        self.degrees.insert(0, degree)
        (shift,) = self._NE.get_min_intervals(
            self._base_scale.notes[degree-1],
            self.bass
        )
        symbols = {
            -1: '\u266d', -2: '\U0001D12B',
            +1: '\u266f', +2: '\U0001D12A',
            0: '',
            }
        sym = symbols[shift]
        self.symbols.insert(0, sym)

    def _build_notation(self):
        """Build a standardised chord notation."""
        add = ""
        if self.add:
            for each in self.add:
                if not add and not str(self.quality):
                    string = 'add'  # when quality is blank
                elif not each[0]:
                    string = 'add'  # when there's no accidental
                else:
                    string = ''
                add += string + each[0] + str(each[1])
        if self.bass:
            bass = '/'+str(self.bass)
        else:
            bass = ''
        self._notation = str(self.root) + str(self.quality) + add + bass
        if not self.string:
            self.string = self._notation

    def transpose(self, semitones, letter):
        """Transpose a `Chord` according to semitone and letter intervals.

        Parameters
        ----------
        semitones
            The difference in semitones to the new transposed `root` of the `Chord`.
        letters
            The difference in scale degrees to the new transposed `root` of the `Chord`.

        Examples
        --------
        >>> CE = ChordEditor()
        >>> c = CE.create_chord("Csus")
        >>> c.transpose(6, 3)
        F\u266fsus chord
        >>> c.transpose(0, 1)
        G\u266dsus chord

        """
        self.root.transpose(semitones, letter)
        if self.bass:
            self.bass.transpose(semitones, letter)
        self.build()
        return self

    def transpose_simple(self, semitones, use_flats=False):
        """Transpose a `Chord` according to semitone intervals.

        Parameters
        ----------
        semitones : int
            The difference in semitones to the new transposed `root` of the `Chord`.
        use_flats : boolean, Optional
            Selector to use flats or sharps for black keys. Default False when optional.

        Examples
        --------
        >>> CE = ChordEditor()
        >>> c = CE.create_chord("Cm")
        >>> c.transpose_simple(6)
        F\u266fm chord
        >>> c.transpose(2, use_flats=True)
        A\u266dm chord

        """
        prev = self._NE.create_note(self.root.value)
        self.root.transpose_simple(semitones, use_flats)
        if self.bass:
            # bass has to be transposed exact!
            (diff,) = self._NE.get_tone_letter(prev, self.root)
            self.bass.transpose(*diff)
        self.build()
        return self

    def __repr__(self):
        return f'{self._notation} chord'

    def __str__(self):
        return self._notation

    def __eq__(self, other):
        """Compare between other `Chords`.

        Checks if the other `Chord` has the same attributes. Since the attributes and not the notation is being compared, `Chords` with different notation but same structure are equal (see ``Examples``).

        Parameters
        ----------
        other
            The object to be compared with.

        Returns
        -------
        boolean
            The outcome of the `value` comparison.

        Examples
        --------
        >>> CE = ChordEditor()
        >>> d = CE.create_chord("Dsus")
        >>> d2 = CE.create_chord("Dsus4")
        >>> d == d2
        True
        >>> d3 = CE.create_chord("Dsus2")
        >>> d == d3
        False

        Another example of the same `Chord` with different notation:

        >>> CE = ChordEditor()
        >>> e = CE.create_chord("Eaug7")
        >>> e2 = CE.create_chord("E7#5")
        >>> e == e2
        True

        """
        if not isinstance(other, Chord):
            return NotImplemented
        return (
            self.root == other.root
            and self.quality == other.quality
            and self.add == other.add
            and self.bass == other.bass
            )
Пример #4
0
class ChordAnalyser:
    """A `Chord` analyser.

    The ChordAnalyser can analyse `Chords` with reference to `Scales` and other `Chords` and find their relationships or functions.

    """
    _mode_list = [
        'major',
        'minor',
        'dorian',
        'mixolydian',
        'lydian',
        'phrygian',
        'locrian',
    ]
    _CE = ChordEditor()
    _SE = ScaleEditor()
    _CRC = ChordRomanConverter()

    def analyse_diatonic(
        self,
        chord,
        scale,
        incl_submodes=False,
        allow_power_sus=False,
        default_power_sus="M",
    ):
        """Analyse if a `Chord` is diatonic to a `Scale`.

        There may be multiple tuples in the returned list if submodes are included.

        Parameters
        ----------
        chord : Chord
            The `Chord` to be analysed.
        scale : Scale
            The `Scale` to check against.
        incl_submodes : boolean, Optional
            Selector to include the minor submodes if `scale` is minor. Default False when optional.
        allow_power_sus : boolean, Optional
            Selector to allow power and sus chords when analysing them. Default False when optional.
        default_power_sus : {"M", "m"}, Optional
            The default quality to convert power and sus chords to if analysing them. "M" is major and "m" is minor.

        Returns
        -------
        list of (Roman, str, str)
            A list of information on the `Chord` if it is diatonic. The first `str` is the `Scale`'s `mode` and the second `str` is the `Scale`'s `submode`. The list is empty if the `Chord` is not diatonic.

        Examples
        --------
        >>> CE = ChordEditor()
        >>> SE = ScaleEditor()
        >>> CA = ChordAnalyser()

        Diatonic chords

        >>> c_scale = SE.create_scale("C", "minor")
        >>> degree_1 = CE.create_diatonic(c_scale, 1)
        >>> CA.analyse_diatonic(degree_1, c_scale)
        [(i roman chord, 'minor', 'natural')]

        Checking against minor submodes

        >>> degree_7 = CE.create_chord("G")
        >>> CA.analyse_diatonic(degree_7, c_scale)
        []
        >>> CA.analyse_diatonic(degree_7, c_scale, incl_submodes=True)
        [(VII roman chord, 'minor', 'harmonic'), (VII roman chord, 'minor', 'melodic')]

        Analysing power/sus chords

        >>> power = CE.create_chord("C5")
        >>> CA.analyse_diatonic(power, c_scale)
        []
        >>> CA.analyse_diatonic(power, c_scale, allow_power_sus=True, default_power_sus="m")
        [(i roman chord, 'minor', 'natural')]

        """
        if chord.quality.value in {"power", "sus2", "sus4"}:
            if not allow_power_sus:
                return []
            chord = self._CE.change_chord(chord,
                                          quality=default_power_sus,
                                          inplace=False)
        if not incl_submodes:
            j = [scale.key.submode]
        elif scale.key.mode in {'minor', 'aeolian'}:
            j = ['natural', 'harmonic', 'melodic']
            j.remove(scale.key.submode)
            j.insert(0, scale.key.submode)  # shift to the front
        else:
            j = [None]
        chords = []
        for submode in j:
            for i in range(7):
                nscale = self._SE.create_scale(scale.key.root, scale.key.mode,
                                               submode)
                diatonic = self._CE.create_diatonic(nscale, i + 1)
                if chord.base_notes[0:3] == diatonic.base_notes:
                    chords.append(
                        (self._CRC.to_roman(chord,
                                            nscale), nscale.key.mode, submode))
        return chords

    def analyse_all(
        self,
        chord,
        scale,
        incl_submodes=False,
        allow_power_sus=False,
        default_power_sus="M",
    ):
        """Analyse if a `Chord` is diatonic to a `Scale` for any mode.

        The `Chord` is analysed against the `Scale` as well as the other modes of the `Scale`.

        Parameters
        ----------
        chord : Chord
            The `Chord` to be analysed.
        scale : Scale
            The `Scale` to check against.
        incl_submodes : boolean, Optional
            Selector to include the minor submodes if `scale` is minor. Default False when optional.
        allow_power_sus : boolean, Optional
            Selector to allow power and sus chords when analysing them. Default False when optional.
        default_power_sus : {"M", "m"}, Optional
            The default quality to convert power and sus chords to if analysing them. "M" is major and "m" is minor.

        Returns
        -------
        list of (Roman, str, str)
            A list of information on the `Chord` if it is diatonic. The first `str` is the `mode` of the scale it is diatonic to and the second `str` is the `submode`. The list is empty if the `Chord` is not diatonic.

        Examples
        --------
        >>> CE = ChordEditor()
        >>> SE = ScaleEditor()
        >>> CA = ChordAnalyser()

        Diatonic chords

        >>> c_scale = SE.create_scale("C", "minor")
        >>> degree_1 = CE.create_diatonic(c_scale, 1)
        >>> CA.analyse_all(degree_1, c_scale)
        [(i roman chord, 'minor', 'natural'), (i roman chord, 'dorian', None), (i roman chord, 'phrygian', None)]

        Checking against minor submodes

        >>> degree_7 = CE.create_chord("G")
        >>> CA.analyse_diatonic(degree_7, c_scale)
        [(V roman chord, 'major', None), (V roman chord, 'lydian', None)]
        >>> CA.analyse_diatonic(degree_7, c_scale, incl_submodes=True)
        [(V roman chord, 'minor', 'harmonic'), (V roman chord, 'minor', 'melodic'), (V roman chord, 'major', None), (V roman chord, 'lydian', None)]

        Analysing power/sus chords

        >>> power = CE.create_chord("C5")
        >>> CA.analyse_diatonic(power, c_scale)
        []
        >>> CA.analyse_diatonic(power, c_scale, allow_power_sus=True, default_power_sus="m")
        [(I roman chord, 'major', None), (I roman chord, 'mixolydian', None), (I roman chord, 'lydian', None)]

        """
        self._mode_list.remove(scale.key.mode)
        self._mode_list.insert(0, scale.key.mode)  # shift to the front
        chords = []
        for mode in self._mode_list:
            nscale = self._SE.create_scale(scale.key.root, mode)
            result = self.analyse_diatonic(chord, nscale, incl_submodes,
                                           allow_power_sus, default_power_sus)
            if result:
                chords += result
        return chords

    def analyse_secondary(
        self,
        prev_chord,
        next_chord,
        scale,
        incl_submodes=False,
        allow_power_sus=False,
        default_power_sus="M",
        limit=True,
    ):
        """Analyse if a `Chord` has a secondary function.

        Check if a `Chord` is a secondary chord. By default, only secondary dominant and secondary leading tone chords are checked for.

        Parameters
        ----------
        prev_chord : Chord
            The `Chord` to be analysed for secondary function.
        next_chord : Chord
            The `Chord` to be tonicised.
        scale : Scale
            The `Scale` to check against.
        incl_submodes : boolean, Optional
            Selector to include the minor submodes if `scale` is minor. Default False when optional.
        allow_power_sus : boolean, Optional
            Selector to allow power and sus chords when analysing them. Default False when optional.
        default_power_sus : {"M", "m"}, Optional
            The default quality to convert power and sus chords to if analysing them. "M" is major and "m" is minor.
        limit : boolean, Optional
            Selector to only check for secondary dominant and leading tone chords. Default True when optional.

        Returns
        -------
        str
            The secondary chord notation ``prev_roman``/``next_roman``.

        Examples
        --------
        >>> CE = ChordEditor()
        >>> SE = ScaleEditor()
        >>> CA = ChordAnalyser()

        Diatonic chords

        >>> c_scale = SE.create_scale("C")
        >>> g = CE.create_diatonic(c_scale, 5)
        >>> d = CE.create_chord("D")
        >>> CA.analyse_secondary(d, g, c_scale)
        'V/V'

        Checking against minor submodes

        >>> vii = CE.create_chord("C#dim7")
        >>> degree_2 = CE.create_diatonic(c_scale, 2)
        >>> CA.analyse_secondary(vii, degree_2, c_scale)
        ''
        >>> CA.analyse_secondary(vii, degree_2, c_scale, incl_submodes=True)
        'vii\u00B07/ii'

        Analysing power/sus chords

        >>> power = CE.create_chord("D5")
        >>> g = CE.create_diatonic(c_scale, 5)
        >>> CA.analyse_secondary(power, g, c_scale)
        ''
        >>> CA.analyse_secondary(power, g, c_scale, allow_power_sus=True)
        'V/V'

        Analysing other secondary chords

        >>> e = CE.create_chord("Em")
        >>> CA.analyse_secondary(e, g, c_scale)
        ''
        >>> CA.analyse_secondary(e, g, c_scale, limit=False)
        'vi/V'

        """
        # We only care about chords leading to major/minor/dominant chords
        if next_chord.quality.value not in {"major", "minor", "dominant"}:
            return ""
        next_roman = self._CRC.to_roman(next_chord, scale)
        if next_roman.root in {"i", "I"}:  # ignore tonic next chords
            return ""
        if next_chord.quality.value == "dominant":
            next_scale = self._SE.create_scale(next_chord.root, "major")
        else:
            next_scale = self._SE.create_scale(next_chord.root,
                                               next_chord.quality.value)
        results = self.analyse_diatonic(prev_chord, next_scale, incl_submodes,
                                        allow_power_sus, default_power_sus)
        if results and (not limit or results[0][0].root in {"V", "vii"}):
            return "{}/{}".format(results[0][0], next_roman.root)
        return ""