jMax IRCAM - Centre Georges Pompidou

Objects



Objects

From the patch programmer point of view, an object is the elementary unit he use to build up a MAX program; they are the equivalent to basic language statement and definitions in a standard programming language.

In order to execute this program, jMax, as its predecessors, use an object oriented interpreter; each statement/definition is represented with an C object implemented using what we call the FTS message system, that have features that resemble from some point of view some of the characteristic of some object oriented languages (in the old time when you called a generic function by sending a message to an object).

"Max" messages are mapped to messages in messages to these objects, and executed by executing these objects methods.

This cover primitive jMax objects like "int" or "float"; but currently the jMax language is a lot richer, including the definition of new objects in terms of the language itself (template and old abstractions), variables and expressions and so on.

So when a user type an object description in an object box, the result is not always the creation of a single object, but actually can include much more work, like loading files, redefining other objects and so on; so there are two meaning in the system for creating an object, one is to process a language level object description and do all the needed work to add this statement to the program (implemented by the C function fts_eval_object_description, and actually instantiating an instance of a class of the FTS message system (implemented by the C function fts_object_new); these two functions are documented below.

Changing and deleting an object require also that all effects tied to variables are propagated in the right ways, so also these functionalities need to be explained.

One non-user visible mechanism that jMax define is the object doctor, essentially a macro processor for object description where macros are defined as C functions, and can do invisible or visible transformation on objects; quite usefull, for example, to keep compatibility between different version of objects by automatically translating the old object syntax in the new syntax.

Creating an FTS message system instance

It is done by the fts_object_new function.

The object creation is pretty straighforward; including a call to the class discrimination mechanism to find the right class, allocating and initializing the system part of the object, adding it to the container patcher, and calling the object initialization (the init method).

Things worth to note:

  • In jMax, objects know in which patcher they are; they can refer to the context of the patcher for various purposes, notably variable handling.
  • Patcher are not handled as standard objects, and always assigned to the same class; see the patcher chapter for details.
  • The init message is alway sent with all the checks active; see the message chapter for details on optional run time checks.
  • While the object include in its state its description (i.e. the text given by the user parsed as a list of atoms) this function do not initialize it; the description is seen has an high level functionalities, and is handled in the language level object creation.

Creating a jMax object

The language work is done in the fts_eval_object_description function; given a description, a number of things in the right order need to be done.
  1. Variable preparation
    If the object description define a variable ("foo : const 10") syntax, the variable need to be defined, in order to inform the variable propagation algorithm that we are adding a new variable in the local context; this action can have quite a number of side effects; note that the variable is defined before the object is created, so we don't have a value for the variable; the variable will be set later; this mechanism is needed to prevent loops in variable value propagation; see the chapter on variables for details.

  2. Doctor
    We check now if there is a object doctor (a C coded macro) registered on the class name of this object; we do it here to give the macro a chance to change the object before the expression evaluation; but, since also the class name can be the result of an expression evaluation, we will need to retry this after the expression evaluation. See the paragraph on object doctors for details on what a doctor can actually do and how.

  3. Expression evaluation
    Taken away the variable definition (if any) the rest of the description is evaluated by the expression evaluation; every expression is subtituted by its value; in the process, the expression evaluator produce a list of the variable used in this evaluation, and a list of name/value pairs corresponding to the assignement syntax at the end of the description (corresponding to named variables for templates and patchers); see the expression chapter for details on expression evaluation.

  4. Doctor (II)
    We re-check for doctors after the expression evaluation, for the reasons explained above.

  5. Explicit templates
    We check if the class name correspond to an explicitly declared template, i.e. currently declared with the tcl macro template. Doing this before trying a standard FTS class allow an explictly declared template to overwrite a C written class, for example a library class.

  6. Explicit Abstractions
    If we hav'nt created the object yet, we check if the class name correspond to an explicitly declared abstraction, i.e. currently declared with the tcl macro abstraction. Doing this before trying a standard FTS class allow an explictly declared template to overwrite a C written class, for example a library class. abstractions are the Max 0.26 and FTS 1.5 abstraction mechanism; jMax can read old abstractions, but cannot save new ones.

  7. Standard FTS objects
    If we have not created the object yet, the fts_object_new function is called, to build a standard FTS object if the right class exists; see above.

  8. Template on a declared template Path
    If we have not created the object yet, we check if a template with the correct name exists in one of the template path declared (currently template path are declared with the templateDirectory tcl command). Note that template on a template path do not overwrite standard FTS class; the reason is a matter of loading perfomance: if a template on a template path would overwrite an FTS class, we had to look for the template file before looking for the FST class for all the objects instantiations, with a fenomenal degradation in loading performance (a file system call for every template path for every object loaded).

  9. Abstractions on a declared abstraction Path
    If we have not created the object yet, we check if a abstraction with the correct name exists in one of the abstraction path declared (currently abstraction path are declared with the abstractionDirectory tcl command). As for templates, an abstraction on an abstraction path do not overwrite standard FTS class.

  10. Error object
    If we have not created the object yet, create a fake error object; an error object is a special object that work as a place holder for all the connections, and have a special error properties for user interface purposes; see the Error object chapter for details.

  11. Check the variable value
    In case the description define a variable, check if the object can actually define a variable, i.e. do export a state property; if it cannot, substitute the object with an error object.

  12. Set the object description
    If the previous object creation process have not set the object description, assign it; for example, a doctor can have translated on the fly an object to another one, producing also the new description, so in this case we should not set the original one.

  13. Register itself as user of the used variables
    Register the created object as user of the variable used in the expression evaluation; this links will allow the object to be redefined if some of these variables change values.

  14. Handle the assignements
    Get the expression assignements and assign them to internal variables in case of patchers or templates, or to object properties in case of standard objects.

  15. Restore of the variable value
    Now that the object exists, the state property value can be assigned to the defined variable (if any); this action will have as effect the redefinition of all the objects depending on this variable.

Of course, the code have also to deal with a number of error checks in between all this phases.

Deleting an Object

Deleting an object (function fts_object_delete is a multiple phases operation; first, we reset the object (function fts_object_reset), i.e. we take it away from the variable propagation network; if this object define a variable, it is undefined, and all the objects depending on it are recomputed; and the object is removed from the user list of the variables referred during its instantiation. Second, the delete message is sent to the object, so that the object can free its resources.
Third the object is removed from the list having property changes (function fts_object_reset_changed); to keep this list consistent (otherwise, deleting an object having a pending update would cause a crash).
Finally the object is freed (function fts_object_free) all its connections are deleted, system structures are updated (patcher object, id table and so on), its private data structure are freed. Deleting an object can optionally be done without destroying the client counterpart, if any; this feature is important when redefining/recomputing an object, because we want to leave the object identity intact on the client side.

Redefining an Object

In the message system, we talk about two slightly different concepts, recomputing an object, and redefining an object.

To recompute (function fts_object_recompute) an object means to redefine it with the same description, taking care of communicating the modification to the client, because the recomputing is always a server initiative, tied to variable propagation; recomputing is used when a variable the object depend on change value; its implementation release the object edited data (value of the data property), i.e. close any editor opened on the object "content", redefine the object using the object description as new definition, and finally upload the kernel properties to the client if needed.

To redefine an object (function fts_object_redefine) means to substitute it with a new one, with potentially a different description in the patcher, without telling the other objects or the client; connections are moved to the new one and id are kept.

Redefining patchers is done with a different strategy, to avoid loosing their content; see below for details.

The object redifinition algorithm is the following:

  1. If the new object description define the same variable as the old one the variable is suspended: essentially this means that while the variable name does still exists in its scope, any access to its value is illegal; suspending a variable is an essential concept in the variable propagation mechanism, see the relative chapter.

  2. If the new object keep the same id, update the object table to reflect the new binding of the id.

  3. As during an object delete, remove the old object from the variable propagation structure.

  4. Prepare for data trasfer if needed, send the delete message otherwise.

  5. Build the new object, using the standard fts_eval_object_description function.

  6. Fix the internal structures of the loading vm: if we are loading a jMax file, the vm may refer the object being redefined (in the object table); the references are transparently converted to the new object.

  7. If the new object one is an error object, set its number of inlets and outlets to be equal to the ones of the old object.

  8. Copy standard properties (like x and y position) from the old object to the new one.

  9. Offer the two objects a chance to exchange data by means of a redefining message, only if the old object defined a method for the release message.

  10. Move all the connections from the old object to the new one, keeping their client identity if any.

  11. Complete the deleting of the old object, by calling fts_object_free.

Doctors

An object doctor is a macro expansor for object definition, written as a C function; a specific object doctor is bound to a class name, and invoked during the object creation process.

We way identify three kinds of doctors:

Invisible doctors.
An invisible doctor produce a macro expansion that is not visible to the user (and so not persistent); the expanded object keeps the same description as the original; an object doctor of this kind usually produce the expanded object description starting from the original description, create the new object by calling fts_eval_object_description and then remove the object description of the created object using the fts_object_reset_description; this will cause the object creation function to install the original description on the object; the argument is actually implemented as an object doctor producing a const object.

Visible doctor.
A visible doctor produce a user visible and persistent transformation of an object; this trasformation is usefull in case like automatically updating a patch to a different syntax, in case of object changes. An example of use if for example the automatic translation of an old version of the comment object to the current version.

Special purpose
A doctor have a lot of freedom in trapping the object creation mechanism; freedom that can be quite usefull, and of course dangerous, in some special situation; for example, an object doctor can instantiate an object calling directly fts_make_objecy, to avoid recursive calls to himself and expression evaluation; this technique is used to transform old instances of exp to the new syntax, prevening any interaction with the new expression mechanism.

Note: there is a conceptual bug that should be fixed in the way doctors work: as said above, a doctor can be called before or after the evaluation of the expressions defining the object; but, the doctor function have no way to know if the expressions has been evalutated or not, so it is not clear if the fts_eval_object_description function or the fts_object_new should be called; calling the first can produce inespected results when the result of the previous evaluation is still an expression; calling the second can fail because the expressions have not been evaluated; the doctor mechanism should be refined to give more information to the doctor function .

Patchers

Patchers are very special objects in FTS; their main characteristic is that they can dynamically change of class (not metaclass), so they can change dynamically their number of inlets and outlets; in general a patcher cannot be redefined by reinstantiation, in order to avoid loosing their content; also, patchers represent contexts in different case, like update handling and variable resolution; all this require a dedicated support right in the kernel of the message system.

Patchers Inlet and Outlets

Main role of a patcher is to present a group object as a single entity in the parent patcher; in order to allow to communicate with its content, the patcher have inlets and outlets, that are mapped to inlet and outlet objects inside the patcher.

This mapping is handled by the patcher itself; the inlet/outlet object are not a lot more than placeholders for connections, rerouting, together with the patcher object itself, messages from or to the outside of the patcher.

A patcher is always instantiated without inlets and outlets; then, adding inlets and outlets objects will automatically change its number of inlet and outlet; a new inlet/outlet object can be instantiated with a given position, or can automatically require the next available position; changing the number of inlets or outlets dynamically require from one side finding out the new class instance, on the other side change the object system structures to reflect the new number of inlets/outlets; this work is done in functions fts_patcher_redefine_number_of_inlets and fts_patcher_redefine_number_of_outlets.

When edited, inlet and outlet objects are not redefined; they are just repositioned, and the patcher updated.

Inlet and outlet objects require special handling for loading a .pat file; they are loaded inactive, in the sense they are stored in the patcher but the patcher is not redefined, and only when the whole patcher has been read, the inlets and outlets are analized and sorted starting from their x position and really positioned in the patcher.

Patcher Variables

As objects, patchers can now define a variable; the variable is bound to the patcher itself, i.e. to a variable scope; this is made to allow composed name and inter-scope access for variables; note that the variable value propagation engine do not yet support this feature.

Also, in order to parametrize the patcher content, a patcher can define a number of internal variables; the array variable args, that include a number of positional arguments, and a number of keyword argument. The variable are defined and handled with techniques similar to those of objects.

Patcher Redefinitions

Patchers have a special redefine strategy: since we don't want its content to be lost; also, note that changing the description of a patcher do not change its number of inlets/outlets, that is instead changed by adding inlets/outlet objects inside the patcher; the patcher redefinition algorithm (function fts_patcher_redefine) is the following:

  1. Set the patcher description to the new decription.
  2. Handle variable definition: if the patcher defined a variable before the redefinition, and do not define the same variable now, undefine the old variable; otherwise suspend it; define the new one if any.
  3. Suspend the old argument variables.
  4. Eval the patcher arguments expression.
  5. Restore the argument variables, either positional or keyword, using the result of the expression evaluation.
  6. Register the patcher as user of the variables accessed during the expression evaluation.
  7. Undefine all the variable that are still suspended, i.e. old key and positional arguments that have not been redefined.
  8. Restore the variable the patcher define, if any.
  9. Inform the UI of the changes.

Patchers and templates, abstractions, error objects

Given their ability to change number of inlets, patchers are used to implement abstractions, templates and error objects; this means that all they integrate the house keeping code for template instances, for example.

Actually, a template or an abstraction consists precisely of a patcher; the argument passing mechanism of templates is actually the same mechanism that allow a patcher to define internal variables; the template specific code regard only the mapping of names to files, the loading of those files and the automatic reinstantiation of templates during editing; see the relative chapter for details.

Error objects need to emulate input/output behaviour of other objects; this is why they are actually implemented with patchers; a internal flag prevent some of the operation to be done on a patcher that is actually an error object.

Content Editing

In order to export the patcher content to a client editor, patchers define a data property, that is a patcherdata used to export the content of a patcher to the application layer; actually, a patcher editor is an editor of patcherdata; this features can be blocked with a special property, to make the patcher not editable.

Other features

Patchers directly support a number of extra features:

  • Load bangs: the load init message is automatically propagated to its content; the mechanism guarantee that the inner patchers are initialized before the standard objects.
  • Support for find: look for atoms occurencies in a patcher content, and recursively in all its subpatchers.
  • Support for find error: look for error objects in its content and recusively in all its subpatchers.
  • Deleting an patcher: patchers can recursively delete their content.
  • Update control: a patcher keep track of open editors, and this is used to enable/disable updates from its content.
  • Support for blips, i.e. context dependent messages usually shown in the first ancersto open patcher status line.

Properties

Object properties are a system, and not a language, feature; essentially consists of associating to each object a dynamic associative list mapping a symbol to a single atom value, so that each part of the system can use properties as block-notes during computation about objects; they are currently used to implement all the language features different from the object description; for example, position and color (since we have a visual language, these are language and not ui feature).

Properties, together with object description, are the basic FTS mechanism for persistency: only escription and properties of an object are saved to a file; properties are also used to represent those dynamical characteristic of an user interface object that are affect the interface at run time; see the update mechanism description.

Properties are stored/retrieved at three level: explicitly at the object level, explicitly at the class level, and implicitly thru class daemons; a daemon is a function that can override the standard system behaviour for setting/getting a property. When getting the property from an object, the system first check if the property has been explictly set on the object, then check the class property as a default value, and if not yet found, check if there is daemon available to compute this property on the fly; also when setting an object property the system check if there is a daemon for setting the property; daemons can be also define for the remove property operation.

The property implementation is straightforward: they are stored in an associative list for each object, or in the class, and the class have a unique daemon list with tags for the action type.

Currently there is no way to define global daemons, i.e. deamons that are applied for all the objects for a given property; this can come handy for example for some system wide or UI relative property (like for example a property like is_dsp)

Currently, the set of properties of an object save to a patcher file is hardwired; the set of properties uploaded to the client for editing is fixed, but each object can define some more with a special method. This is not very correct; each object should define its own persistent and uploaded properties; in this way the kernel handle a group of properties that are actually dependent on the objects.

The FTS kernel define a number of properties; the following table include all the properties the kernel use; some of the properties are applicable to all objects, or may just to one or two.

Kernel Defined Properties
Property Name Meaning Automatically
Uploaded
Saved
name Name of the object.
Used to in converting .pat explode
N N
ninlets Number of object inlets Y Y
noutlets Number of object outlets Y Y
x object x position Y Y
y object y position Y Y
width object width Y Y
height object height Y Y
wx object window x position Y Y
wy object window y position Y Y
wh object window height Y Y
ww object window width Y Y
font Object font Y Y
fontSize Object font size Y Y
color Object Color (for buttons) Y Y
comment Comment Text (comment objects) Y Y
layer z position of an object Y Y
max_value Max value (for sliders) Y Y
min_value Min value (for sliders) Y Y
error If not zero, this is object have an error Y N
error_description textual description of an error Y N
no_upload if set, the object cannot be uploaded N N
state If set, its value can be assigned
to the variable defined by this object
N N
patcher_type the type of a patcher
(template/abstraction/standard patcher)
Y N
data if set, its value will be edited
when the object is double clicked
its value must be an fts_data_t
N N
filename The file a patcher has been loaded from
currently used for autosave
N N

Error Objects

With the term error objects we actually identify two different things: a normal object that is, in its own code, signalling an initialization error to the user by becoming red, and a real error object, i.e. a place holder for an object that the message system could not instantiate at all for some reason.

In the first case, we just have an object that set the error and error_description properties properly, possibly using the fts_object_set_error function.

The error object system define also a call (fts_recompute_errors and fts_do_recompute_errors) to try to recompute all the objects with errors; this is done when some big environmental changes happen (for example, a new template directory is added), to give a chance to fix configuration errors without reloading the edited patches.

Related Files

  • objects.c
  • objects.h
  • doctor.c
  • doctor.h
  • patcher.c
  • patcher.h
  • properties.c
  • properties.h
  • errobj.c
  • errobj.h