Scripting 5.5. Procedures

Sometimes in a Praat script, you will want to perform the same thing more than once. In §5.4 we saw how loops can help there. In this section we will see how procedures (also called subroutines) can help us.

Imagine that you want to play a musical note with a frequency of 440 Hz (an "A") followed by a note that is one ocatve higher, i.e. has a frequency of 880 Hz (an "a"). You could achieve this with the following script:

Create Sound as pure tone: "note", 1, 0, 0.3, 44100, 440, 0.2, 0.01, 0.01
Play
Remove
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, 880, 0.2, 0.01, 0.01
Play
Remove

This script creates a sound with a sine wave with an amplitude of 0.4 and a frequency of 440 Hz, then plays this sound, then changes the sound into a sine wave with a frequency of 880 Hz, then plays this changed sound, and then removes the Sound object from the object list.

This script is perfect if all you want to do is to play those two notes and nothing more. But now imagine that you want to play such an octave jump not only for a note of 440 Hz, but also for a note of 400 Hz and for a note of 500 Hz. You could use the following script:

Create Sound as pure tone: "note", 1, 0, 0.3, 44100, 440, 0.2, 0.01, 0.01
Play
Remove
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, 880, 0.2, 0.01, 0.01
Play
Remove
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, 400, 0.2, 0.01, 0.01
Play
Remove
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, 800, 0.2, 0.01, 0.01
Play
Remove
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, 500, 0.2, 0.01, 0.01
Play
Remove
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, 1000, 0.2, 0.01, 0.01
Play
Remove

This script works but is no longer perfect. It contains many similar lines, and is difficult to read.

Here is where procedures come in handy. With procedures, you can re-use similar pieces of code. To make the three parts of the above script more similar, I'll rewrite it using two variables (frequency and octaveHigher):

frequency = 440
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, frequency, 0.2, 0.01, 0.01
Play
Remove
octaveHigher = 2 * frequency
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, octaveHigher, 0.2, 0.01, 0.01
Play
Remove
frequency = 400
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, frequency, 0.2, 0.01, 0.01
Play
Remove
octaveHigher = 2 * frequency
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, octaveHigher, 0.2, 0.01, 0.01
Play
Remove
frequency = 500
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, frequency, 0.2, 0.01, 0.01
Play
Remove
octaveHigher = 2 * frequency
Create Sound as pure tone: "note", 1, 0, 0.3, 44100, octaveHigher, 0.2, 0.01, 0.01
Play
Remove

You can now see that seven lines of the script appear identically three times. I'll put those seven lines into a procedure that I name "playOctave":

procedure playOctave
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, frequency, 0.2, 0.01, 0.01
    Play
    Remove
    octaveHigher = 2 * frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, octaveHigher, 0.2, 0.01, 0.01
    Play
    Remove
endproc

As you see, a procedure definition in Praat consists of three parts:

1. a line with the word procedure, followed by the name of the procedure, followed by a pair of parentheses;
2. the body of the procedure (here: seven lines);
3. a line with the word endproc.

You can put a procedure definition anywhere in your script; the beginning or end of the script are common places.

The bodies of procedures are executed only if you call the procedure explicitly, which you can do anywhere in the rest of your script:

frequency = 440
@playOctave
frequency = 400
@playOctave
frequency = 500
@playOctave
procedure playOctave
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, frequency, 0.2, 0.01, 0.01
    Play
    Remove
    octaveHigher = 2 * frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, octaveHigher, 0.2, 0.01, 0.01
    Play
    Remove
endproc

This script works as follows. First, the number 440 is assigned to the variable frequency in line 1. Then, execution of the script arrives at the @ ("call") statement of line 2. Praat then knows that it has to jump to the procedure called playOctave, which is found on line 7. The execution of the script then proceeds with the first line of the procedure body, where a Sound is created. Then, the other lines of the procedure body are also executed, ending with the removal of the Sound. Then, the execution of the script arrives at the endproc statement. Here, Praat knows that it has to jump back to the line after the line that the procedure was called from; since the procedure was called from line 2, the execution proceeds at line 3 of the script. There, the number 400 is assigned to the variable frequency. In line 4, execution will jump to the procedure again, and with the next endproc the execution will jump back to line 5. There, 500 is assigned to frequency, followed by the third jump to the procedure. the third endproc jumps back to the line after the third @, i.e. to line 7. Here the execution of the script will stop, because there are no more executable commands (the procedure definition at the end is not executed again).

Arguments

The above example contains something awkward. The procedure playOctave requires that the variable frequency is set to an appropriate value, so before calling playOctave you always have to insert a line like

frequency = 440

This can be improved upon. In the following version of the script, the procedure playOctave requires an explicit argument:

@playOctave: 440
@playOctave: 400
@playOctave: 500
procedure playOctave: frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, frequency, 0.2, 0.01, 0.01
    Play
    Remove
    octaveHigher = 2 * frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, octaveHigher, 0.2, 0.01, 0.01
    Play
    Remove
endproc

This works as follows. The first line of the procedure now not only contains the name (playOctave), but also a list of variables (here only one: frequency). In the first line of the script, the procedure playOctave is called with the argument 440. Execution then jumps to the procedure, where the argument 440 is assigned to the variable frequency, which is then used in the body of the procedure.

Encapsulation and local variables

Although the size of the script has now been reduced to 12 lines, which cannot be further improved upon, there is still something wrong with it. Imagine the following script:

frequency = 300
@playOctave: 440
@playOctave: 400
@playOctave: 500
writeInfoLine: frequency
procedure playOctave: frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, frequency, 0.2, 0.01, 0.01
    Play
    Remove
    octaveHigher = 2 * frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, octaveHigher, 0.2, 0.01, 0.01
    Play
    Remove
endproc

You might have thought that this script will write "300" to the Info window, because that is what you expect if you look at the first five lines. However, the procedure will assign the values 440, 400, and 500 to the variable frequency, so that the script will actually write "500" to the Info window, because 500 is the last (fourth!) value that was assigned to the variable frequency.

What you would want is that variables that are used inside procedures, such as frequency and octaveHigher here, could somehow be made not to "clash" with variable names used outside the procedure. A trick that works would be to include the procedure name into the names of these variables:

frequency = 300
@playOctave: 440
@playOctave: 400
@playOctave: 500
writeInfoLine: frequency
procedure playOctave: playOctave.frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, playOctave.frequency, 0.2, 0.01, 0.01
    Play
    Remove
    playOctave.octaveHigher = 2 * playOctave.frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, playOctave.octaveHigher, 0.2, 0.01, 0.01
    Play
    Remove
endproc

This works. The six tones will be played, and "300" will be written to the Info window. But the formulation is a bit wordy, isn't it?

Fortunately, Praat allows an abbreviated version of these long names: just leave "playOctave" off from the names of the variables, but keep the period (.):

frequency = 300
@playOctave: 440
@playOctave: 400
@playOctave: 500
writeInfoLine: frequency
procedure playOctave: .frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, .frequency, 0.2, 0.01, 0.01
    Play
    Remove
    .octaveHigher = 2 * .frequency
    Create Sound as pure tone: "note", 1, 0, 0.3, 44100, .octaveHigher, 0.2, 0.01, 0.01
    Play
    Remove
endproc

This is the final version of the script. It works because Praat knows that you are using the variable names .frequency and .octaveHigher in the context of the procedure playOctave, so that Praat knows that by these variable names you actually mean to refer to playOctave.frequency and playOctave.octaveHigher.

It is advisable that you use such "local" variable names for all parameters of a procedure, i.e. for the variables listed after the procedure word (e.g. .frequency), as well as for all variables that you create in the procedure body (e.g. .octaveHigher). In this way, you make sure that you don't inadvertently use a variable name that is also used outside the procedure and thereby perhaps inadvertently change the value of a variable that you expect to be constant across a procedure call.

A list of numeric and string arguments

You can use multiple arguments, separated by commas, and string arguments (with a dollar sign in the variable name):

@listSpeaker: "Bart", 38
@listSpeaker: "Katja", 24
procedure listSpeaker: .name$, .age
    appendInfoLine: "Speaker ", .name$, " is ", .age, " years old."
endproc

or

@conjugateVerb: "be", "I am", "you are", "she is"
procedure conjugateVerb: .verb$, .first$, .second$, .third$
    writeInfoLine: "Conjugation of 'to ", .verb$, "':"
    appendInfoLine: "1sg ", .first$
    appendInfoLine: "2sg ", .second$
    appendInfoLine: "3sg ", .third$
endproc

For the arguments you can use expressions:

@playOctave: 400 + 100

As with all string literals, the double quotes in literal string arguments should be doubled:

procedure texts: .top$, .bottom$
    Text top: "yes", .top$
    Text bottom: "yes", .bottom$
endproc
@texts: """hello"" at the top", """goodbye"" at the bottom"

Functions

The Praat scripting language does not have the concept of a "function" like some other scripting languages do. A function is a procedure that returns a number or a string. For instance, you can imagine the function squareNumber which takes a number (e.g. 5) as an argument and returns the square of that number (e.g. 25). Here is an example of how you can do that, using the global availability of local variables:

@squareNumber: 5
writeInfoLine: "The square of 5 is ", squareNumber.result, "."
procedure squareNumber: .number
    .result = .number ^ 2
endproc

Another way to emulate functions is to use a variable name as an argument:

@squareNumber: 5, "square5"
writeInfoLine: "The square of 5 is ", square5, "."
procedure squareNumber: .number, .squareVariableName$
    '.squareVariableName$' = .number ^ 2
endproc

However, this uses variable substitution, a trick better avoided.

Links to this page


© ppgb, September 4, 2017