Recently, I had to make some corrections on a series of TextGrid objects I had lying around. I needed to add a “syllable” tier with boundaries taken from a different, already existing tiers.
The process to make them was fairly trivial, since I knew the structure of the objects very well: the new tier (which should be the third), would be named “syllable” and would have four boundaries, each at the start of one of the even- numbered intervals in the “segment” tier (since all syllables were CV).
With modifications like these, I always like to work first on copies of the objects, so the changes are not final until I am confident I made no mistakes1, and I often begin by writing the script I want assuming it will only have to work once, so that I can worry about the iteration later on.
My first version might look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Copy: extractWord$(selected$(), " ") segment_tier = 4 new_tier = 3 syllables = 3 Insert interval tier: new_tier, "syllable" segment_tier += 1 for i to syllables + 1 start = Get start point: segment_tier, i*2 Insert boundary: 3, start if i <= syllables label$ = Get label of interval: segment_tier, i*2 label$ = Get label of interval: segment_tier, 1+(i*2) Set interval text: 3, 1+i, label$ + label$ endif endfor
That takes care of what I initially wanted to do, and here’s where I’d like to stop. But we’re not done here, because now we need to make it loop through a number of files. This is not difficult, but it can get tedious, specially if we want to do it right.
We need to
save the object IDs into an array that I can loop over, and
make sure we restore the final selection of newly created objects at the end
Since Praat doesn’t have any knowledge of selections, all this has to be done by hand2. The new code, with boilerplate added, would be like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 total_textgrids = numberOfSelected("TextGrid") for i to total_textgrids textgrid[i] = selected(i) endfor for i to total_textgrids selectObject: textgrid[i] new[i] = Copy: extractWord$(selected$(), " ") # Include the rest of the example above endfor nocheck selectObject: undefined for i to total_textgrids plusObject: new[i] endfor
Note that we also had to change the first line of the original snippet, so that now we keep track of what object was created to select it at the end.
That boilerplate works fine, but it also requires that all objects be open in the Object window, which when dealing with large number of files can be troublesome. The alternative would be to change it to use a Strings objects holding the paths to the file names we want, so objects can be read one at a time.
In that case, the script would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 file_list = selected("Strings") total_strings = Get total strings for i to strings selectObject: file_list file_name$ = Get string: i source = Read from file: base_path$ + file_name$ new[i] = Copy: extractWord$(selected$(), " ") # Include the rest of the example above removeObject: source endfor # Include the selection-restoration here
And this is when things start to get messy, because both of these solutions work well… as long as this is exactly what we want to do. This boilerplate we’ve added only works in this scenario: when we are looping through a series of objects, selecting one at a time, and generating a single new file for each.
But what if we wanted to loop over combinations of objects? And what if for each of those combinations we wanted to create a number of different objects? This might sound like a far-fetched example, but this is exactly what happens when you want to extract intervals in a Sound object using a TextGrid annotation. And even if Praat has internal commands to do that, I think it’s clear that it’s not hard to think of scenarios where the boilerplate can get really annoying, really quickly.
Programmers can sometimes sound a bit lazy with all their attempts at automatising tasks. But in fact, laziness is one of the three great virtues of a programmer. It’s what makes us “write labour-saving programs that others will find useful”. Which is exactly what I did.
Let’s get lazier
Enter vieweach, a CPrAN plugin for Praat, whose main objective is to implement versatile and customisable object looping routines. vieweach has many applications, because its objective is to try to cover all the cases in which we might want to iterate over objects, and it tries to cover that by exposing an interface that is customisable and extensible, so that — to borrow from the Llama book — easy things are easy, and hard things are possible.
One of the things that are made easy by vieweach is precisely what we’ve been trying to do. By saving that original script to disk (a current requirement that hopefully will get lifted in the future), we can replace the entire script with boilerplate included by this:
1 2 runScript: preferencesDirectory$ + "/plugin_vieweach/scripts/" + ... "for_each.praat", "my_script.praat", "Don't bundle"
That does everything we talked about above: it loops through all selected objects, runs the script for each, and if at the end of the script the selection has changed, then those new objects are selected automatically at the end.
And if the selection contained TextGrid and Pitch objects, and the script needed to be run with one of each each time? Easy:
1 2 runScript: preferencesDirectory$ + "/plugin_vieweach/scripts/" + ... "for_each.praat", "my_script.praat", "Per type"
The difference between the two commands above is the type of bundling that the iterator is using.
In the first case, it does no bundling, and it simply iterates through each of the selected objects from the first to the last, regardless of what objects they are.
The second line does something similar, but slightly more sophisticated: it also works with the selected objects, but it separates them by object type, and then iterates through combinations of one object of each type. So if the selection had Sound and TextGrid objects, one of each will be selected each time the iterator runs.
What happens if they are not the same number? Then the shorter list of objects loops over, and the iterator still runs once for each item in the longest list. This means that I can use the same process if I want to operate on a number of objects, each of them in combination to a single other.
But what if I want to combine the two, to iterate over sets in which some objects are in disk and some objects are in the Object window? In that case we need to use the third bundling option, which wasn’t shown here: “Use sets”.
vieweach uses another CPrAN plugin called selection for all its selection management and bundling. I won’t go into the details of what selection does (I’ll leave that for another post), but one of the things it does is define “sets” of objects that can reside either in disk or in the Object window.
Sets in disk are represented by Strings objects, much like it’s done traditionally in Praat (although using full paths instead of just the file names), and sets in memory are represented by what are called “selection tables”, which exists in Praat as a Table object which holds the objects’ name, type, ID number, etc.
So all you’d have to do to combine these two is to make a Strings object for your objects in disk, and a Table object for your objects in the Object window, select them, and run the script again telling it to assume that each selected object represents a set of objects, and use those for bundling.
This is just a taster of the kinds of things that can be done with vieweach and the rest of the plugins in CPrAN.
Get involved! And stay tuned for more.