Source code for scamp_extensions.pitch.chord_builder

"""
Module containing functions for voicing a chord: given a set of pitch classes and a range to place them in,
these search the possible voicings and return whichever one best fits the preferences given, such as being
compactly spaced or centered in the range.
"""

#  ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++  #
#  This file is part of SCAMP (Suite for Computer-Assisted Music in Python)                      #
#  Copyright © 2020 Marc Evanstein <marc@marcevanstein.com>.                                     #
#                                                                                                #
#  This program is free software: you can redistribute it and/or modify it under the terms of    #
#  the GNU General Public License as published by the Free Software Foundation, either version   #
#  3 of the License, or (at your option) any later version.                                      #
#                                                                                                #
#  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;     #
#  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     #
#  See the GNU General Public License for more details.                                          #
#                                                                                                #
#  You should have received a copy of the GNU General Public License along with this program.    #
#  If not, see <http://www.gnu.org/licenses/>.                                                   #
#  ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++  #

import itertools
import numpy as np
from statistics import mean


#: Functions that score a voicing on some quality; the names are those accepted as spacing and range
#: preferences by :func:`chords_from_pitch_classes`. Each takes the pitches of a voicing and the
#: (min, max) range they were drawn from, and returns a score, higher being a better fit.
measurement_functions = {
    "uniqueness": lambda pitch_collection, _: len(set(p % 12 for p in pitch_collection)),
    "wide": lambda pitch_collection, _: max(pitch_collection) - min(pitch_collection),
    "compact": lambda pitch_collection, _: -(max(pitch_collection) - min(pitch_collection)),
    "even": lambda pitch_collection, _: -np.std(np.diff(sorted(pitch_collection))) if len(pitch_collection) > 2 else 1,
    "uneven": lambda pitch_collection, _: np.std(np.diff(sorted(pitch_collection))) if len(pitch_collection) > 2 else 1,
    "high": lambda pitch_collection, _: mean(pitch_collection),
    "low": lambda pitch_collection, _: mean(pitch_collection),
    "mid": lambda pitch_collection, pitch_range: -abs(mean(pitch_collection) - mean(pitch_range)),
}


[docs] def chords_from_pitch_classes(pcs, min_pitch, max_pitch, num_notes, prefer_unique_pcs=True, spacing_and_range_prefs=("compact", "mid", "even"), how_many=1): """ Find the voicings of the given pitch classes that best fit the given preferences. :param pcs: the pitch classes (integers from 0 to 11) to voice :param min_pitch: lowest MIDI pitch the voicing may use :param max_pitch: highest MIDI pitch the voicing may use :param num_notes: how many notes the chord should have; capped at the number of pitches available :param prefer_unique_pcs: if True, prefer voicings that cover as many distinct pitch classes as possible, ahead of any of the other preferences :param spacing_and_range_prefs: the qualities to prefer in a voicing, in decreasing order of priority. These are keys of :data:`measurement_functions`: "wide"/"compact" (spread out or close together), "even"/"uneven" (equally or unequally spaced), and "high"/"low"/"mid" (placement within the range). :param how_many: how many voicings to return :return: a list of the best `how_many` voicings, each a tuple of MIDI pitches, best last """ available_pitches = get_pcs_instances_in_range(pcs, min_pitch, max_pitch) pref_functions = ("uniqueness", ) + spacing_and_range_prefs if prefer_unique_pcs else spacing_and_range_prefs combos = sorted(itertools.combinations(available_pitches, min(num_notes, len(available_pitches))), key=lambda pitches: tuple(measurement_functions[func_name](pitches, (min_pitch, max_pitch)) for func_name in pref_functions)) return combos[-how_many:]
[docs] def chord_from_pitch_classes(pcs, min_pitch, max_pitch, num_notes, prefer_unique_pcs=True, spacing_and_range_prefs=("compact", "mid", "even")): """ Find the single voicing of the given pitch classes that best fits the given preferences. See :func:`chords_from_pitch_classes`, of which this is the single-result version. :param pcs: the pitch classes (integers from 0 to 11) to voice :param min_pitch: lowest MIDI pitch the voicing may use :param max_pitch: highest MIDI pitch the voicing may use :param num_notes: how many notes the chord should have; capped at the number of pitches available :param prefer_unique_pcs: if True, prefer voicings covering as many distinct pitch classes as possible :param spacing_and_range_prefs: the qualities to prefer, in decreasing order of priority :return: the best voicing, as a tuple of MIDI pitches """ return chords_from_pitch_classes(pcs, min_pitch, max_pitch, num_notes, prefer_unique_pcs, spacing_and_range_prefs, how_many=1)[0]
[docs] def get_pcs_instances_in_range(pcs, min_pitch, max_pitch): """ Every pitch within the given range belonging to one of the given pitch classes. :param pcs: the pitch classes (integers from 0 to 11) in question :param min_pitch: lowest MIDI pitch to consider (inclusive) :param max_pitch: highest MIDI pitch to consider (exclusive) """ return [pitch for pitch in range(min_pitch, max_pitch) if pitch % 12 in pcs]