Understanding containers
Container types are implemented as specially delimited strings. Elements are accessed (retrieved,set) with the use of functions, where an integer index argument specifies the desired element to operate with. This is the only way to implement them with the capabilities offered by the Avisynth script language while retaining relative simplicity and atomicity of container types' variables.
Implementation selections
The valid index range was selected to be from zero to number-of-elements minus one, both because it is the more usual case and because it makes coding easier. Furthermore, it was decided to allow negative indices, in the style used by the Python programming language.
Thus, the access functions add number-of-elements to negative indices and then check if resulting index is in the interval [0..number-of-elements); if yes the element is accessed, else an 'index error' occures. This makes easier to access container elements backwards from the end (for example, to access the last element, use an index of -1).
The index is checked during each element or subrange access for containment in the interval [0..number-of-elements); this assures that an out of bound index (a common programming error) will be reported to the user.
In order to implement clip containers a scheme was deviced for copying clips passed to containers in "reserved" global variables and storing these global variables' names into the container.
Rather than having a special type of clip container it was chosen to create a unified implementation of container types which can handle every possible type. For this purpose special handler functions were created to transparently set/retrieve variables of the appropriate type to/from the string representation of them in the container. The actions of the handler depend on the type of operation:
Operation | Handler's actions |
---|---|
Retrieve an element |
Evaluate the element as an expression (by passing it to the Eval() standard Avisynth function). If the element evaluates, return the result of the evaluation. Else, return the element itself (ie the string stored in element). |
Assign to an element |
If the new value is of the clip type, copy it to a newly created "reserved" global variable1 and assign the name of the global variable to the element. Else, assing the result of applying String() to new value. |
1 The variable is created by combining a prefix with the string representation of an integer counter which is incremented every time a call to the creation function is made.
The element delimiter was chosen to be variable and not hard-coded, in order to allow the maximum of flexibility to users. A reasonable default (the comma character) was selected and special globals hold the variables associated with the delimiters. These were made "private" and accessible by special functions in order to aid error-free manipulation of them.
For the same reason the element handler functions were also chosen to be variable and accessible by special functions. However, since this feature is considered to be developer-level, no additional documentation but the source code will be provided.
Impact of selections on functionality
A first consequence of the above setup is that containers can be created by assigning appropriate string expressions to variables, such as in the examples below.
# assume library's defaults for delimiters apply ar1 = "1.2, 3.4, 4.5, 6.7" # whitespace is ignored (allowed) ar2 = "true, false, true" ar3 = "This, is, a, nice, day." # whitespace in strings is NOT ignored # clip arrays can also be directly coded, if elements are globals global clip1 = AVISource(...) global clip2 = AVISource(...) global clip3 = AVISource(...) global clip4 = AVISource(...) ar4 = "clip1,clip2,clip3,clip4"
A second one is that accessing containers' elements is slower than directly accessing variables because:
- Any variable retrieved or stored to a container needs to be transformed from string to the desired type (and vice versa) which results in calling a handler function.
- In order to access an element, a scan of the string for delimiters has to be performed; this is particularly costly when accessing the last elements of an array or recursing backwards over it, from end to start.
However, keep in mind that the extra time needed when parsing a script to produce frames is compensated more than enough from the time gained during the development of the script by the flexibility, ease and speed of coding that containers offer.
Taking into account that this extra time will be in the majority of cases a small fraction of the time needed to process the video streams, most of the time using containers will be the best choice of implementation.
Another consequence is that container variables can contain apart from the usual case of one type of elements, the following:
- Elements of different types, in any order and proportion desired.
- Global variables' names or even arbitrary expressions, as long as they contain only globals (variables and/or functions) and constant values, since Eval() will be called from the handler functions and not from user code).
In fact one can even store entire scripts in a container (if working only with globals) by embedding blocks of code in double quotes and assigning them to the container elements [1].
The following examples illustrate the above.
inclip = AVISource(...) ovclip = AVISource(...) global rect = BlankClip(width=320, height=240, color=$ffffff) mixar = "0, 0, true, rect, 0.8" # mixed arrays can be used as structures # for reducing arguments, specifying presets, etc. clipvar = some_user_filter(inclip, ovclip, mixar) ...
branch = ... # construct an array of size 3 with code blocks # note the unescaped with backslash lines codar = \ "global c = BlankClip(color=$ff00cc) global d = c.Levels(0,1.0,255,0,128)" \ + "#" + \ "global c = BlankClip(color=$00ff00) global d = c.ConvertToYUY2().Tweak(hue=135)" \ + "#" + \ "global c = ColorBars() global d = c.Levels(0,1.4,255,100,200).ConvertToYUY2().Tweak(hue=150)" # because code blocks contain commas, # was used as delimiter # that's why we have to tell AVSLib to use our delimiter ArrayDelimiterSet("#") temp = codar.ArrayGet(branch > 2 ? 0 : (branch < 0 ? 1 : 2) ) ArrayDelimiterReset() # and restore to defaults when finished (see below) new_clip = some_user_filter(d) ...
Be aware though that this functionality is rarely needed; wrapping code to functions or using the container's operator functions provided by AVSLib is a more scallable and easier to understand alternative.
Of course, this added flexibility also means that one must be careful with the types stored in the container, since most of the time uniformity is the desired feature (after all, the major purpose for working with containers is to perform a series of operations to a group of similar objects).
In particular the following note has to be made:
When creating containers with strings one must be aware that due to the evaluation of each string in a retrieve action, there is the possibility of retrieving a global's value instead of the string if the contents of the string are the same with the name of the global.
In addition, the facility provided by Avisynth to allow calling a function without parentheses means that all function names, both standard and user-defined, obey the same rule. This may lead to strange errors since a function called without parentheses tries first to operate on the implicit variable last.
For example:
global animals = 34 species = ArrayCreate("fishes", "birds", "animals", "serpents", "humans") t = species.ArrayGet(2) # t is 34 (an integer) and not "animals"!!
# 'not ' will produce a strange error in ArrayOpValue if the bool package # is loaded due to the existence of the 'Not' function in the package # (lang is case insensitive!) and the fact that functions can be called without parentheses # (which results in "last" implicit variable supplied as argument). a9 = "is ,is ,has ,was " a14 = ArrayOpValue(a9, "not ", "+") # an error pointing that Avisynth forgotten a previously declared # local variable appears
In addition, one must be careful if he/she decides to play with the container's delimiters. Since these are global variables, the effect of setting them to a different value is immediate. The following example demonstrates this issue:
coords = "100, 10, 200, 25, 300, 40, 400, 80" # change delimiter to # to allow a string-array with commas inside. ArrayDelimiterSet("#") subtitles = "Hey friend, how are you?#Fine, thanks!#Nice weather today." # This won't work; the entire coords array will be returned. x0 = coords.ArrayGet(0) # This won't work either; script execution will halt with an 'index error'. y0 = coords.ArrayGet(1) sub1 = subtitles.ArrayGet(0) sub2 = subtitles.ArrayGet(1) ...
The correct way to code in such situations is to organise access to non-standard arrays in blocks and wrap each block with calls to the pair of ArrayDelimiterSet() and ArrayDelimiterReset() functions, as demonstrated by the following example:
coords = "100, 10, 200, 25, 300, 40, 400, 80" # change delimiter to # to allow a string-array with commas inside. subtitles = "Hey friend, how are you?#Fine, thanks!#Nice weather today." x0 = coords.ArrayGet(0) y0 = coords.ArrayGet(1) # wrap blocks of operations on non-standard arrays; # do not access normal arrays inside these blocks. ArrayDelimiterSet("#") sub1 = subtitles.ArrayGet(0) sub2 = subtitles.ArrayGet(1) ArrayDelimiterReset() ...
What containers can do for you
Well, actually nearly everything, since they are a general purpose programming facility. That's why they are a standard element of nearly every programming and scripting language.
A few possibilities specific to Avisynth usage are outlined at the list below. Keep in mind though that this is a very small set; the only limit is imagination.
- Organisation of clips in timelines (ie arrays of clips) and batch application of filters and
any type of processing. For example, in order to combine a collection of titled-at-the-start episodes
into a continuous movie one can use:
- a timeline and an array of titles,
- an animation function inside a custom user function (taking a clip and a string argument) to create a title-clip with a transition effect and return the combination of the effect and the original clip,
- the ArrayOpArrayFunc() function on the timeline and titles arrays with the user function mentioned above,
- the ArraySum() function to join all timeline clips in the final video stream.
- Overlaying of timelines, for example by passing user-function wrappers of Overlay() to ArrayOpArrayFunc().
- Animation on arbitrary curves, with varying opacity and size. See the animate module's source code for an example of this use.
- Rotation of clips accross the vertical or horizontal axes, if animated resizing is organised with an appropriate sequence, an auxiliary video used as a back and FlipVertical() / FlipHorizontal() is applied on one copy of the clip.
- Operations on clip groups such as stacking in rows, columns or matrices, application of masks, etc.
- Creation of preset collections of clips, masks, curves, etc. as global variables / constants for using with Import() in scripts.
See the examples at the container operators tutorial and the source code of the test scripts included in the distribution of AVSLib to get an inshight of real use cases of container types.
Hints for effective use
The following list provides some guidelines for effective use of containers.
- Carefully review the corresponding parts of the library specifications section to avoid exceeding the allowed limits for containers size (in case you use Avisynth 2.5.5 or earlier).
- Try to use negative indices When accessing elements near the end of the container; they result in easier to read code.
- Avoid direct manipulation of containers; try to use only the associated AVSLib functions for interacting with their elements.
- Try to use the associated container operators whenever you want to process a container's elements. This also applies for subranges, since containers are accompanied with a rich set of functions to operate on subranges.
- When creating a recursive function that parses a container type try to code it such that it parses the container forward, from start to end.
- Cache wherever possible the container's number of elements into an integer variable (since determining it requires parsing the entire string each time it is requested).
- Try to use OOP notation; while it is of course a matter of style, it does makes chaining container related functions more easier to write (no need to worry if parentheses are coupled) and also promotes the notion of a container class with properties and methods, which overally enhances code readability.
[1]: Make sure that the code blocks do not generate errors because these will be masked by the try..catch block of the container's retrieve handler and it will appear that the scheme does not work, while it actually does. A hint to debugging: If an error appears inside a code block, only the globals assigned before the error occured will have the correct values. That way you can trace, by examining all associated globals, where the error occured.