A Collection of of Harmonic Constraint Satisfaction Problems

back

Forming Music which Expresses a Harmony
Expressing a Single Chord
Monophony with Additional Pattern Constraints
Allowing for Non-Harmonic Tones
Constraining the Rhythmical Structure
Multiple Voices
Expressing a Fixed Harmonic Progressions: a Simple Cadence
Constraining the Harmony
Defining the Music Representation
Defining a Simple Theory of Harmony

This section presents several harmonic constraint satisfaction problems. Strasheela provides a harmony model which makes it relatively easy to define harmonic CSPs, because this model predefines the required building blocks. This section demonstrates this model with examples. These examples are often edited versions of each other — in order to encourage you to create your own by further editing them ;-)

Forming Music which Expresses a Harmony

The examples in this section demonstrate the use of Strasheela's harmony model for constraining note pitches in a score to follow pre-composed harmonic progressions. Additional constraints further shape the music, for example, constrain melody pitches to follow a specific contour.

Expressing a Single Chord

The first example constrains all pitch classes of a sequence of notes to members of the chord D major. The actual pitches are chosen randomly by the search. In the music notation of this and all following examples, the lowest staff is not sounding but shows analytical information on the harmony (the chord root note and a textual description).

Setting the Harmony Database

Strasheela's harmony model makes use of user-defined harmonic material such as chords, scales and intervals. The user specifies this material by filling 'databases' provided by the model. The following code fragment defines a chord database consisting of a single major chord. A chord database entry usually contains the features pitchClasses and roots. The example specifies the chord pitch classes [0 4 7] (denoting c, e, and g), the only possible root of this chord 0 (i.e. c), and a short description at the comment feature. Other chords may have multiple root pitch class candidates. For example, the diminished seventh chord [0 3 6 9] is very ambiguous and allows for the following four possible root pitch classes [2 5 8 11], depending on the interpretation of the chord (Schoenberg (1911) calls such chords vagrant harmonies).

  MajorChordSpec = chord(comment:'major'
                         pitchClasses:[0 4 7]
                         roots:[0])
  {HS.db.setDB unit(chordDB:chords(MajorChordSpec))}

A chord database consists of untransposed chord types. For example, a database may consist in the three chord types major chord, minor chord, and major seventh chord. The database chords can then be transposed in the CSP. For example, an instance of the major chord in the database can be transposed by 2 such that it becomes a D major chord (as in the music notation above).

CSP Definition

Following is the full implementation of the example above. The definition consists mainly of a specification of the music representation: a sequence of N notes with undetermined pitches running in parallel to a single chord (the notion of nested score objects and the function LUtils.collectN were introduced before). The note sequence and the chord both start at time 0 (derived from the start time of their surrounding simultaneous container), and end at the same time (the end time of both objects is unified by setting it to the same variable EndTime). The chord is determined to the first chord in the database above (index 1) and is transposed by 2, that is the chord is a D major chord with the pitch classes {2, 6, 9}.

  proc {MyScript HarmonisedScore}
     N=12 EndTime
  in
     HarmonisedScore = {Score.makeScore
                        sim(items:[seq(items:{LUtils.collectN N
                                              fun {$}
                                                 note(duration:4
                                                      pitch:{FD.int 60#72}
                                                      amplitude:64)
                                              end}
                                       endTime:EndTime)
                                   chord(endTime:EndTime
                                         index:1
                                         transposition:2)]
                            startTime:0
                            timeUnit:beats(4))
                        Aux.myCreators}
  end

The actual 'magic' of the example — the established relation between the notes and the chord is hidded 'backstage' for simplicity: the note creator function — which is part of Aux.myCreators — constrains the pitch of each note it creates to express the harmony of the simultaneous chord object. This technique will be explained later. The source of this first example is extensively documented.

Pitch Representation

This example (as well as the following examples) make use of an extended note object provided by the harmony model (see HS.score.note). This extended note object represents its pitch by three interdependent constrained variables: the note's pitch (in the following examples measured in MIDI keynumbers, 60 is middle c), pitchClass (0 denotes c), and octave. Strasheela's harmony model also introduces variables for a (scale) degree and accidental, but these variables are not used in the present examples for simplicity. Consequently, the music notation export has too little information to distinguish between enharmonic pitches (e.g. c-sharp and d-flat), and hence only sharp accidentals are used in the notation. Likewise, the notation export does not specify key signatures.

Monophony with Additional Pattern Constraints

The following examples slightly variate the example before, by applying a few additional rules. These rules restrict the melody to form specific melodic patterns — while still expressing the underlying harmony. The next example constrains the melody to form a continuously raising pitch succession. In addition, the melody must start with the chord root. Besides, this example also changes the chord in the database and sets it to a minor chord with an added sixth (pitch classes {0, 3, 7, 9}).

source

The example differs only slightly from the example above. The added bits are marked by comments. The feature handle (supported by the textual representation of every Strasheela score object) was already introduced in the florid counterpoint example: this feature binds a variable (here MyNoteSeq and MyChord) to the corresponding score object instance. Two rules are applied to these two variables.

proc {MyScript HarmonisedScore}
   N=8
   EndTime MyNoteSeq MyChord
in
   HarmonisedScore = {Score.makeScore
                      sim(items:[seq(handle:MyNoteSeq  % bind seq object to MyNoteSeq
                                     items:{LUtils.collectN N
                                            fun {$}
                                               note(duration:4
                                                    pitch:{FD.int 48#72}
                                                    amplitude:64)
                                            end}
                                     endTime:EndTime)
                                 chord(handle:MyChord
                                       endTime:EndTime
                                       transposition:2)]
                          startTime:0
                          timeUnit:beats(4))
                      Aux.myCreators}
   %% pitch class of first note is chord root
   {{Nth {MyNoteSeq getItems($)} 1} getPitchClass($)} = {MyChord getRoot($)}
   %% constrain pitches of the note in NoteSeq to raise continuously
   {Pattern.increasing {MyNoteSeq mapItems($ getPitch)}}
end

The additional rules (the last two lines in the example) are explained in the following. Firstly, how is the pitch class of the first note set to the chord root? The first melody pitch is accessed with the following expression (the method getItems returns all score objects directly contained a sequential container, a subclass of container)

{Nth {MyNoteSeq getItems($)} 1}

The pitch class of this note is then accessed (with the method getPitchClass) and constrained to (i.e. unified with) the root of the chord.

The sequence of melody pitches is constrained to raise continuously in the following way. The pitch sequence of the melody is accessed with the method mapItems. mapItems is a higher-order method, that is it expects a function or a method as argument.

{MyNoteSeq mapItems($ getPitch)}

The method mapItems applies the method getPitch to every score object — i.e. every note — directly contained in MyNoteSeq and returns the collected pitch variables. This pitch sequence is then constrained by the pattern constraint Pattern.increasing.

These basic principles can be applied to constrain the shape of the melody in various ways. The next example constrains the pitch classes of the notes in NoteSeq to form a cycle pattern of length 4. Moreover, all pitches must be pairwise distinct.

source

This example replaces the two melody constraint code lines shown above by the following two lines. Pattern.cycle constrains the pitch classes to form a cycle pattern of length 4. FD.distinct forces all pitches in the melody (in contrast to the pitch classes) to be pairwise distinct.

{Pattern.cycle {MyNoteSeq mapItems($ getPitchClass)} 4}
{FD.distinct {MyNoteSeq mapItems($ getPitch)}}

Many more pattern constrains could be used to constrain the melody pitches, pitch classes, or dependent variables. For example, the intervals between the melody pitches may be constrained to form a rotation pattern. Alternatively, a pattern constraint could be applied to only a subsequence of the melody notes. For example, the first N note pitches may be constrained to increase, whereas the remaining note pitches may decrease.

Nevertheless, a melody consisting only of chord notes is a rather restricted case. Therefore, the next section explains how to introduce non-harmonic tones in a controlled way.

Allowing for Non-Harmonic Tones

This example is very similar to the previous examples, but it allows for non-harmonic (or non-chord) tones as well (non-harmonic tones are marked by an 'x' above the note). However, non-harmonic tones are only allowed under specific circumstances. In this example, passing tones are the only non-harmonic tones permitted (chord tones can of course occur freely as before). Moreover, non-harmonic tones must always be diatonic tones: even if melodic notes do not fit into their corresponding chord, they must nevertheless fit into a scale which is suitable for the chord. The chord in this example is the plain D minor chord (i.e. the pitch classes {0, 3, 7}, transposed by 2). The scale is the D minor scale (i.e. the pitch classes {0, 2, 3, 5, 7, 8, 10}, also transposed by 2). In addition, the example constrains the melodic contour of the melody. The melody first raises and then falls.

source

The following example again variates only the pattern constraint applied to the pitch sequence of the melody. In this case, the melodic contour follows a cycle pattern. Here, the melodic contour is a sequence of directions of melodic intervals (i.e., whether the melodic interval is ascending, descending, or is unison).

source

Constraining the Relation of a Note to a Scale

The last two examples constrained every melody note to fit into a given scale. This section explains how this relation between notes and a scale is defined. First of all, the examples add a scale to their harmony database. A scale has a collection of (untransposed) scale pitch classes and a collection of (untransposed) root pitch class candidates. There is unusally only a single scale root candidate, nevertheless a scale database entry has the same format as a chord database entry for consistency.

{HS.db.setDB unit(chordDB:chords(chord(comment:'minor'
                                       pitchClasses:[0 3 7]
                                       roots:[0]))
                  scaleDB:scales(scale(comment:'minor'
                                       pitchClasses:[0 2 3 5 7 8 10]
                                       roots:[0])))}

Next, the examples instantiate a scale object. Former examples instantiated chord objects as part of the temporal score. This approach is also valid for a scale object, but for the present examples it is more simple to instantiate this object `directly'. The index of the scale can be omitted and is derived implicitly (there is only a single scale in the database). The transposition is set to 2, that is the resulting scale represents the D minor scale.

D_Minor = {Score.makeScore2 scale(transposition:2)
           Aux.myCreators}

Finally, the examples extend the textual specification of the note objects in order to tell each note to which scale it belongs. In the present case, the note-scale relation is very simple. All notes are related to the same scale, this scale is already known in the CSP definition, and the pitch class of every note is a member of the scale's pitch class set. However, there are cases where all this information may be missing in the CSP definition, where this information is constrained, and found out only during the search process. In order to allow for such cases, Strasheela's harmony model is highly programmable. Therefore, the following note specification is relatively complex for a simple example like the present one. Nevertheless, this complexity can be hided in case it is not needed. For example, the relation between notes and chords in the previous examples was defined in the same way (see below), but these examples didn't show this specification at all. Instead, these examples used some predefined and easy-to-use abstraction (here Aux.myCreators), which created this relation `backstage'.

We will now study the definition of the relation between a note and a scale in full detail. To each note specification, the arguments getScales, isRelatedScale, and inScaleB are added. The arguments getScales and isRelatedScale express which scale the note is related to, and inScaleB indicates whether the note's pitch class is a member of the scale's pitch class set (inScaleB is 1) or not (inScaleB is 0). The arguments getScales and isRelatedScale expect first-class functions (i.e. procedures which return their last value). The function (given to) getScales returns a list of related scale candidates. The function isRelatedScale returns a boolean variable (i.e. a constrained variable with the domain {0, 1}) indicating whether or not a given scale candidate is indeed related to a given note. In the note specification below, the function getScales simply returns a list which only contains the D minor scale defined above. The function isRelatedScale always returns true (i.e. 1). The argument inScaleB is set to 1, that is the note must be a diatonic note in the D minor scale.

note(duration: 4
     pitch: {FD.int 60#72}
     getScales: proc {$ MyNote MyScales} MyScales = [D_Minor] end
     isRelatedScale: proc {$ MyNote MyScale B} B = 1 end
     inScaleB: 1
     amplitude: 64)
Efficiency Remarks

A simpler note creation interface would only introduce a single function getScale instead of the two functions getScales and isRelatedScale. Strasheela introduces these two functions for efficiency reasons. Efficiency is an important concern, because a generic system like Strasheela effectively invites the user to define highly complex CSPs. If not defined cautiously, they quickly result in problems which can take a long time to solve (e.g. hours or even days). All CSP defined here, however, are solved reasonably fast (usually within a few milliseconds — the longest time before a result is shown is taken by Lilypond ;-) ).

Strasheela's constraint programming model features constraint propagation, which reduces the domain of constrained variables by automatic deduction and that way considerably reduces the search space (see my thesis for details). However, constraint propagation first needs to know which variables are involved in the propagation process. The function getScales is purely deterministic, and constraint propagation between the parameters of the note and scales can not happen before getScales returned its list of scale candidates. In fact, getScales should best only depend on information already available in the CSP definition and should immediately return. Besides, the user should aim to keep the number of chord candidates low in order to keep the search space as small as possible. The function isRelatedScale, on the other hand, can constrain the relation between a note and a scale candidate. This function defines an arbitrary relation between a note object, a scale object, and a boolean variable. The constraints posted by isRelatedScale cause propagation. In the example above, the function getScales returns a list with only a single scale candidate. Therefore, the function isRelatedScale is not really needed here — it sets its boolean argument always to true (i.e. 1).

[NB: the following sections are unfinished, please come back later]

Constraining the Relation of a Note to a Chord

note(duration:4
     pitch:{FD.int 60#72}
     amplitude:64
     inChordB:{FD.int 0#1}
     getChords:proc {$ Self Chords}
                  Chords = {Self getSimultaneousItems($ test:HS.score.isChord)}
               end
     isRelatedChord:proc {$ Self Chord B} B=1 end)
Efficiency Remarks

Allowing for Non-Harmonic Notes in a Controlled Way

This subsection explain how the examples above restrict non-harmonic tones to only specific conditions such as passing tones.

{MyNoteSeq
 forAllItems(proc {$ MyNote}
               {MyNote nonChordPCConditions([Aux.isPassingNoteR])}
             end)}

The passing note rule is already predefined for convenience (Aux.isPassingNoteR calls HS.rules.isPassingNoteR from Strasheela's harmony model). Nevertheless, you can also define such non-chord conditions yourself. For example, the rule ResolveStepwiseR constrains that a non-chord tone is always resolved stepwise: the interval to its successor note is 2 at maximum. This rule is a generalisation of a passing tone and a neighbour tone (or auxiliary tone), where multiple non-chord tone can also follow each other. This rule also states that the first and last note in the melody must be chord tones.

proc {ResolveStepwiseR Note1 B}
   MaxStep = 2
   Container = {Note1 getTemporalAspect($)}
in
   if {Note1 isFirstItem($ Container)}
      orelse {Not {Note1 hasSuccessor($ Container)}}
      %% the first note and the last note must be chord tones
   then B=0
      %% the interval to the following note is MaxStep at maximum
   else Note2 = {Note1 getSuccessor($ Container)} in
      B = {FD.reified.distance
           {Note1 getPitch($)} {Note2 getPitch($)} '=<:' MaxStep}
   end
end

A non-chord condition constrains the boolean variable B to 1 (i.e. true) in case non-chord tones are permitted for the note argument (Note1), and to 0 in case only chord tones are valid. When the passing note rule is replaced by the rule ResolveStepwiseR, we get the following result.

source

Constraining the Rhythmical Structure

All the examples shown here constrain only the pitch structure. Strasheela is not restricted to such CSPs (as was already shown in the florid counterpoint example). The following example constrains the rhythmical structure and the pitch structure as well. The example allows for various rhythmic note values, but all notes which are an eighth note or shorter must be non-harmonic notes and passing notes. In addition, the contour follows again a cycle pattern.

source

Efficiency Remarks

Multiple Voices

simple case: four parallel voices. three voices which express chord but without any further constraints and an additional long base note whose pitch class is constrained to chord root

source

constraining pitch classes of simultaneous notes to be different

source

Expressing a Fixed Harmonic Progressions: a Simple Cadence

shown with multiple voices: then harmonic progression is more clear

source

Constraining the Harmony

The following examples constrain the harmonic progression itself. Please note that all techniques shown above can also be applied to examples where the harmony is searched for as well.

Setting the harmony database

{HS.db.setDB unit(chordDB:chords(chord(comment:'maj'
                                       pitchClasses:[0 4 7]
                                       roots:[0])
                                 chord(comment:'min'
                                       pitchClasses:[0 3 7]
                                       roots:[0])))}

Defining the Music Representation

sim(items:[seq(handle:MyVoice
               items:{LUtils.collectN NoteNo
                      fun {$}
                         note(duration:NoteDur
                              pitch:{FD.int 60#72}
                              amplitude:64)
                      end})
           %% chord indices and transpositions specified explicitly
           seq(handle:ChordSeq
               items:{LUtils.collectN ChordNo
                      fun {$} chord(duration:ChordDur) end})]
    startTime:0
    timeUnit:beats(4))

Defining a Simple Theory of Harmony

%% different root neighbours
{Pattern.for2Neighbours Chords
 proc {$ Chord1 Chord2} {Chord1 getRoot($)} \=: {Chord2 getRoot($)} end}
%% harmonic band
{HS.rules.neighboursWithCommonPCs Chords}
%% start and end with c
0 = {Chords.1 getRoot($)} = {{List.last Chords} getRoot($)}

Contour follows cycle pattern (the first three pitches are distinct)

source

Only diatonic pitches in D-major (the key signature is missing..) — again the contour follows cycle pattern. (the first three pitches are distinct)

source

Multiple voices, allow for non-harmonic but diatonic pitches (again D-major)

source

Transformation of the previous example into a 'canon': the exact pitches do not necessarily match, but the contour of the voices.

[TODO: refine CSP definition]

source

back


[TODO:]