Florid Counterpoint

back

Overview
Music Representation
Compositional Rules Overview
Rule Definition and Application

Overview

This example demonstrates Strasheela's capabilities for solving polyphonic CSPs where both the pitch structure as well as the rhythmical structure is constrained by rules. Users of previous systems could not define and solve such complex CSPs (at least not in a reasonable amount of time), because such CSPs require suitable score search strategies not supported by these systems. For example, Score-PMC — a pioneering system for polyphonic CSPs — requires that the temporal structure of the music is fully determined in the problem definition. Strasheela has been designed with complex musical CSPs like florid counterpoint in mind. The Strasheela user can select (and even define) a search strategy suitable for her CSP. Polyphonic problems like the one below are solved in a few seconds — and thus in a reasonable amount of time for practical use — by Strasheela's left-to-right distribution strategy (explained in my thesis).

For simplicity, this example compiles rules from various sources instead of following a specific author closely. For example, some rules are variants from Fuxian rules introduced before, while other rules (in particular rhythmical rules) were inspired by Motte (1981). Accordingly, the result does also not imitate a particular historical style (but neither does Fux, cf. Jeppesen (1930)).

The example creates a two voice counterpoint. In the score, the analysis brackets point out that the first notes of each voice form a canon. An 'x' on top of a note denotes a passing note (the pause at the end was manually added to the output).

click the score for sound (mp3)

source

The rest of this section explains important aspects of the implementation of this example. The music representation is discussed, the compositional rules are explained, and two example rules are fully defined and applied to the music representation.

Music Representation

The music representation consists of two parallel voices — represented by a nesting of Strasheela score objects. Two sequential containers (expressing that the contained melody notes form a sequence) are nested in a simultaneous container (expressing that the sequential containers run in parallel). The score topology has thus the following form (items is the argument of a container listing the contained score objects).

sim(items:[seq(items:[note1 ... noteN])
           seq(items:[note1 ... noteN])])

Such a textual music representation specification is transformed into a nested score object (with an extensive data abstraction interface for accessing score information) by the function Score.makeScore. The following code snippet shows the music representation specification in full detail. Here, notes are created by the function MakeNote. MakeNote returns a note specification with individual variables at the parameters duration and pitch — in contrast to the Fuxian example, not only all note pitches but also all note durations are searched for. Functions are first-class objects in Oz (e.g. a function can expect other functions as arguments).1 The function LUtils.collectN receives MakeNote as an argument, calls it multiple times, and returns the collected results.

fun {MakeNote PitchDomain}
  %% duration domain {eighth, quarter, halve note} -- depends on timeUnit set below
  note(duration: {FD.int [2 4 8]}
       pitch: {FD.int 53#72}               % midi keynumbers in {53, ..., 72}
       amplitude: 80)
end

MyScore = {Score.makeScore
           sim(items: [seq(handle:Voice1   % bind variable Voice1 to instance of seq
                           items: {LUtils.collectN 17 MakeNote}
                           offsetTime:0
                           %% Voice1 and Voice2 end at same EndTime
                           endTime:EndTime)
                       seq(handle:Voice2
                           items: {LUtils.collectN 15 MakeNote}
                           %% Voice2 starts whole note later
                           offsetTime:16
                           endTime:EndTime)]
               startTime: 0
               timeUnit:beats(4))  % a beat has length 4 (i.e. 1 denotes a sixteenth note)
           unit}

The two sequential containers are accessible via the variables Voice1 and Voice2 (due to the handle argument), whereas the surrounding simultaneous container is accessible via the variable MyScore (bound to the return value of Score.makeScore). The start time and end time of both voices is further restricted. Voice1 begins a bar before Voice2. This is expressed by setting the offset time of these two sequential containers (and thus their start time with respect to their surrounding simultaneous container) to different values. The offset of Voice1 is 0 (i.e. it is starting with the score), and the offset of Voice2 is a semibreve (i.e. its start is delayed by a semibreve). In addition, both voices end at the same time (the end time of both sequential containers is unified by binding them to the same variable EndTime).

Compositional Rules Overview

The example defines rules on various aspects of the music; it applies rhythmic rules, melodic rules, harmonic rules, voice-leading rules and rules on the formal structure. These rules are listed in the following.

Rhythmical Rules

Melodic Rules

The rules constraining the melodic peak — inspired by Schoenberg — have great influence on the personally evaluated quality of the result but also on the combinatorial complexity of the CSP.

Harmonic Rules

Formal Rule

Rule Definition and Application

In the following, two rule implementations are shown as examples. A Strasheela rule is a procedure expecting arguments which are somehow constrained.

The rule InCMajor constrains its argument MyNote to have a diatonic pitch in C major. Internally, this rule creates a new variable for the pitch class of MyNote (the pitch class is modulus 12 of the pitch of MyNote). The rule states that this pitch class is not an element of the set of pitch classes representing the 'black keys' on the piano, that is {1, 3, 6, 8, 10}. In conventional mathematics notation, the rule states the following.

let pitchClass = getPitch(myNote) mod 12

pitchClass not in {1, 3, 6, 8, 10}

The following code fragment shows the implementation of this rule in Oz syntax. The rule expresses by an iteration that PitchClass is not a member of the set of black-key pitch classes. For every element in the list [1 3 6 8 10] it is stated that the PitchClass must be different. The iteration is defined by applying an anonymous first-class procedure (defined inline) to each element of the list.2 This approach is very similar to mapping as known from functional programming, only no results are returned.

proc {InCMajor MyNote}
   PitchClass = {FD.modI {MyNote getPitch($)} 12}
in
   {List.forAll [1 3 6 8 10]
    proc {$ BlackKey} PitchClass \=: BlackKey end}
end

The rule IsCanon constrains Voice1 and Voice2 to form a canon in the fifth. The rule loops in parallel through the first CanonNo notes of each voice. It constrains note pairs at the same position in their containing voice to equal durations and to pitches exactly 7 apart (i.e. a fifth measured in semitones).

proc {IsCanon Voice1 Voice2}
   CanonNo = 10
in
   for
      Note1 in {List.take {Voice1 getItems($)} CanonNo}
      Note2 in {List.take {Voice2 getItems($)} CanonNo}
   do
      {Note1 getDuration($)} =: {Note2 getDuration($)}
      {Note1 getPitch($)} + 7 =: {Note2 getPitch($)}
   end
end

Strasheela supports various means for conveniently applying a rule to the score. The rule IsCanon is applied directly, because the definition of the music representation (see above) made its arguments already accessible via the variables Voice1 and Voice2.

{IsCanon Voice1 Voice2}

Other rules require the access of score object sets to which they are applied. InCMajor is applied to all notes in the score. The Strasheela score object method forAll applies a procedure to all objects for which a test returns true. The test can be a Boolean function, or the name of a Boolean method as in the example below. The method isNote returns true for a note object (and false for any other score object). Please note that the notes are not directly contained in MyScore. The method forAll traverses the full score hierarchy.

{MyScore forAll(test: isNote
                InCMajor)}

The present example uses only the style-independent Strasheela core, no Strasheela extension is applied. By contrast, the next examples (e.g. the automatic melody harmonisation and the collection of of harmonic CSPs) make use of Strasheela's harmony model extension in order to simplify their definition.

back


1. A first-class function is sometimes also called a lambda expression.

2. The Oz expression $ denotes a return value. If it substitutes the name, for example, of a function, then the function itself is returned (see the Mozart tutorial). Also, Strasheela score accessor methods make use of $, for example, {MyNote getPitch($)} returns the pitch of MyNote.


[TODO:]

Present a few variations of this example: