Demonstration 8 provided the first of two practical components for Chapter 11: “Musical Design II: — Grammars” of my unpublished textbook on composing programs. It illustrates the method of generative grammars and, not coincidentally, the principle of top-down design. This method produces complex musical statements by starting with a fairly simple archetype and applying a set of fairly simple rewrite rules, also known as productions. The productions are applied recursively, which means that productions are capable of operating upon their own products.
Demonstration 8 consists of three consecutive versions (variations) of a basic tune — or in linguistic jargon, three consecutive elaborations of an archetype. Each successive variation ornaments the tune by increasing orders of embellishment. The grammar of Demonstration 8 is ‘functional’ in the sense that the notes of the basic tune directly constitute grammatical units at the highest structural level, while all other notes may be regarded as ‘ornamental’ with varying levels of significance. The productions used to embellish both the basic tune and the higher-level ornaments are drawn from traditional formulae: seconds are embellished by either escaping tones or reaching tones; thirds by changing-note groups (“cambiata”).
The basic tune is illustrated in Figure 1 while the ornaments appear in Figure 2. Notice that the basic tune and each of the ornaments consist solely of seconds and thirds. It is this consistent property which enables the ornamenting process to be applied recursively up to five levels (counting the archetype as level 1): not only does the program elaborate upon the tune itself, but also upon the ornaments, upon the ornaments upon the ornaments, and so forth. Notes from the basic tune receive durations of 8 sixteenths; ornamental notes receive durations of 5, 3, 2, and 1 sixteenths, depending on their recursive levels.
The transcribed product appears in Figure 3.
The trick to implementing top-down recursion in FORTRAN, which does not employ a system stack and which therefore
does not permit self-referencing code modules, is to set up an explicit collection of parallel stacks so that all of the
attributes characterizing a grammatical unit at an arbitrary level of recursion can be accessed using a single index.
|
|
|
Listing 1: Program DEMO8.
|
Listing 2: BLOCK DATA routine for DEMO8.
|
Program DEMO8 proper appears as Listing 1; the BLOCK DATA
routine used to initialize COMMON memory variables appears as Listing 2.
The variable LEVEL provides a single
stack pointer, while the various stacks indexed by this pointer are all implemented as arrays with names ending in LEV.
Notice that LEVEL and its associated stack arrays are all declared in common memory, which makes them accessible
both to DEMO8 proper and to the program's subroutines.
Notice also that none of LEVEL and its associated stack arrays are initialized in the
BLOCK DATA routine. These variables will provide active workspace whose
content will frequently be overwritten.
The following explanations seek to illustrate recursive grammars in play. The explanations are peppered with line numbers, but you are are by no means expected to chase down every line of code. Rather, you should follow through with line numbers only when you have a specific question that the narrative is not answering.
The loop spanning lines 16-26 is indexed by IVER.
It iterates through Demonstration 8's three versions; line 19's call to subroutine COMPOS
generates the individual versions.
The only explicit parameter in this call is ITIME, which holds the current score time in thirty-second notes.
Understand that FORTRAN passes subroutine arguments by address rather than by value, so that anything COMPOS
does with ITIME also affects ITIME's value here in the main program.
The variable
Subroutine COMPOS takes the
The three-element array EMBVER
It also passes a value from array EMBVER to the first location
of array EMBLEV; this value controls the number of nested ornaments.
The heart of the compositional process is subroutine COMPOS, presented in Listing 3.
Although the body of this subroutine is implemented as a loop (lines 23-49), this is not your normal iterative sequence.
The loop indexing is bi-directional:
LEVEL iterates from the top down and back, where LEVEL=1 is topmost.
IDX iterates from left-to-right in the time dimension, where IDX=1 is leftmost.
To complicate things further, each change in LEVEL disrupts the left-to-right progression of IDX.
Thus whenever COMPOS descends to a lower level, it needs set aside (“push”) the current value of IDX so that
this value can be reclaimed (“pop”) once lower-level processing has completed.
That is what a stack is for.
In particular, array IDXLEV keeps track of IDX at the various recursive levels, while array CNTLEV
stores corresponding limits. Each loop iteration begins first by advancing IDX one position to the right (line 25), then
immediately tests (line 26) whether IDX has reached its limit.
COMPOS descends to the next (deeper) level in line 40 and immediately thereafter initializes IDXLEV(LEVEL)
to 0. COMPOS ascends to the previous (shallower) level in line 47, but the value of IDX is not reclaimed
until the loop iterates (line 25).
COMPOS keeps track of the levels of levels of recursion, each of which ornaments a melodic fragment passed down
from the previous level. The variable LEVEL indicates the level of recursion, array element CNTLEV(LEVEL)
indicates how many items occur in a fragment, and array element IDXLEV(LEVEL) indicates which of these items
is currently under consideration.
For increased efficiency, COMPOS transfers this indicator to the holding variable IDX.
The pitch of the current item is stored in array element PCHLEV(IDX,LEVEL).
For LEVEL=1, COMPOS initializes PCHLEV to the basic tune (lines 13-18), which
is specified to the program as array TUNE and populated in line 8 of the BLOCK DATA routine
presented in Figure 4 (d).
Items at lower levels may be either notes or rests; rests are represented as notes with null pitches.
For each item but the last in a fragment, COMPOS performs the following actions:
COMPOS directs subroutine
WNOTE to append it to the human-readable listing (line 34 for rests, line 36 for notes).
The duration of a note is stored in array element DURLEV(LEVEL) as populated in line 9 of the
BLOCK DATA routine.
EMBLEV(LEVEL)) has yet to be reached, then COMPOS does two things:
COMPOS requests subroutine DEDUCE to select a possible ornament
at the next level down (line 38)
COMPOS then advances to the next recursive level (lines 40-41).
The last note in a fragment at the current level is the same as the next note in the fragment at the previous level, so
COMPOS backtracks one level (lines 46-47) when IDX reaches CNTLEV(LEVEL).
Subroutine DEDUCE is presented in Listing 4.
Although the subroutine has no formal arguments, information is
passed along from subroutine DEDUCE through common memory variables. In particular, LEVEL
gives the current level of recursion, LEVEL1 gives the level plus 1, IPCH1 gives the origin pitch, and IPCH2
gives the goal pitch.
For each pair of consecutive pitches
passed to it either by subroutine COMPOS, or by a prior level of recursion, subroutine DEDUCE
first decides whether or not to ornament at the current level.
For each pair of consecutive pitches
passed to it either by subroutine COMPOS, or by a prior level of recursion, subroutine DEDUCE
first decides whether or not to ornament at the current level. If so DEDUCE first identifies a repertory of
suitable ornaments, then chooses a specific ornament from this repertory.
The decision to ornament (lines 24-31) is a yes-or-no trial which incorporates feedback from corresponding trials at higher
levels.1
Line 24 executes this decision with a call to the library function SUCCES
where the probability of a positive outcome is EMBLEV(LEVEL)/FLOAT(MLEV1-LEVEL).
Here the numerator reflects how many choices-to-ornament are to be made from the current recursive level on down, while the
denominator reflects the number of levels remaining, and thus the number of opportunities left.
EMBLEV(LEVEL1) = EMBLEV(LEVEL)-1.0.
DEDUCE then proceeds to select which ornament to apply.
EMBLEV(LEVEL1) = EMBLEV(LEVEL).
DEDUCE then skips the remaining actions and returns immediately to COMPOS.
If it decides in favor of ornamenting an interval at the current level, lines 35-44 of DEDUCE determine what kind of interval it must
ornament. It locates a repertory of ornaments for this level among the eight repertories illustrated in Figure 2.
All ornamental pitches are expressed (lines 12-19) in array ORNMNT as offsets from the origin pitch.
How this data is accessed exemplifies the index gymnastics needed to represent non-rectangular data in
FORTRAN arrays.
For each of the eight intervals, array PTRORN holds a pointer (array index) indicating which element of ORNMNT holds the first
offset for the first ornament in the repertory corresponding to the indicated interval.
Array INCORN holds the number of ornamental pitches (1 for escaping or reaching tones, 2 for changing-note groups);
incrementing the pointer by this number gives successive ornaments in the repertory.
Array NUMORN indicates the number of ornaments in the repertory.
| Element | Pitch Offset | Numeric Pitch | Symbolic Pitch | Chromatic Number |
|---|---|---|---|---|
ORNMNT(37) |
-1 2 |
53-1=52 53+2=55 |
E4 G4 |
5 8 |
ORNMNT(39) |
-1 3 |
53-1=52 53+3=56 |
E4 G#4 |
5 9 |
ORNMNT(41) |
1 5 |
53+1=54 53+5=58 |
F#4 E4 |
7 5 |
ORNMNT(43) |
-2 2 |
53-2=51 53+2=55 |
E!4 G4 |
4 8 |
ORNMNT(45) |
2 5 |
53+2=55 53+5=58 |
G4 B!4 |
8 11 |
ORNMNT(47) |
2 6 |
53+2=55 53+6=59 |
G4 B4 |
8 12 |
For example, Table 1 dereferences from ORNMNT the various ornaments which would apply to
the second interval in Figure 1. This interval ascends from TUNE(2)=53 (F4) to TUNE(3)=57 (A4).
The pitch number 53 comes into DEDUCE through the common variable IPCH1,
while the pitch number 57 comes through IPCH2.
Line 35 calculates the index I into arrays PTRORN, INCORN, and NUMORN as
IPCH2-IPCH1+5=57-53+5=4+5=9. Array element PTRORN(9) contains the index value 37.
This is the leftmost position in the seventh line of the multi-line data statement which initializes ORNMNT (line 18 of the listing).
The number of ornamental pitches in the present example INC is INCORN(9)=2, which is why Table 1 dereferences
ORNMNT elements in pairs. The number of ornaments NUM is NUMORN(3)=6; therefore Table 1
dereferences six ornaments. These ornaments correspond to the second-from-bottom line of staves in Figure 2.
The six indices 37, 39, 41, 43, 45, and 47 are gathered into the scheduling array SCHED.
Once it has established a schedule of pointers to ornaments, the next step is to select a specific ornament.
DEDUCE does this in lines 48-61. Selection is heuristic with cumulative feedback, driven by chromatic usage.
This process is uncomplicated by constraints, since the ornaments listed in SCHED are precisely those deemed
suitable for the present context.2
A complication does exist, however, and this is posed by the changing-note groups: how to reconcile the contributions from
two separate chromatic numbers. The tactic for reconciling such competing influence is called the minimax
principle.
Referring to Table 1, consider the ornament E4-G#4 in row 2 and the ornament E!4-G4 in row 4. Assume that E and G# have been moderately used, that E! has been lightly used, and that G4 has been heavily used. Since G4 has already been heavily used, choosing E!4-G4 would throw G4 even more strongly out of balance, despite the leveling effect of using an E. Thus E4-G#4 is the more sensible choice. Thus the minimax principle at work, in a rudimentary way.
The first thing is to eliminate any bias introduced by the physical sequence
of ornaments in array ORNMNT. This was started by gathering indices into array SCHED (previous step). Bias-elimination
continues in line 44 by calling the library routine SHUFLE to randomize the content-order
in SCHED and in line 51 by using the index IDXORN of the ornament-evaluating loop to dereference an ornament locator
LPTR from SCHED(IDXORN).
Program DEMO08 maintains statistics of chromatic usage in the 12-element array CUMDEG, made
visible to DEDUCE through common memory. Within the current code segment, variable CUM0 holds the smallest
CUMDEG value discovered for an ornament so far; this variable is set very large in line 48 and is reduced in line 58 each time
a more preferable ornament is discovered. Similarly, variable CUM1 holds the largest
CUMDEG value discovered for the candidate ornament; this variable is set very small in line 50 and is increased in line 54 each time
a larger CUMDEG value is discovered. Coming out of this code segment, an index to the selected ornament has been placed
in variable IPTR.
Having selected an ornament, the next step is to marshal details about the ornament, plus its bracketing origin and goal pitches, into
the PCHLEV stack for transmission to the next level of recursion. This happens in lines 65-76. The circumstance of extracting
individual pitches for the ornament also makes this a convenient place to increment chromatic usage in CUMDEG. Notice
in line 72 that the incrementing amount is provided by DURLEV(LEVEL+1). Thus upon program completion CUMDEG(I)
will reflect not the total count of notes employing the Ith chromatic degree, but rather the combined duration of such notes.
The final action DEDUCE happens in lines 80-91.
This code employs a Bernoulli trial to decide whether or not to include a rest in the ornament.
If the trial outcome is favorable, then DEDUCE inserts a rest between two consecutive notes of the ornamented fragment.
| © Charles Ames | Original Text: 1984-11-01 | Page created: 2017-03-12 | Last updated: 2017-03-12 |