Session 13
Advanced Concepts

Procedures

Procedures are encapsulated code blocks. The code block inside a procedure is only executed when the procedure is explicitly called, i.e. you can define as many procedures as you like—as long as they are not explicitly called their code blocks are ignored during procedural execution of the script (simple input forms are another exception to the procedural execution principle). Procedures are defined as follows:

procedure proc_name ... code block ... endproc

The reserved word procedure followed by a procedure name you can choose initiates a procedure. After the code block a new line with the reserved word endproc marks the end of the procedure. A 'working' example:

writeInfoLine: "How" procedure write_line appendInfoLine: "are" endproc appendInfoLine: "you?"

We define a procedure named write_line which is never called. Therefore, the code block (appendInfoLine: "are") is just ignored and the script produces:

How
you?

So, how do we call a procedure? That's easy, just put a "@" in front of the procedure's name:

writeInfoLine: "How" procedure write_line appendInfoLine: "are" endproc appendInfoLine: "you?" @write_line

The last line calls the procedure which results in the execution of the procedures code block. So, what's the output this script produces?

How
you?
are

The code block is executed exactly when the procedure is called, which in our example is a little bit too late. To produce the desired output let's move the procedure call between the other output commands. And let's move the procedure definition out of the way. Usually, procedures are defined at the beginning or at the end of a script. This is fine:

procedure write_line appendInfoLine: "are" endproc writeInfoLine: "How" @write_line appendInfoLine: "you?"

… as well as this:

writeInfoLine: "How" @write_line appendInfoLine: "you?" procedure write_line appendInfoLine: "are" endproc

From both scripts we get the desired output:

How
are
you?

But what's the advantage of all this? The script has 6 lines of code where—without the procedure—3 lines would have been enough. Well, procedures start to get interesting when the encapsulated code block grows to more than one line and they start to shine when code recycling becomes an issue. Code recycling is an issue if you need the same functionality again and again. For example, if you need to extract jitter and shimmer from a voice report more than once (within the same script or in different scripts) you should consider writing a procedure. This procedure encapsulates the necessary steps to generate a voice report and extract jitter and shimmer. Once defined, you can copy and paste the procedure to all scripts that need jitter and shimmer and call it as often as necessary. A first version of this procedure could look like this:

procedure jitter_shimmer sound = selected ("Sound") pitch = To Pitch (cc): 0, 75, 15, "no", 0.03, 0.45, 0.01, 0.35, 0.14, 600 pp = To PointProcess selectObject: sound, pitch, pp voice_report$ = Voice report: 0, 0, 75, 600, 1.3, 1.6, 0.03, 0.45 jitter = extractNumber (voice_report$, "Jitter (local):") shimmer = extractNumber (voice_report$, "Shimmer (local):") removeObject: pitch, pp endproc

The procedure creates the auxiliary objects (pitch and point process), generates a voice report, extracts jitter and shimmer, and removes the auxiliary objects. If you have this procedure in your script you can call it wherever and as often as needed.

procedure jitter_shimmer sound = selected ("Sound") pitch = To Pitch (cc): 0, 75, 15, "no", 0.03, 0.45, 0.01, 0.35, 0.14, 600 pp = To PointProcess selectObject: sound, pitch, pp voice_report$ = Voice report: 0, 0, 75, 600, 1.3, 1.6, 0.03, 0.45 jitter = extractNumber (voice_report$, "Jitter (local):") shimmer = extractNumber (voice_report$, "Shimmer (local):") removeObject: pitch, pp endproc # do something # ... @jitter_shimmer # do something else # e.g. select another sound # ... @jitter_shimmer

But how do you access the calculated jitter and shimmer values outside of the procedure? In its current form, the procedure uses global variables, i.e. all variables used inside the procedure behave like variables everywhere else in the script. That means, you can use jitter and shimmer outside the procedure as usual:

procedure jitter_shimmer sound = selected ("Sound") pitch = To Pitch (cc): 0, 75, 15, "no", 0.03, 0.45, 0.01, 0.35, 0.14, 600 pp = To PointProcess selectObject: sound, pitch, pp voice_report$ = Voice report: 0, 0, 75, 600, 1.3, 1.6, 0.03, 0.45 jitter = extractNumber (voice_report$, "Jitter (local):") shimmer = extractNumber (voice_report$, "Shimmer (local):") removeObject: pitch, pp endproc @jitter_shimmer writeInfoLine: "jitter: ", jitter, tab$, "shimmer: ", shimmer

This is convenient but dangerous if you paste the procedure into an existing script that already uses variables named e.g. jitter, shimmer, or pitch. In this case the procedure messes with the existing variables which could cause serious problems. Generally, procedures should be restricted to the use of local variables, i.e. variables that exist only in the context of the procedure. To illustrate this, let's go back to the simple procedure from above and add some variables:

procedure write_line word$ = "dare" endproc word$ = "are" writeInfoLine: "How" @write_line appendInfoLine: word$ appendInfoLine: "you?"

First, "are" is assigned to the global variable word$ (variables defined outside procedures are always global), then we write How to Praat Info, then we call the procedure which assigns "dare" to word$. The initial value of word$ is overwritten because the procedure uses a global variable! Therefore, when we write word$ to Praat Info in the next line, the script produces:

How
dare
you?

To avoid this, it is good practice to only use local variables in procedures. Local variables' names start with a dot and they exist only in the context of a procedure:

procedure write_line .word$ = "dare" endproc word$ = "are" writeInfoLine: "How" @write_line appendInfoLine: word$ appendInfoLine: "you?"

The procedure assigns "dare" to the local variable .word$ which has no connection with the global variable word$. Since we write the global variable word$ to Praat Info, this is the output:

How
are
you?

But what if we want to use the local variable .word$ outside of the procedure? To do that, we need to put the procedure's name in front of its local variable:

procedure write_line .word$ = "dare" endproc word$ = "are" writeInfoLine: "How" @write_line appendInfoLine: write_line.word$ appendInfoLine: "you?"

How
dare
you?

Now we can write a better version of the jitter-shimmer procedure:

procedure jitter_shimmer .sound = selected ("Sound") .pitch = To Pitch (cc): 0, 75, 15, "no", 0.03, 0.45, 0.01, 0.35, 0.14, 600 .pp = To PointProcess selectObject: .sound, .pitch, .pp .voice_report$ = Voice report: 0, 0, 75, 600, 1.3, 1.6, 0.03, 0.45 .jitter = extractNumber (.voice_report$, "Jitter (local):") .shimmer = extractNumber (.voice_report$, "Shimmer (local):") removeObject: .pitch, .pp endproc @jitter_shimmer writeInfoLine: "jitter: ", jitter_shimmer.jitter, tab$, "shimmer: ", jitter_shimmer.shimmer

All variables inside the procedure are converted to local variables to prevent any interference with global variables. Outside the procedure we can access the two variables of interest (last line: jitter_shimmer.jitter and jitter_shimmer.shimmer) and ignore the rest (which are auxiliary variables of interest only inside the procedure).

The last important feature of procedures are arguments. For example, if you determined the appropriate pitch floor of a sound somewhere in the script, you probably want to use it in the jitter-shimmer procedure. One posibility is to use a global variable in the procedure:

procedure jitter_shimmer sound = selected ("Sound") pitch = To Pitch (cc): 0, pitch_floor, 15, "no", 0.03, 0.45, 0.01, 0.35, 0.14, 600 pp = To PointProcess selectObject: sound, pitch, pp voice_report$ = Voice report: 0, 0, 75, 600, 1.3, 1.6, 0.03, 0.45 jitter = extractNumber (voice_report$, "Jitter (local):") shimmer = extractNumber (voice_report$, "Shimmer (local):") removeObject: pitch, pp endproc # select first sound, determine pitch floor, and assign it to global variable # pitch_floor = ... @jitter_shimmer # select second sound, determine pitch floor, and assign it to global variable # pitch_floor = ... @jitter_shimmer

That works as long as the calling script has a global variable named pitch_floor. But if you use the procedure in a script that has a variable named pitchFloor instead of pitch_floor it breaks. We can avoid this by handing over the necessary value(s). To implement this we extend the procedure definition so that the procedure accepts an argument—the pitch floor— by adding a colon to the procedure name and a variable to consume the passed argument:

procedure jitter_shimmer: .pf .sound = selected ("Sound") .pitch = To Pitch (cc): 0, .pf, 15, "no", 0.03, 0.45, 0.01, 0.35, 0.14, 600 .pp = To PointProcess selectObject: .sound, .pitch, .pp .voice_report$ = Voice report: 0, 0, 75, 600, 1.3, 1.6, 0.03, 0.45 .jitter = extractNumber (.voice_report$, "Jitter (local):") .shimmer = extractNumber (.voice_report$, "Shimmer (local):") removeObject: .pitch, .pp endproc # select first sound, determine pitch floor, and assign it to global variable # pitch_floor = ... @jitter_shimmer: pitch_floor # select second sound, determine pitch floor, and assign it to global variable # pitch_floor = ... @jitter_shimmer: pitch_floor

In accordance with our recently cultivated preference for local variables we use a local variable (.pf) for the argument. To call the procedure it is now required to pass an argument. This is done by adding the required value after a colon (@jitter_shimmer: pitch_floor). Note that a value is handed over (the content of the variable) not the variable itself; it's also possible to call the procedure with a literal value (e.g. @jitter_shimmer: 180) or the result of a formula (e.g. @jitter_shimmer: pitch_floor - 20). In another script you only need to adapt the procedure call (e.g. @jitter_shimmer: pitchFloor), not the procedure itself.

Conclusion: Arguments serve to make procedures more universal and more independent by improving encapsulation, same as local variables. If you consistently use arguments and local variables you can—in the course of your scripting career—assemble a library of handy procedures. Collect them all in one file, e.g. my_praat_library.praat, and have them ready to asssit in a script by simply including the library: include /path/to/my_praat_library.praat (see Praat Help).

Practical example

If you still have a little concentration left, let's have a look at a fully functional example. In an editor script I want the user to choose a window shape and then create and display a broadband and a narrowband spectrum, calculated at the cursor position.

First, I assign some default values (if desired, the input form could be extended to include the view range selection). Next, I make sure that nothing is selected and assign the position of the cursor to a variable to be able to restore it later. Then I ask the user to choose a window shape. Finally, I call the procedure that handles spectrum creation and display twice.

# defaults window_shape = 1 left_View_range = 0 right_View_range = 5000 # we don't do selections s = Get selection length if s <> 0 exitScript: "Get rid of the selection and try again." endif # get cursor position c = Get cursor beginPause: "Choose a window shape" optionMenu: "Window shape:", window_shape option: "Hanning" option: "Hamming" option: "Kaiser1" option: "Kaiser2" clicked = endPause: "Cancel", "OK", 2, 1 if clicked = 1 exitScript () endif # broadband spectrum @showSpectrum: 0.005, window_shape$, left_View_range, right_View_range, c # narrowband spectrum @showSpectrum: 0.030, window_shape$, left_View_range, right_View_range, c procedure showSpectrum: .wl, .ws$, .lvr, .rvr, .c # generate fft spectrum and apply view range (zoom) # # arguments: # .wl (numeric) --> window length in seconds # .ws$ (string) --> window shape # .lvr (numeric) --> lower limit of view range in Hz # .rvr (numeric) --> upper limit of view range in Hz # .c (numeric) --> original cursor position in seconds Move start of selection by: .wl * -0.5 Move end of selection by: .wl * 0.5 .extsnd = Extract selected sound (windowed): .ws$ + "-" + fixed$ (.wl*1000,0), .ws$, 1, "no" Move cursor to: .c endeditor selectObject: .extsnd .sl_id = To Spectrum: "yes" removeObject: .extsnd View & Edit editor: .sl_id Zoom: .lvr, .rvr endeditor editor endproc

Looking at the procedure, the first thing you'll notice is that procedures can accept more than one argument. My procedure requires 5 arguments, which are assigned to local variables. Lists of arguments are separated by comma, in the definition as well as in the call. And of course, the order of arguments must be identical in the definition as well as in the call! The comments inside the procedure give a short description of the arguments. Both calls to the procedure have a literal value as first argument and some variables as arguments 2 to 5. The rest of the procedure is straight forward:

  1. Create a selection around the cursor.
  2. Extract selection using the choosen window shape. I make an effort with the designation of the extracted sound object although it is removed shortly after. But the designation is inherited by the spectrum object which is not removed, so I want it to have a name that makes some sense to the user. The pattern .ws$ + "-" + fixed$ (.wl*1000,0) combines the window shape with the window length in milliseconds, e.g. Hanning-5 (broadband) or Hanning-30 (narrowband).
  3. Restore cursor position.
  4. Switch to shell environment.
  5. Create spectrum object.
  6. Display spectrum object, switch to environment of spectrum editor and zoom in.
  7. Switch back to environment of the editor in which the script was started.

Next: Session 14: Example