• No results found

Algorithmic Composition

Chapter 10 Functional Programming

10.3 Using the score-apply Function

Now that you have a taste of passing functions as parameters to modify the behavior of another function (in this case, mapcar), let’s look at a higher-order function for editing and transforming scores.

score-apply, introduced in Chapter 4, applies a function to each note in a score. Since the function to be applied is provided by the caller, you can tailor score-apply to solve many tasks. The template for score-apply is

score-apply(score, quote(function),

from-index: index1, to-index: index2, from-time: time1, to-time: time2) As with other “score-” functions, all the keyword parameters, starting with from-index, are optional, so to apply a function to every note in a score, you would write something like

score-apply(score, quote(function))

The function input is a symbol – the name of a function that must accept three parameters: the start time of the note, the duration of the note, and the note expression. So if the score is

{{0 0 {score-begin-end 0 3}}

{0 2 {flute pitch: 74 vel: 100}}

{2 1 {flute pitch: 75 vel: 90 }}}

then the function will be called once with the parameters 0, 2, {flute pitch: 74 vel: 100}

and once with the parameters

2, 1, {flute pitch: 75 vel: 90}

Example 10.3.1 demonstrates the use of score-apply to transform a score. In this example, notes that have pitch classes 1, 3, 6, 8, and 10 are transposed up an octave and played using the function violin. Other notes are unchanged.

Essentially all of the work in this example is in the definition of transform-accidentals. As described earlier, this function takes

10.3 Using the score-apply Function 129 three parameters: the start time, duration, and sound expression.

There are two cases, handled by the if-then-else construct. The first case applies when the pitch class is in the set {1, 3, 6, 8, 10}, so the first problem is to extract the pitch from the note expression. The function expr-get-attr (described in Example 4.5.9) is used to find the value for any given keyword. Once we have the pitch, we use member to test for membership in the set, represented by a list. If that condition is true, we need to transform the current note into one with violin as the instrument and pitch raised one octave. The tech-nique is to construct a new note in terms of the old note. The form of a note is

{start-time duration expression}

so we can use list to construct a list of these three elements. We use the same start time and duration as the original note, and these are just the parameters start and dur, so the note expression begins with list(start, dur, …). Now we just need to construct the note expres-sion. Take a moment to think about what is going on here. We are going to write a SAL expression that, when evaluated, returns a Lisp expression that will eventually be evaluated again to compute a note.

This idea that Lisp programs can be both data and programs is very powerful and one of the big attractions of Lisp.

Example 10.3.1: score-apply.sal

define function transform-accidentals(

start, dur, expr) begin

if member(expr-get-attr(expr,

keyword(pitch)) % 12, {1 3 6 8 10}) then

return list(start, dur,

cons(quote(violin),

params-transpose(rest(expr), keyword(pitch), 12))) else

return list(start, dur, expr) end

set new-score = score-apply(my-score,

quote(transform-accidentals))

Getting back to our problem, the note expression is a list starting with violin and followed by a list of alternating keywords and val-ues. To just replace the first item in a list, we can write

cons(quote(violin), rest(expr))

where expr is the original note expression. rest is used to get the

“rest” of the expression (a list) after the first item, and cons is used to insert a new item at the head of the list. Effectively, this replaces the head of the list with violin.

However, we need to do more than this. We also want to trans-pose the pitch: attribute by 12. For this, we use another convenient function, params-transpose, that can be used to add a numerical offset to the value of any keyword parameter:

params-transpose(parameter-list, keyword(attribute), offset) The parameter-list is just a list of alternating attribute symbols (keywords) and values. This should not be a complete note expres-sion with an initial function symbol. That’s why we pass in rest(expr) in Example 10.3.1. For the attribute, we pass in keyword(pitch) because that is what we want to transpose, and the offset is 12 (semitones) to get one octave.

Putting all this together, the expression to compute a new note with the function changed to violin and the pitch transposed up one octave is

list(start, dur,

cons(quote(violin), params-transpose(

rest(expr), keyword(pitch), 12)))

The rest of Example 10.3.1 handles the second case where the pitch is not in the set {1, 3, 6, 8, 10}. This case merely reconstructs the original note using list:

list(start, dur, expr)

and returns this value from the if command. This completes the defi-nition of transform-accidentals.

Finally, we pass transform-accidentals to score-apply to apply the function to each note and return a new score. Notice how quote is used so that transform-accidentals is not treated as a variable.

There is no set or other data-altering expression in Example 10.3.1. This is typical in functional programming: there is input data, often consisting of a complex structure such as a score, there is a function that “walks” through the data, either extracting useful information or rebuilding a transformed version of the data, and finally a completely new data structure is returned. In this case, the value of my-score (the input data) is unaltered, so unless we do something with the result of score-apply, this whole program has no real effect. In practice, you might want to play the outcome:

exec score-play(score-apply(my-score,

quote(transform-accidentals)))

10.3 Using the score-apply Function 131 Or, while functional purists might object to the use of set, you might want to save the final result as a variable:

set my-new-score = score-apply(my-score,

quote(transform-accidentals))) You have now seen a rather detailed example of functional pro-gramming used to perform a customized operation on score data. In this example, notes that met a certain condition were transformed in a couple of ways. While there are many built-in functions for ma-nipulating scores, it is important for a composer to have the power to create whatever comes from the imagination. You should not be lim-ited to a fixed palette of editing operations. The transformations you apply can include various uses of random numbers to select notes and control how they are changed. The possibilities are endless!

Chapter 11 Recursion

Recursion is an important concept in computer science. In this chapter, we will explore what it means for a function to call itself and why that might be a good idea.