Pitch/Note Sequences

A sequence is a structure representing a succession of pitches or notes. Sequences don’t hold any rhythmic information. They are conceptualized as a data structure representing purely the “harmonic flow” of a melody.

from xenharmlib import WesternNotation

western = WesternNotation()

# harmonic flow of the first two measures from the vocals of
# Schubert's Wiegenlied ("Schlafe, Schlafe, holder, süßer Knabe"),
# rearranged into the key of F major
schlafe_schlafe = western.seq(
    western.note(pcs, 4) for pcs in
    ['A', 'C', 'G', 'A', 'Bb', 'A', 'A', 'G', 'F', 'E', 'F', 'G', 'C']
)

In the context of serial music, the sequence type is used to analyze segments. Sequences allow inspection of pitch indices and pitch class indices of their content:

print(schlafe_schlafe.pitch_indices)
print(schlafe_schlafe.pc_indices)
[57, 48, 55, 57, 58, 57, 57, 55, 53, 52, 53, 55, 48]
[9, 0, 7, 9, 10, 9, 9, 7, 5, 4, 5, 7, 0]

Sequences act similarly to standard Python list or tuple types in terms of their interface, but implement a couple of additional methods specific to music theory. In contrast to scales, there is no restriction with regard to uniqueness or order, so sequences can also be used to analyze scale-like data for which the scale primitive is too restricted, e.g., for “descending scales”, in which pitches and notes are not in ascending, but descending order:

from xenharmlib import WesternNotation

western = WesternNotation()
scale = western.pc_scale('CDEFGABC', 4)
descending = western.seq(scale).retrograde()

print(descending)
WesternNoteSeq([C5, B4, A4, G4, F4, E4, D4, C4])

Deriving sequences from pitches/notes

Sequences can easily be constructed from Lists (or any Iterable) of notes or pitches. This method of construction is preferable when writing utilities because it works the same on all origin contexts:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

C0 = edo31.pitch(0)
D0 = edo31.pitch(5)
E0 = edo31.pitch(10)

seq = edo31.seq([C0, D0, E0, C0, E0])
print(seq)
EDOPitchSeq([0, 5, 10, 0, 10], 31-EDO)

Element-wise Construction

Sequences can be constructed element-wise. In line with xenharmlib’s immutable object design, this is not done with an append method like in standard Python, but with a method that returns a sequence with an additional element:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

C0 = edo31.pitch(0)
D0 = edo31.pitch(5)
E0 = edo31.pitch(10)

# start with an empty sequence
seq = edo31.seq()

for e in [C0, D0, E0, C0, E0]:
    seq = seq.with_element(e)
    print(seq)
EDOPitchSeq([0], 31-EDO)
EDOPitchSeq([0, 5], 31-EDO)
EDOPitchSeq([0, 5, 10], 31-EDO)
EDOPitchSeq([0, 5, 10, 0], 31-EDO)
EDOPitchSeq([0, 5, 10, 0, 10], 31-EDO)

Given an additional positional parameter, new elements can appear at a place of the user’s free choosing in the new sequence. The parameter works exactly like the equivalent parameter in the insert method of python’s builtin List type, meaning the given value will be the index in which the new element will be found in the new sequence:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

C0 = edo31.pitch(0)
D0 = edo31.pitch(5)
E0 = edo31.pitch(10)

# start with an empty sequence
seq = edo31.seq()

for e in [C0, D0, E0, C0, E0]:
    seq = seq.with_element(e, 0)
    print(seq)
EDOPitchSeq([0], 31-EDO)
EDOPitchSeq([5, 0], 31-EDO)
EDOPitchSeq([10, 5, 0], 31-EDO)
EDOPitchSeq([0, 10, 5, 0], 31-EDO)
EDOPitchSeq([10, 0, 10, 5, 0], 31-EDO)

Index-based Construction

Sequences can also be conveniently constructed by providing a list of indices, which - depending on origin context - can be integers or lattice points. This type of construction also works on the notation layer by employing the notation’s enharmonic strategy.

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([0, 5, 10, 0, 5])
print(seq)
EDOPitchSeq([0, 5, 10, 0, 5], 31-EDO)

Index-based construction can be especially useful for dealing with segments in 12-tone music. The following snippet, e.g., creates a random 12-tone segment and prints the result as a note sequence:

from random import shuffle
from xenharmlib import WesternNotation

western = WesternNotation()
pc_indices = list(range(0, 12))
shuffle(pc_indices)

segment = western.index_seq(pc_indices)

print(segment)

Origin contexts built on lattice point indexing also allow construction from an iterable of tuples with vec_seq() as an alternative to the more verbose above method, which expects a full LatticePoint object.

from xenharmlib import PrimeLimitTuning
limit3 = PrimeLimitTuning(3)

major_seq = limit3.vec_seq(
    [(-3, 2), (-3, 2), (8, -5), (-3, 2), (-3, 2), (-3, 2)]
)
print(major_seq)
PrimeLimitPitchSeq([9/8, 9/8, 256/243, 9/8, 9/8, 9/8], 3-Limit)

Construction Based on Closest Frequency

Sequences can be constructed by finding pitches or notes closest to an iterable of frequencies. This can be useful if you have extracted data on fundamental frequencies of a melody from an audio sample and want to know the notes played in the recording. The Essentia library, e.g., allows raw frequency extraction from melodies; however, its note conversion feature only supports 12 equal temperament. Augmented with xenharmlib, you can extend your analysis to non-Western harmonic systems (e.g., 53-EDO Turkish Makam)

from xenharmlib import EDOTuning
from xenharmlib import Frequency

edo31 = EDOTuning(31)

# melody frequencies extracted by another library
frequencies = [
    Frequency(f) for f in [230, 410, 230, 670, 600]
]

seq = edo31.closest_seq(frequencies)
print(seq)
EDOPitchSeq([118, 144, 118, 166, 161], 31-EDO)

Relation to Interval Sequences

Sequences and Interval Sequences are closely related. While a sequence stores the information “what succession of sounds should be played”, an interval sequence stores the information “given an arbitrary start point, what relative movements should be made”. Sequences can be transformed into interval sequences by the to_interval_seq() method:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

C0 = edo31.pitch(0)
D0 = edo31.pitch(5)
E0 = edo31.pitch(10)

seq = edo31.seq([C0, D0, E0, C0, E0])
iseq = seq.to_interval_seq()
print(iseq)
EDOPitchIntervalSeq([5, 5, -10, 10], 31-EDO)

Vice versa, interval sequences can be transformed into a pitch/note sequence by providing a root note:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

iseq = edo31.diff_interval_seq([5, 5, 2, 5, 2])
seq = edo31.pitch(18).seq(iseq)
print(seq)
EDOPitchSeq([18, 23, 28, 30, 35, 37], 31-EDO)

Relation to Interval Fans

While sequences define a melody flow in concrete terms (for example: The melody flow of “Mary Had a Little Lamb” in C major), interval fans abstract away the tonic center (the melodic flow of “Mary Had a Little Lamb” in some key). This abstraction can be calculated using the to_interval_fan() method, which expects the pitch or note defining the tonic of the key as a parameter:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

C0 = edo31.pitch(0)
D0 = edo31.pitch(5)
E0 = edo31.pitch(10)

# melodic flow of "Mary Had a Little Lamb"
seq = edo31.seq([E0, D0, C0, D0, E0, E0, E0])

# calculate interval fan by using the information that
# it is written in reference to the tonal center of C
ifan = seq.to_interval_fan(C0)
print(ifan)
EDOPitchIntervalFan([10, 5, 0, 5, 10, 10, 10], 31-EDO)

The reverse is also possible: Start with an interval fan defining an abstract sequence in relation to an unspecified tonic, add a tonic and receive the melodic flow:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

# abstract form of "mary had a little lamb"
# relating to an unspecified tonic note/pitch
ifan = edo31.diff_interval_fan([10, 5, 0, 5, 10, 10, 10])

# make an instance with the tonic G0
in_g = edo31.pitch(18).seq(ifan)

# make an instance with the tonic F0
in_f = edo31.pitch(13).seq(ifan)

print(in_g)
print(in_f)
EDOPitchSeq([28, 23, 18, 23, 28, 28, 28], 31-EDO)
EDOPitchSeq([23, 18, 13, 18, 23, 23, 23], 31-EDO)

Identity and Equivalency

Two sequences are considered identical if the series of frequencies of one sequence is identical to the series of frequencies of another. This means that sequences can be compared across different origin contexts:

from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation
from xenharmlib import WesternNotation

edo24 = EDOTuning(24)
n_edo24 = UpDownNotation(edo24)
western = WesternNotation()

Cm_1 = edo24.index_seq([0, 8, 14, 8, 8, 0, 14])
Cm_2 = n_edo24.seq([n_edo24.note(pcs, 0) for pcs in 'CEGEECG'])
Cm_3 = western.seq([western.note(pcs, 0) for pcs in 'CEGEECG'])

print(Cm_1 == Cm_2 == Cm_3)
True

Keep in mind that even if two sequences might have the same symbolic string representation in a notation, they are not necessarily equal. A G4 in 31-EDO has, for example, a different frequency than a G4 in 12-EDO:

from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation

edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)

edo31 = EDOTuning(31)
n_edo31 = UpDownNotation(edo31)

Cm_1 = n_edo12.seq([n_edo12.note(pcs, 4) for pcs in 'CEGEECG'])
Cm_2 = n_edo31.seq([n_edo31.note(pcs, 4) for pcs in 'CEGEECG'])
print(Cm_1)
print(Cm_2)

print(Cm_1 == Cm_2)
UpDownNoteSeq([C4, E4, G4, E4, E4, C4, G4], 12-EDO)
UpDownNoteSeq([C4, E4, G4, E4, E4, C4, G4], 31-EDO)
False

Identity transfers to object hashes, so two sequences that are equal are also considered the same object when put into a Python set:

from xenharmlib import EDOTuning
from xenharmlib import WesternNotation

edo24 = EDOTuning(24)
western = WesternNotation()

Cm_1 = edo24.index_seq([0, 8, 14, 8, 8, 0, 14])
Cm_2 = western.seq([western.note(pcs, 0) for pcs in 'CEGEECG'])

print({Cm_1, Cm_2})
{EDOPitchSeq([0, 8, 14, 8, 8, 0, 14], 24-EDO)}

Sequences can also be tested for equivalency. This means that not frequencies are compared, but frequencies normalized to the first base interval, making e.g. “C4 and C5” the same. Like equality, this can also be tested across origin contexts as long as they have the same equivalency interval ratio:

from xenharmlib import EDOTuning
from xenharmlib import UpDownNotation

edo12 = EDOTuning(12)
n_edo12 = UpDownNotation(edo12)

edo24 = EDOTuning(24)
n_edo24 = UpDownNotation(edo24)

Cm_1 = n_edo12.seq([n_edo12.note(pcs, 4) for pcs in 'CEGEECG'])
Cm_2 = n_edo24.seq([n_edo24.note(pcs, 3) for pcs in 'CEGEECG'])

print(Cm_1)
print(Cm_2)
print(Cm_1.is_equivalent(Cm_2))
UpDownNoteSeq([C4, E4, G4, E4, E4, C4, G4], 12-EDO)
UpDownNoteSeq([C3, E3, G3, E3, E3, C3, G3], 24-EDO)
True

The base interval difference does not need to be uniform between the two sequences for them to be considered equivalent, as you can see from the following examples:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq_a = edo31.index_seq([0, 10, 49, 10, 0])
seq_b = edo31.index_seq([31, 10, 49, 41, 62])
seq_c = edo31.index_seq([10, 0, 49, 41, 62])

print(seq_a.is_equivalent(seq_b))
print(seq_a.is_equivalent(seq_c))
True
False

Equality/Identity also translates to Python’s built-in set type. If two sequences are considered equal, combining them in a Python set will result in a one-element set:

from xenharmlib import EDOTuning
from xenharmlib import WesternNotation

edo24 = EDOTuning(24)
western = WesternNotation()

seq_a = edo24.index_seq([4, 10, 4, 18])
seq_b = western.index_seq([2, 5, 2, 9])

# since the second element is equal to the first
# only the first element will be added to the set
print({seq_a, seq_b})
{EDOPitchSeq([4, 10, 4, 18], 24-EDO)}

Iteration

Like other sequence types, pitch/note sequences support iteration:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([10, 8, 7, 10])

for pitch in seq:
    print('-->', pitch)
--> EDOPitch(10, 31-EDO)
--> EDOPitch(8, 31-EDO)
--> EDOPitch(7, 31-EDO)
--> EDOPitch(10, 31-EDO)

Element Containment

If you want to know if a specific note or pitch is contained inside of a sequence, you can use the in operator:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([10, 8, 7, 10])
pitch_a = edo31.pitch(7)
pitch_b = edo31.pitch(9)

print(pitch_a in seq)
print(pitch_b in seq)
True
False

Item Retrieval and Slicing

Single elements from the sequence can be obtained by their index. You can also use slices (including step size) to extract portions of a sequence:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([10, 8, 7, 10])

print(seq[2])
print(seq[1:3])
print(seq[::2])
EDOPitch(7, 31-EDO)
EDOPitchSeq([8, 7], 31-EDO)
EDOPitchSeq([10, 7], 31-EDO)

Counting

For statistical analysis, xenharmlib can calculate the number of times a specific pitch or note occurs in a sequence:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([10, 8, 7, 10])
pitch = edo31.pitch(7)

print(seq.count(pitch))
1

Index Masks and Partial Sequences

You can extract (continuous or non-continuous) parts from a sequence with the partial() method that expects an index mask expression, i.e., a tuple with indices pointing to sequence elements that should be extracted.

The mask (1, 3, 4), for example, extracts the second, fourth, and fifth element of the sequence (like with item retrieval indices in a mask expression start with 0):

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([10, 8, 10, 9, 10, 8])
print(seq.partial((1, 3, 4)))
EDOPitchSeq([8, 9, 10], 31-EDO)

Mask expressions targeted at longer continuous spans inside the sequence can be written as a “shortform” with the ellipsis symbol (...):

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([10, 8, 10, 9, 10, 8])

# an ellipsis as a prefix matches all elements from
# the start of the sequence until and including (!)
# the first mask index
print(seq.partial((..., 3, 4)))

# an ellipsis between two indices matches the two indices
# in the sequence and all elements between them
print(seq.partial((1, ..., 4)))

# an ellipsis at the end matches all remaining indices
# in the sequence after the last mask index
print(seq.partial((0, 3, ...)))
EDOPitchSeq([10, 8, 10, 9, 10], 31-EDO)
EDOPitchSeq([8, 10, 9, 10], 31-EDO)
EDOPitchSeq([10, 9, 10, 8], 31-EDO)

Selection can also be inverted with the partial_not() method that returns all elements not covered by the mask expression:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([10, 8, 10, 9, 10, 8])
print(seq.partial_not((1, 3, 4)))
EDOPitchSeq([10, 10, 8], 31-EDO)

To get both the substructure indicated by the mask and its complement as a tuple, you can use the partition() method:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([10, 8, 10, 9, 10, 8])
a, b = seq.partition((1, 3, 4))
print(a)
print(b)
EDOPitchSeq([8, 9, 10], 31-EDO)
EDOPitchSeq([10, 10, 8], 31-EDO)

Concatenation and Repetition

Like Python’s built-in list and tuple types, xenharmlib’s sequence primitive allows concatenation of two objects and multiplication with scalars with the meaning “repeat x times”:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq_a = edo31.index_seq([0, 5, 10, 5])
seq_b = edo31.index_seq([10, 5, 5, 10])
print(seq_a + seq_b)
print(3 * seq_a)
EDOPitchSeq([0, 5, 10, 5, 10, 5, 5, 10], 31-EDO)
EDOPitchSeq([0, 5, 10, 5, 0, 5, 10, 5, 0, 5, 10, 5], 31-EDO)

Transposition

Sequences can be transposed with the transpose() method. This is typically done by providing an interval object of the same origin context:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([3, 10, 19, 10])
interval = edo31.diff_interval(8)
print(seq.transpose(interval))
EDOPitchSeq([11, 18, 27, 18], 31-EDO)

The transpose() method also accepts a pitch difference. A pitch difference is an integer or lattice point that defines the numeric distance between two frequency representations. In the case of notations with enharmonic ambiguity, xenharmlib makes a guess which note should be picked.

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([3, 10, 19, 10])
print(seq.transpose(8))
EDOPitchSeq([11, 18, 27, 18], 31-EDO)

Retrograde and Inversion

Sequences provide the retrograde() and inversion() methods, which return a horizontally mirrored and vertically mirrored sequence, respectively: Retrograde reads the sequence from end-to-start while Inversion flips the direction of every interval in the sequence:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq = edo31.index_seq([0, 10, 49, 13, 18])
print(seq.retrograde())
print(seq.inversion())
EDOPitchSeq([18, 13, 49, 10, 0], 31-EDO)
EDOPitchSeq([0, -10, -49, -13, -18], 31-EDO)

Subsequence Containment

If you have two sequences, you can test whether one sequence is a subsequence of the other with the is_subseq() method:

from xenharmlib import EDOTuning
edo31 = EDOTuning(31)

seq_a = edo31.index_seq([9, 9, 10])
seq_b = edo31.index_seq([9, 7, 10])
seq_c = edo31.index_seq([10, 8, 7, 10, 9, 9, 10, 8])

print(seq_a.is_subseq(seq_c))
print(seq_b.is_subseq(seq_c))
True
False