JavaScript Tutorial 2: JavaScript Scripting
Introduction
With the Max js object, it’s possible to use JavaScript code to perform patcher scripting, where you can create Max objects in a patcher dynamically, setting their properties, sending them messages, and making connections between them. JavaScript allows you to use procedural code to generate patcher elements in ways that may be more difficult to do through messages to the thispatcher object (the other way to automatically create Max objects in a patcher). This Tutorial covers how to create and delete objects and connections in a Max patch through custom methods written in JavaScript, as well as to show how to use methods to handle custom messages coming from the patcher.
To open the tutorial patch please open 02jJavascriptScripting.maxpat
from the the zip archive, which is available for download at the top of this page.
Patcher Scripting with JavaScript
When you initially open the tutorial patch, you will see a largely empty patcher with a js object in the lower part of the patcher window. The js object has loaded a JavaScript source file called ‘autosurface.js’, which is located in the same folder as the Tutorial patch.
The js object is configured to send numbers to a MIDI output device (using the makenote and noteout objects). It also has a right outlet sending values to the right inlet of the pack object driving messages to a multislider object. In addition, our js object has a number of objects connected to its inlet. A metro object is connected to our js object, as are two message boxes that will send the messages sliders $1
and reverse $1
, where $1
in each case is the value present in the number box connected to them.
From the patch layout, we can infer that the JavaScript code in our js object should have at least three functions, for bang
, sliders
, and reverse
. It actually has one more, which will become apparent when we use the patch.
Patch Auto-Generation
Select the number box attached to the message box containing the sliders $1
message. Type in or scroll to the number 5
, and watch what happens. Change the value in the number box. Try setting it to a large number (like 50
).
Set it to 0
, and see what happens.
In response to our sliders $
1 message, our js object dynamically creates Max objects and connections through scripting. It creates pairs of ctlin and slider objects to match the number of sliders you request through the message to the js object. Furthermore, it creates a funnel object with the appropriate number of inlets for the slider objects and makes the appropriate connections between them. The funnel object is then connected to our js object, allowing the values generated by the sliders to be used by our JavaScript code as well.
As you create sliders, note that the ctlin objects are automatically numbered to listen to incrementing MIDI controller numbers. As a result, a MIDI control surface that sends MIDI continuous control values on multiple controller numbers will send values to independent slider objects. Also, note than when you decrement the number of sliders, the excess objects will disappear (actually, everything disappears and is recreated again). If you set the number of sliders to 0
, all the script-created objects (including the funnel) will be deleted from the patch.
Set the number of sliders to something modest, such as 5
. Change the values in the slider objects, either by clicking on them or by using a MIDI controller input. Turn on the metro object by clicking the toggle attached to it. The values in the slider objects should come out of the js object in turn, creating a sequence of MIDI notes. Double-click the noteout object to select a valid MIDI synthesizer, and you should hear them.
The multislider object to the right of the patch will give you a running display of the current note out of our sequencer, set at its appropriate position in the sequence.
Click the toggle attached to the message box containing the message reverse $1
. Note that the order in which the slider values are sequenced is now backwards. Our multislider display runs backwards as well.
In brief, our js object dynamically creates a scalable MIDI control surface (with ctlin and slider objects), and uses those objects’ values to create a simple MIDI step sequencer. The number of sliders created by our JavaScript code determines the length of the sequence.
Turn off both toggle objects, stopping the sequence and putting the sequencer transport back into ‘forward’ mode. Let’s look at the code for our js object.
The Global Block: Arrays and Maxobjs
Double-click the js object in the Tutorial patch. The code for ‘autosurface.js’ should appear. At the top of the code should be the familiar comment block, explaining what the script does. Below that we should see our global code statements:
// inlets and outlets
inlets = 1;
outlets = 2;
// global variables and arrays
var numsliders = 0;
var seqcounter = 0;
var thereverse = 0;
var thevalues = new Array(128);
// Maxobj variables for scripting
var controlin = new Array(128);
var thesliders = new Array(128);
var thefunnel;
As we saw in the previous tutorial, our inlets and outlets at the top of the code tell js how many inlets and outlets we want in our object.
The following block of code defines some variables that our JavaScript code will need to use globally. These variables include: numsliders
: Stores how many ‘sliders’ (ctlin and slider pairs) we have in our patch. This is set by the sliders
message to our js object. seqcounter
: Stores the current point in our sequence. This is driven by the metro object in our patch, and therefore is changed by a bang()
method in our code. thereverse
: Sets whether or not our sequencer is running backwards. This is set by the reverse
message to our js object. thevalues
: An array (see below) of values reflecting the state of the slider objects in our patch. The funnel object in our patch sets these values by sending lists to our object.
The new Array()
constructor creates arrays in JavaScript. The array variable thevalues
, above, has 128 elements, which are accessed by bracket notation following the array name, e.g.:
k = thevalues[5];
will set the variable k
to the value of the sixth element (starting from 0
) of the array thevalues
.
thevalues[n] = 55;
will set the element n
of the our array thevalues
to 55
.
Note that JavaScript treats Arrays as objects, so that:
k = thevalues.length;
will set the variable k
to the number of elements in the array thevalues
. For more information on this, consult any good JavaScript reference.
After our variable declarations, we have variables that we will use to reference dynamically created objects in our Max patch. These variable names are used internally in our JavaScript code so that we can create, connect, delete, and modify objects all through properties to these objects. Objects in js that refer to Max objects in a patcher are referred to as Maxobjs. We have the following Maxobj variables in our script: controlin
: An array of Maxobjs that refer to the ctlin objects in our patch. thesliders
: An array of Maxobjs that refer to the slider objects in our patch. thefunnel
: A Maxobj which references the funnel object in our patch.
Note that there is no difference in JavaScript variable declaration with relation to the type of value that the variable stores; integers, floats, strings, and objects are all considered equivalent when declaring a variable. Similarly, arrays are defined simply to refer to quantity of information, rather than what type of information will be stored in them. Similarly, JavaScript will correctly type variables following a calculation, e.g.:
x = 4/2;
will set the variable x
to 2
(an integer), whereas:
x = 3/2;
will set the variable x
to 1.5
(a floating-point value). Variables can switch types dynamically throughout their existence. This use of untyped variables only exists within the JavaScript environment, however, which is why we still need independent methods (msg_int()
and msg_float()
) to deal with differently typed numbers coming in from Max.
We will use various properties of the Maxobj object class to perform our scripting, all of which is accomplished by a single function: our sliders()
method.
Arguments, Agreements…
Our js object responds to the sliders
message via a method contained in the sliders()
function (remember that the function name typically matches the message you want to trigger that function). Examine the code for the sliders()
function. The comments at the top of each section explain what’s happening at each step in the process:
// sliders -- generates and binds the sliders in the max patch
function sliders(val)
{
if(arguments.length) // bail if no arguments
{
// parse arguments
a = arguments[0];
// safety check for number of sliders
if(a<0) a = 0; // too few sliders, set to 0
if(a>128) a = 128; // too many sliders, set to 128
// out with the old...
if(numsliders) this.patcher.remove(thefunnel); // if we've done this before, get rid of the funnel
for(i=0; i< numsliders;i++) // get rid of the ctlin and slider objects using the old number of sliders
{
this.patcher.remove(controlin[i]);
this.patcher.remove(thesliders[i]);
}
// ...in with the new
numsliders = a; // update our global number of sliders to the new value
if(numsliders) thefunnel = this.patcher.newdefault(300, 300, " funnel ", a); // make the funnel
for(var k=0;k<a;k++) // create the new ctlin and uslider objects, connect them to one another and to the funnel
{
controlin[k] = this.patcher.newdefault(300+(k*100), 50, " ctlin ", k+1);
thesliders[k] = this.patcher.newdefault(300+(k*100), 100, " uslider ");
this.patcher.connect(controlin[k], 0, thesliders[k], 0);
this.patcher.connect(thesliders[k], 0, thefunnel, k);
}
// connect new objects to this js object's inlet
ourself = this.box; // assign a Maxobj to our js object
if (numsliders) this.patcher.connect(thefunnel, 0, ourself, 0); // connect the funnel to us
}
else // complain about arguments
{
post("sliders message needs arguments");
post();
}
}
In pseudo-code, our sliders()
function performs the following steps: Check to see if the arguments for the sliders method are valid. If true… Make sure the number of sliders requested are in a reasonable range (0
- 128
) Delete any objects previously created by our js object. Make the new objects and connect them to one another. Find our js object (see below) and connect our new funnel to it. If false… Post an error message in the Max Console and exit the function.
Our JavaScript code takes advantage of two important features of procedural programming, namely conditional statements (if…else…) and iteration (for() loops). If you’ve used another programming language such as C or Java, you should find these constructions familiar. A JavaScript reference will help you with the specifics.
One of the first things we do in our sliders()
function is check to see what the arguments were to the sliders message sent in from the patcher. We do this by checking the arguments property of the function itself, e.g.:
if(arguments.length) {
// some code here
}
will execute the code between the braces only if there are a non-zero number of arguments to the message that called the function. Otherwise, that part of the code will be ignored. Similarly, you can access the arguments by number as an array:
a = arguments[0];
will assign the variable a
to the value stored in the first argument of the message. In our case, this refers to the number of sliders we want to create.
Object Creation and Maintenance
From the perspective of using js for object creation in Max, the Maxobj class allows us to use our object variables to create, connect, and destroy objects. This is done by first accessing the Patcher object, which is a JavaScript representation of our Max patch. The statement:
this.patcher.remove(thefunnel)
tells js to find a Maxobj called thefunnel
in the Patcher called this
(which is always the patcher containing the js object) and delete it. The ‘this’ in the statement is actually optional, but it’s worth noting that you can use JavaScript to control objects in patches other than the one in which the js object resides.
To create an object, we assign a variable to a new Maxobj created by the Patcher:
thefunnel = this.patcher.newdefault(300, 300, " funnel ", a);
In this case, the Maxobj thefunnel
is created to be a default object at coordinates 300
by 300
on the patcher window. The object’s class is set to funnel, with the object’s arguments set to whatever is contained in the variable a
.
Note: the newdefault()
method to the Patcher object creates a new object just as if you had created it manually from the palette or patcher contextual menu. This simplifies scripting substantially. If you wish to specify all the object parameters (object width, flags, etc.) you can use the newobject()
method instead.
Connections are made by taking two Maxobjs and linking them using the connect()
method to a Patcher object, e.g.:
this.patcher.connect(thesliders[5], 0, thefunnel, 5)
will connect the leftmost (0) outlet of the sixth Maxobj in the array thesliders
to the sixth inlet of the Maxobject thefunnel
. Remember that numbering starts at 0
for both arrays and inlet/outlet numbers.
We use iteration and arrays to create multiple objects at once, for example:
for(k=0;k<8;k++)
{
controlin[k] = this.patcher.newdefault(300+(k*100), 50, " ctlin ", k+1);
thesliders[k] = this.patcher.newdefault(300+(k*100), 100, " slider ");
this.patcher.connect(controlin[k], 0, thesliders[k], 0);
this.patcher.connect(thesliders[k], 0, thefunnel, k);
}
will automatically generate 8
ctlin and slider objects spaced 50
pixels apart on the patcher window (starting at horizontal coordinate 300
), connect them to one another, and then connect them to the funnel object referenced by thefunnel
. Note that the variable k
in our JavaScript code is never declared, since we only use it as a local variable (in the sliders()
function) and re-initialize it every time that function is called. In our actual JavaScript code in the Tutorial patch, the number 8
is replaced by the local variable a
, which represents the number of sliders we want to create.
Finding Ourself in All of This
One important thing we accomplish in our sliders()
method is the connection of the JavaScript-created funnel object to our js object’s inlet. However, our js object was created by hand, not by our JavaScript code (this would be impossible, if you think about it). How do we bind a Maxobj to an object that was created independently of a JavaScript program?
ourself = this.box; // assign a Maxobj to our js object
The ‘box’ property of our patcher returns a Maxobj referring to our js object itself! We then take the variable ourself and assign it to our js object. This allows us to make connections to the object containing our JavaScript code.
We then connect our funnel object to our js object using our newly assigned Maxobj ourself:
this.patcher.connect(thefunnel, 0, ourself, 0);
Other Methods
The js object in this Tutorial doesn’t just create and connect a MIDI control surface; it also reacts to messages from the control surface as well as other messages from the Max patcher. Open up the source code for the js object in the Tutorial patch again, and look for the function called list()
:
// list -- read from the created funnel object
function list(val)
{
if(arguments.length==2)
{
thevalues[arguments[0]] = arguments[1];
}
}
As with our sliders()
function, our list()
function first checks out how many values we’ve sent in from Max, e.g.:
if(arguments.length==2) {}
The funnel object puts out a list corresponding to the number of the inlet receiving the value followed by the value received. For example, the number 55
arriving at the second inlet (which is really inlet number 1) will trigger the list 1 55
from the funnel object. We check to make sure we have two arguments in our message before we proceed in our list()
method, as we use both the values in the list in the function. We use the first argument (which slider we moved) to determine which element of the array thevalues we set to the second argument (the value).
Look at the bang ()
and reverse()
functions in the JavaScript code.
// bang -- steps through sequencer
function bang ()
{
if(seqcounter>=numsliders) // reset sequencer
{
seqcounter = 0;
}
if(thereverse) // read from the array backwards
{
outlet(1, numsliders-seqcounter-1); // send out our location in the sequence
outlet(0, thevalues[numsliders-seqcounter-1]); // send out the current note
}
else // read from the array forwards
{
outlet(1, seqcounter); // sound out our location in the sequence
outlet(0, thevalues[seqcounter]); // send out the current note
}
seqcounter++; // increment the sequence
}
// reverse -- changes sequence direction
function reverse(val)
{
if(arguments.length)
{
thereverse = arguments[0]; // flip it
}
}
Our bang()
method (which in our patch is triggered by a metro object) steps through a sequence of values in a manner analogous to the counter object. The maximum count is set by the number of sliders we have in our patch (defined by numsliders
). The direction of the counting is always upwards, snapping back to 0
when we exceed the number of sliders. The reverse()
function sets a variable (thereverse
) based on the arguments for a reverse
message sent in from Max. This changes the way in which the bang()
method reads from the array (thevalues
) storing the numbers from our control surface of slider objects. Our two outlet()
functions send our current index value out our js object’s right (1
) outlet, followed by the value at that index in the sequence out our js object’s left (0
) outlet. Note that we follow the important Max convention of outputting values from outlets in a right-to-left order. Otherwise, our pack object would be triggered by its left inlet before it receives the value it needs in its right inlet.
The outlet()
function outputs the value at the current index in the sequence out our js object’s left (0
) outlet.
Now that you know how the JavaScript code is working, play with the patch some more. Think about how you would recreate the sequencer using the normal Max table and counter objects.
Summary
The js object offers you a powerful way to create Max patches dynamically in JavaScript. Object creation is accomplished through the assignment of variables to Maxobj objects created by a Patcher object, which represents the patch in JavaScript. The newdefault()
and newobject()
methods allow you to create objects, which can be destroyed by a remove()
method. The connect()
method lets you make patcher connections between Maxobjs in your script. A Maxobj can be assigned to the js object itself through the ‘box’ property to the patcher. When designing JavaScript functions to act as methods for Max messages, the arguments passed with the messages are available through the arguments array from within the function.
Code Listing
// autosurface. js
//
// automatically generate a MIDI control surface with
// visual feedback (sliders), hook it up to a funnel
// object, and use it to drive a simple sequencer.
//
// rld, 5.04
//
// inlets and outlets
inlets = 1;
outlets = 2;
// global variables and arrays
var numsliders = 0;
var seqcounter = 0;
var thereverse = 0;
var thevalues = new Array(128);
// Maxobj variables for scripting
var controlin = new Array(128);
var thesliders = new Array(128);
var thefunnel;
// methods start here
// sliders -- generates and binds the sliders in the max patch
function sliders(val)
{
if(arguments.length) // bail if no arguments
{
// parse arguments
a = arguments[0];
// safety check for number of sliders
if(a<0) a = 0; // too few sliders, set to 0
if(a>128) a = 128; // too many sliders, set to 128
// out with the old...
if(numsliders) this.patcher.remove(thefunnel); // if we've done this before, get rid of the funnel
for(i=0;i<numsliders;i++) // get rid of the ctlin and slider objects using the old number of sliders
{
this.patcher.remove(controlin[i]);
this.patcher.remove(thesliders[i]);
}
// ...in with the new
numsliders = a; // update our global number of sliders to the new value
if(numsliders) thefunnel = this.patcher.newdefault(300, 300, " funnel ", a); // make the funnel
for(k=0;k<a;k++) // create the new ctlin and slider objects, connect them to one another and to the funnel
{
controlin[k] = this.patcher.newdefault(300+(k*100), 50, " ctlin ", k+1);
thesliders[k] = this.patcher.newdefault(300+(k*100), 100, " slider ");
this.patcher.connect(controlin[k], 0, thesliders[k], 0);
this.patcher.connect(thesliders[k], 0, thefunnel, k);
}
// connect new objects to this js object's inlet
ourself = this.box; // assign a Maxobj to our js object
if (numsliders) this.patcher.connect(thefunnel, 0, ourself, 0); // connect the funnel to us
}
else // complain about arguments
{
post("sliders message needs arguments");
post();
}
}
// list -- read from the created funnel object
function list(val)
{
if(arguments.length==2)
{
thevalues[arguments[0]] = arguments[1];
}
// bang -- steps through sequencer
function bang ()
{
if(seqcounter>=numsliders) // reset sequencer
{
seqcounter = 0;
}
if(thereverse) // read from the array backwards
{
outlet(1, numsliders-seqcounter-1); // send out our location in the sequence
outlet(0, thevalues[numsliders-seqcounter-1]); // send out the current note
}
else // read from the array forwards
{
outlet(1, seqcounter); // sound out our location in the sequence
outlet(0, thevalues[seqcounter]); // send out the current note
}
seqcounter++; // increment the sequence
}
// reverse -- changes sequence direction
function reverse(val)
{
if(arguments.length)
{
thereverse = arguments[0]; // flip it
}
}