tandav / musiclib

Set of tools to work with scales, modes, modulations, chord progressions, voice leading, rhythm and more
12 stars 2 forks source link

Generating chord progression voicings with musiclib? #151

Closed ttblum closed 10 months ago

ttblum commented 11 months ago

Hello,

Is it possible for musiclib to generate voicings for chord progressions, for example I - IV - V - I in the key of C?

Does it check for parallel fifths and parallel octaves?

tandav commented 11 months ago

Hi, sure, here how you can do it: this code is written for musiclib==2.2.0

import functools
import itertools
import operator
from typing import NamedTuple

from opseq import OpSeq  # the opseq library is installed when you install musiclib

from musiclib.scale import Scale
from musiclib.noteset import SpecificNoteSet
from musiclib.note import SpecificNote
from musiclib.note import Note
from musiclib.note import config
from musiclib.progression import Progression
from musiclib.voice_leading import checks

scale = Scale.from_name('C', 'major')
noterange = SpecificNoteSet.from_noterange(SpecificNote('C', 2), SpecificNote('C', 5), noteset=scale.noteset)

class SNSR(NamedTuple):
    """SpecificNoteSet with root Note"""
    sns: SpecificNoteSet
    root: Note

def possible_chords(noterange: SpecificNoteSet, scale: Scale) -> tuple[SpecificNoteSet]:
    scale_triads = scale.nths(config.nths['triads'])
    notes_to_triad_root = {s.notes: s.root for s in scale_triads}

    def _notes_to_chord(notes: frozenset[SpecificNote]):
        abstract = frozenset({n.abstract for n in notes})
        if root := notes_to_triad_root.get(abstract):
            sns = SpecificNoteSet(notes)

            if sns.noteset.name == 'dim':
                return

            snsr = SNSR(sns=sns, root=root)
            yield snsr

    it = itertools.combinations(noterange, 4)  # 4 voice chords
    it = itertools.chain.from_iterable(_notes_to_chord(frozenset(x)) for x in it)
    it = [snsr for snsr in set(it) if not checks.is_large_spacing(snsr.sns, 7)]
    return it

@functools.cache
def no_bad_checks(a_: SNSR, b_: SNSR):
    a = a_.sns
    b = b_.sns
    return all((
        not checks.is_parallel_interval(a, b, 0),
        not checks.is_parallel_interval(a, b, 7),
        not checks.is_hidden_parallel(a, b, 0),
        not checks.is_hidden_parallel(a, b, 7),
        not checks.is_voice_crossing(a, b),
        not checks.is_large_leaps(a, b, 7),
    ))

def unique(it, f=None):
    key = f or (lambda x: x)
    seen = set()
    for item in it:
        k = key(item)
        if k in seen:
            continue
        seen.add(k)
        yield item

def make_progressions(noterange: SpecificNoteSet, n, scale):
    it = OpSeq(
        n,
        options=possible_chords(noterange, scale=scale),
        curr_prev_constraint={-1: no_bad_checks},
        i_constraints={
            0: lambda chord: chord.root == Note('C'), # I
            1: lambda chord: chord.root == Note('F'), # IV
            2: lambda chord: chord.root == Note('G'), # V
            3: lambda chord: chord.root == Note('C'), # I
        },
    )
    it = (Progression(tuple(snsr.sns for snsr in snsrs)) for snsrs in it)
    it = unique(it, f=operator.methodcaller('transpose_unique_key'))
    it = sorted(it, key=operator.attrgetter('distance'))
    return it

progressions = make_progressions(noterange, n=4, scale=scale)
print(progressions)

# [
#     Progression('G3_C4_E4_G4', 'A3_C4_F4_C5', 'G3_D4_G4_B4', 'G3_C4_E4_G4'),
#     Progression('C3_G3_C4_E4', 'C3_F3_A3_C4', 'B2_D3_G3_D4', 'C3_G3_C4_E4'),
#     Progression('E3_G3_C4_G4', 'F3_C4_F4_A4', 'G3_B3_D4_G4', 'E3_G3_C4_G4'),
#     Progression('G3_C4_E4_G4', 'F3_C4_F4_A4', 'G3_B3_D4_G4', 'E3_G3_C4_G4'),
#     Progression('C3_G3_C4_E4', 'C3_F3_A3_C4', 'B2_D3_G3_D4', 'C3_E3_G3_C4'),
#     Progression('C3_E3_G3_C4', 'F2_C3_F3_A3', 'B2_D3_G3_D4', 'C3_E3_G3_C4'),
#     Progression('G2_C3_E3_G3', 'F2_C3_F3_A3', 'B2_D3_G3_D4', 'C3_E3_G3_C4'),
#     Progression('C4_E4_G4_C5', 'A3_C4_F4_C5', 'G3_D4_G4_B4', 'G3_C4_E4_G4'),
#     Progression('E3_G3_C4_G4', 'F3_C4_F4_A4', 'G3_B3_D4_G4', 'C3_G3_C4_E4'),
#     Progression('G3_C4_E4_G4', 'F3_C4_F4_A4', 'G3_B3_D4_G4', 'C3_G3_C4_E4'),
#     Progression('C3_E3_G3_C4', 'F2_C3_F3_A3', 'B2_D3_G3_D4', 'C3_G3_C4_E4'),
#     Progression('C3_G3_C4_E4', 'C3_F3_A3_C4', 'G2_D3_G3_B3', 'G2_C3_E3_G3'),
#     Progression('G2_C3_E3_G3', 'F2_C3_F3_A3', 'B2_D3_G3_D4', 'C3_G3_C4_E4'),
#     Progression('C4_E4_G4_C5', 'F3_C4_F4_A4', 'G3_B3_D4_G4', 'E3_G3_C4_G4'),
#     Progression('E2_G2_C3_G3', 'F2_C3_F3_A3', 'B2_D3_G3_D4', 'C3_E3_G3_C4'),
#     Progression('C4_E4_G4_C5', 'F3_C4_F4_A4', 'G3_B3_D4_G4', 'C3_G3_C4_E4'),
#     Progression('E2_G2_C3_G3', 'F2_C3_F3_A3', 'B2_D3_G3_D4', 'C3_G3_C4_E4'),
# ]