The JavaScript-mtr Connection, Part 2


    In the first part of this series, we introduced using JavaScript to take advantage of the new dump dictionary available with the Max multitrack recorder object, mtr. We learned how to import a JavaScript library called mtr-accessor, how the data in mtr-accessor is structured, and how we could perform basic operations on the data.
    The goal of Part 2 of this tutorial is to create a graphical score for our mtr data using the jsui object. Taking inspiration from experimental music notation, we’ll plot data left to right, using different colors, shapes, and vertical positions to represent mtr data.
    An example of my own graphical score, "kitten on the turntable" (title courtesy of Gregory Taylor).
    An example of my own graphical score, "kitten on the turntable" (title courtesy of Gregory Taylor).
    In this tutorial, we’ll touch on:
    1. Where to get started with jsui and the mgraphics system
    2. How we can represent different data types (numbers, arrays, and strings)
    3. How to implement our graphical score in JavaScript

    Getting started with jsui and mgraphics

    For JavaScript graphics in Max, we can use the jsui object. You may find the tutorial “Designing User Interfaces in JavaScript” helpful if you are new to JavaScript and JavaScript graphics in Max. In addition, you may want to bookmark the MGraphics Quick Start Guide as a documentation reference. Of course, the jsui help patcher also provides many examples to give you a taste of what you can do with JavaScript visualizations in Max!

    Representing different data types

    We need to consider that the mtr object can hold more than numbers. (Arrays and strings are fair game!)
    • Numbers: These are straightforward; we can use the numerical value as the y-position.
    • Arrays: We can draw a box where the height is the length of the array.
    • Strings: Since strings have lengths just like arrays (they’re arrays of characters), we can represent them as boxes, too.

    Setting things up

    As in Part 1 of this series, we need to initialize a few global variables and import mtr-accessor.js.
    You might notice a few times in this script where we specify mgraphics.relative_coords. In relative mode (mgraphics.relative_coords = 1) the coordinates of the drawing area are relative to the center of the jsui display and x- and y-coordinates are in the range of -1 to 1. Simply put, this means that you don’t need to worry about the pixel size of the display and jsui will do the scaling math for you. Most of our drawing code uses the relative mode. We’ll explain more about relative and non-relative coordinates as we go through this tutorial.
    autowatch = 1;
    inlets = 1;
    outlets = 0;
    
    var mtr = require("mtr-accessor");
    
    var myMtr;             // holder object for the dictionary contents
    var is_loaded = false; // track whether a valid dictionary has been received
    var show_info = true;  // whether to show text info
    
    // Scaling the jsui window
    var dmin = 0;  // minimum numeric value to graph
    var dmax = 0;  // maximum numeric value to graph
    var dext = 0;  // maximum duration value to graph
    var dlen = 0;  // maximum size of array value length
    
    // Set up mgraphics system
    mgraphics.init();
    mgraphics.relative_coords = 1;
    refresh();

    Reading the data

    We will store the mtr data the same way we did in Part 1. Recall that when we construct the myMtr object we need to pass in the type of data we’re using. This is because the JavaScript Dict object in js and jsui have slightly different structures than a JavaScript object you’d get in a Node4Max environment. If you were to use Node4Max, you would want to pass in an “object” argument. Since we’re using jsui here, however, we pass in the “dict” argument.
    We’ll also use a few helpful functions available from mtr-accessor.js so we can appropriately scale the window size for jsui when we get to the drawing function.
    • myMtr.getLen() gives us the length of the track in ms
    • mtMtr.getMax() gives the size of the largest value across all tracks
    • myMtr.getMin() gives the size of the smallest value across all tracks
    • myMtr.getMaxArr() gives the size of the largest array across all tracks
    The length of the track will be used to scale the window horizontally, and the minimum and maximum values will be used for scaling vertically.
    function dictionary(v) {
        var d = new Dict(v);
    
        // Get all the info we need from the dictionary. For
        // the min, max and ext, we move it to the next largest value
        myMtr = new mtr.Mtr(d, "dict");
    
        dlen = Math.ceil(myMtr.getLen());
        dmax = Math.max(myMtr.getMax(), 1);
        dmin = Math.min(myMtr.getMin(), -1);
        dext = Math.max(myMtr.getMaxArr(), 1);
    
        // We want the min and max to be centered around
        // the vertical center of the display, so we use
        // the largest of the min-max value for scaling
        if (Math.abs(dmin) <= Math.abs(dmax)) {
            dmax = Math.ceil(Math.abs(dmax));
            dmin = dmax * -1;
        } else {
            dmax = Math.ceil(Math.abs(dmin));
            dmin = dmax * -1;
        }
        // refresh the display
        is_loaded = true;
        refresh();
    }

    Utility functions

    There are some small utility functions we will need to set up before we begin drawing.
    In our Max patch, we’ll want to set up a toggle and message so that we can turn info text on and off.
    To handle that, we’ll create a small function that will change the boolean flag show_info.
    function showinfo(v) {
        show_info = v == 1;
        refresh();
    }
    We want the jsui window to be able to handle window resizing. On resizing, we’ll simply refresh the drawing.
    function onresize(w, h) {
        refresh();
    }
    onresize.local = 1; // private
    Finally, we’re going to need to know the aspect ratio of the window (the ratio of width to height) for relative coordinate mode.
    function calcAspect() {
        var width = this.box.rect[2] - this.box.rect[0];
        var height = this.box.rect[3] - this.box.rect[1];
        return width / height;
    }
    calcAspect.local = 1;
    Calculating and knowing the aspect ratio allows us to consider the coordinate system like the following diagram:
    Knowing the aspect ratio means we don't need to know the actual pixel-based height or width of the window
    Knowing the aspect ratio means we don't need to know the actual pixel-based height or width of the window

    Paint and refresh routines

    Now for the meat of our script: the paint function.
    We first set up the local drawing variables for our score like t, x, y, height, and so on.
    function paint() {
        var str;
        var t, x, y;
        var colorcode = 0;
        var aspect = calcAspect();
        var height = box.rect[3] - box.rect[1];
    Then we set up the multipliers needed to scale the drawing to the current visible area.
        var hmult = 1;  // horizontal multiplier
        var vmult = 1;  // vertical multiplier
        if (dext > 0) {
            hmult = (2 * aspect) / dlen;
        }
        if (dmax != 0 || dmin != 0) {
            vmult = 2 / (dmax - dmin);
        }
    What we’ve done here is calculate the fraction of vertical space and horizontal space to create a grid on a [-aspect, aspect] by [-1, 1] coordinate space. In other words, hmult is the horizontal grid spacing, and vmult is the vertical grid spacing. The factor of 2 is necessary since the range is from -1 to 1 (so a distance of 2).
    Now we can start drawing. First we will clear the background and draw a horizontal axis through the middle of the display.
      with (mgraphics) {
          // erase the background
          rectangle(-1.0 * aspect, 1, 2.0 * aspect, 2);
          fill();
    
          // draw zero line - always in the middle of the display
          y = dmin * -1 * vmult - 1;
          set_source_rgb(1, 1, 1);
          move_to(-1.0 * aspect, y);
          line_to(1 * aspect, y);
          stroke();
    From here, we can start iterating through the loaded dictionary, if there is one. For each track, we will designate a new drawing color.
          if (is_loaded) {
                // run through each track in the object
                for (var n = 0; n < myMtr.tracks.length; n++) {
                    // set up a unique but consistent color for each of the tracks
                    colorcode += 500;
                    set_source_rgb(
                        (colorcode % 66) / 66,
                        (colorcode % 88) / 88,
                        (colorcode % 113) / 113
                    );
    Then, for each event, we find the x-coordinate first (the time value). To do this, we start with the value of accum (the “accumulated time”) from the mtr data. To make sure we move by units of the horizontal grid spacing, we multiply accum by hmult. Then to shift the origin of our plot to the left, we have to subtract out the horizontal distance, the aspect ratio.
                    for (var m = 0; m < myMtr.tracks[n].length; m++) {
                        x = myMtr.tracks[n][m].accum * hmult - aspect;
    As discussed earlier, we’ll need to check the data type of the event before we decide how to draw it.
                        t = myMtr.tracks[n][m].type === "number" ? true : false;
    For a number, we take the value of the event and subtract out the minimum y-value (dmin) to get the distance from the bottom of the window. Then we scale it by our vertical grid multiplier. Finally, we subtract off 1 to handle shifting the origin from the center of the screen.
                        if (t) {
                            y = (myMtr.tracks[n][m].value - dmin) * vmult - 1;
    Ellipses are drawn relative to their top left bounding box, but we want the data point to be at the center of the ellipse, so we have to do a little math to shift the x-coordinate right by the radius value (0.025) and the y-coordinate down by the radius value. The final two arguments are the major and minor axis lengths, but since we’re drawing a circle, we use the diameter for both (0.05).
                            ellipse(x - 0.025, y + 0.025, 0.05, 0.05);
                            stroke();
    For the rectangle case, the math is similar. The main difference is that the height of the box is the length of the array or string scaled by the maximum array size.
                        } else {
                            y = myMtr.tracks[n][m].value.length / dext;
                            rectangle(x - 0.025, y + 0.025, 0.05, y);
                            stroke();
                        }
                    }
                }
    Once the data itself is plotted, we can go on to writing the info text on the bottom of the display (if it is turned on). We do this using relative coordinate mode turned off simply because it’s easier to draw text with show_str() using the absolute pixel coordinate system.
               if (show_info) {
                    str = "Length: " + dlen + "  Domain: +/- " + dmax;
    
                    // turn off relative coordinate system for this
                    // function - just makes it easier!
                    relative_coords = 0;
    
                    // set up the color and font
                    set_source_rgb(1, 1, 1);
                    select_font_face("Ariel");
                    set_font_size(9);
    
                    // draw the text in the bottom left corner
                    move_to(2, height - 2);
                    show_text(str);
    
                    // turn the relative coordinate system back on
                    // for the next iteration!
                    relative_coords = 1;
                }
    Finally, we have to handle the case where no dictionary is loaded. In this situation, we can simply write that there is no dictionary loaded, again turning relative coordinates off for ease of plotting.
            } else {
                // no dictionary is loaded, so do a simple display
    
                if (show_info) {
                    str = "No dictionary loaded...";
    
                    // the same procedure as above...
                    relative_coords = 0;
    
                    set_source_rgb(1, 1, 1);
                    select_font_face("Ariel");
                    set_font_size(9);
                    move_to(2, height - 2);
                    show_text(str);
    
                    relative_coords = 1;
                }
            }
        }
    }

    What’s next?

    Now that you’ve seen the power of using js and jsui with mtr, you’re ready to start experimenting. For more fun, try using Node for Max with your mtr data!