Interval Sequences

Interval sequences provide an abstract way to represent musical structures by storing the relative distances between notes, rather than their absolute pitches. This allows for the definition of abstract scale and chord types - for example, representing the major scale as such, rather than as specific instances like C major or E major.

Creating sequences

One method for generating an interval sequence is to begin with a scale and call its to_interval_seq() method. Xenharmlib will then calculate the intervals between successive notes and return the resulting sequence:

from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation

edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
c_major_scale = n_edo12.pc_scale(
    ['C', 'D', 'E', 'F', 'G', 'A', 'B']
)
major_seq = c_major_scale.to_interval_seq()
print(major_seq)
UpDownNoteIntervalSeq([M2, M2, m2, M2, M2, M2], 12-EDO)

As you can see you obtained a sequence of major and minor seconds in the order that they occur in the scale. The resulting interval sequence is only one possible representation of the major scale. Another representation that is more common in textbooks is derived from a form of the scale where the series is bookended by two equivalent notes. This scale form can be obtained by the method plusone_normalized():

alt_c_major = c_major_scale.plusone_normalized()
alt_major_seq = alt_c_major.to_interval_seq()

print(alt_c_major)
print(alt_major_seq)
UpDownNoteScale([C0, D0, E0, F0, G0, A0, B0, C1], 12-EDO)
UpDownNoteIntervalSeq([M2, M2, m2, M2, M2, M2, m2], 12-EDO)

Both methods to define the major scale have their valid usecases. It is upon you to decide which form fits your task.

Alternatively you can define an interval sequence explicitely by calling the builder method interval_seq() of the origin context that takes an iterable of intervals as its argument.

m2 = n_edo12.shorthand_interval('m', 2)
M2 = n_edo12.shorthand_interval('M', 2)

minor_seq = n_edo12.interval_seq(
    [M2, m2, M2, M2, m2, M2]
)
print(minor_seq)
UpDownNoteIntervalSeq([M2, m2, M2, M2, m2, M2], 12-EDO)

Interval sequences can also be defined without choosing a notation by calling interval_seq() directly on the tuning layer with an iterable of pitch intervals as parameter:

major_seq = edo12.interval_seq(
    [
        edo12.diff_interval(2),
        edo12.diff_interval(1),
        edo12.diff_interval(2),
        edo12.diff_interval(2),
        edo12.diff_interval(1),
        edo12.diff_interval(2),
    ]
)
print(major_seq)
EDOPitchIntervalSeq([2, 1, 2, 2, 1, 2], 12-EDO)

The same result can also be achieved more concisely through the diff_interval_seq() method:

major_seq = edo12.diff_interval_seq([2, 1, 2, 2, 1, 2])
print(major_seq)
EDOPitchIntervalSeq([2, 1, 2, 2, 1, 2], 12-EDO)

Templating and categorization

The most obvious usage of interval sequences is templating. You can for example define minor and major chords as interval sequences and then derive specific chords from it:

m3 = n_edo12.shorthand_interval('m', 3)
M3 = n_edo12.shorthand_interval('M', 3)

minor = n_edo12.interval_seq([M3, m3])
major = n_edo12.interval_seq([m3, M3])

D = n_edo12.note('D', 0).scale(major)
G = n_edo12.note('G', 0).scale(major)
Am = n_edo12.note('A', 0).scale(minor)
Em = n_edo12.note('E', 0).scale(minor)

Vice versa, interval sequences can also be used to categorize scale objects. In the next snippet we devise three functions that generate abstract definitions of the three types of minor scales for a given context and an additional identifier function that takes a scale and returns the type of minor scale.

def natural_minor(context):
    return context.pc_scale(
        ['A', 'B', 'C', 'D', 'E', 'F', 'G']
    ).to_interval_seq()

def harmonic_minor(context):
    return context.pc_scale(
        ['A', 'B', 'C', 'D', 'E', 'F', 'G#']
    ).to_interval_seq()

def melodic_minor(context):
    return context.pc_scale(
        ['A', 'B', 'C', 'D', 'E', 'F#', 'G#']
    ).to_interval_seq()

def which_minor(scale):
    seq = scale.to_interval_seq()
    context = scale.origin_context
    if seq == natural_minor(context): return 'natural'
    if seq == harmonic_minor(context): return 'harmonic'
    if seq == melodic_minor(context): return 'melodic'
    raise ValueError('Scale is not minor')

The generic approach allows us to reuse the functions for various tunings wrapped with UpDownNotation:

# 12 EDO
edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)
what_am_i_12 = n_edo12.pc_scale(
    ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb']
)
print(which_minor(what_am_i_12))

# 31 EDO
edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)
what_am_i_31 = n_edo31.pc_scale(
    ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'B']
)
print(which_minor(what_am_i_31))
natural
harmonic

Pattern matching

Interval sequences can be used to match interval patterns in scales with the find_iseq() function from the periodic extension package.

The following snippet finds all minor triads that can be derived from the C major scale:

from xenharmlib import periodic

c_major_scale = n_edo12.pc_scale(
    ['C', 'D', 'E', 'F', 'G', 'A', 'B']
)
minor_chord = n_edo12.pc_scale(['C', 'Eb', 'G']).to_interval_seq()

masks = periodic.find_iseq(c_major_scale, minor_chord)

for mask in masks:
    print(periodic.partial(c_major_scale, mask))
UpDownNoteScale([D0, F0, A0], 12-EDO)
UpDownNoteScale([E0, G0, B0], 12-EDO)
UpDownNoteScale([A0, C1, E1], 12-EDO)