Husserl tutorial series (10). Programming pattrstorage with JavaScript

Ernest's icon

It's much easier to program banks and program functions for pattrstorage in JavaScript than wiring Max objects together. On the other hand, Max does not have built-in access to the pattrstorage data, as it does for the dict object. If I were starting from scratch now, I'd bypass pattrstorage and use the dict object instead, sending it's stored values to objects upon recall with pattrhub. However I'd already built a patch for pattrstorage and had a lot of data in it, so this is the first script I made when I needed two pattrstorage objects...and what I learned from making it. which you might find useful for other purposes, particularly the section storing data() in pattrstorage without memory faults, at the end of this tutorial.

This is the 10th tutorial in the Husserl design series. Previous tutorials:

  1. Designing a good LFO in gen~ Codebox: https://cycling74.com/forums/gen~-codebox-tutorial-oscillators-part-one

  2. Resampling: when Average is Better: https://cycling74.com/forums/gen~-codebox-tutorial-oscillators-part-2

  3. Wavetables and Wavesets: https://cycling74.com/forums/gen~-codebox-tutorial-oscillators-part-3

  4. Anti-Aliasing Oscillators: https://cycling74.com/forums/husserl-tutorial-series-part-4-anti-aliasing-oscillators

  5. Implementing Multiphony in Max: https://cycling74.com/forums/implementing-multiphony-in-max

  6. Envelope Followers, Limiters, and Compressors: https://cycling74.com/forums/husserl-tutorial-series-part-6-envelope-followers-limiting-and-compression

  7. Repeating ADSR Envelope in gen~: https://cycling74.com/forums/husserl-tutorials-part-7-repeating-adsr-envelope-in-gen~

  8. JavaScript: the Oddest Programming Language: https://cycling74.com/forums/husserl-tutorial-series-javascript-part-one

  9. JavaScript for the UI, and JSUI:<a href="https://cycling74.com/forums/husserl-tutorial-9-javascript-for-the-ui-and-jsui"> https://cycling74.com/forums/husserl-tutorial-9-javascript-for-the-ui-and-jsui

  10. Programming pattrstorage with JavaScript: https://cycling74.com/forums/husserl-tutorial-series-programming-pattrstorage-with-javascript

  11. Applying gen to MIDI and real-world cases. https://cycling74.com/forums/husserl-tutorial-series-11-applying-gen-to-midi-and-real-world-cases

  12. Custom Voice Allocation. https://cycling74.com/forums/husserl-tutorial-series-12-custom-voice-allocation

JavaScript for pattrstorage: why, and why not

I wanted to be able to store buffer() data in pattrstorage, in order to load and store multibank data for gen~ voices. That was because I had converted a polyphonic design to a multiphonic one by switching almost all of the audio path to share buffer~ data instead of sending messages into gen~ as Params (described in tutorial 5). So far, there are 160 audio-related control values in each of 16 channels, summing to 2,560 values in each of 128 presets, making a data buffer~ of 1,280KB (when saved as an audio file, current file formats add 1KB of sparse word-aligned data for quick OS access, so the buffer~ file is exactly 1,281KB in size) .

JavaScript can peek and poke blocks of buffer~ data, which is not possible with either Max peek~ objects or in gen~, so it seemed an obvious design choice after the size of the patches and subpatches in Max objects got overwhelmingly large, especially as I would otherwise have to create and store 2,560 entries for each preset. The patch uses its one permitted freebang object to save the buffer~ data on exit, then reloads the saved buffer~ data when starting up. Thus import and export operations are not time critical, and very suitable for JavaScript.

It transpires the 32K words of data for all the multi presets was too much to fit in one line of a pattrstorage slot, so I split them into 2,560-value parcels for each preset, and stored them as a string, which as a little more compact because most of the values were integers. Then I needed only one pattr object to contain all the data for each preset. I called it 'mchan.'

When exporting data, the following exportPrograms() function loads all the buffer~ data for each of the 128 presets into a pattr object and stores it:

function exportPrograms(){
    var a = new Array(160);
    var s ="";
    for(k=0; k< 128; k++){
        s = "";
        for(i = 1; i <=16; i++){ 
            a = programs.peek(i, k*160, 160);
            s += a.toString();
        }
        outlet(0, "setstoredvalue", "mchan", k+1, s);
    }
}

When importing a bank, importPrograms() does much the same in reverse. When it fetches the stored value, the pattrstorage object returns an 'mchan []' message with the data in a comma-delimited string. The mchan() function responds by splitting the string into an array, puts it back in the buffer, and incrementing the preset number before calling itself 127 more times:

importPreset = 0;
function importPrograms(){
    importPreset = 1;
    outlet(0, 'getstoredvalue', 'mchan', importPreset);
}
function mchan(s){
    var a = new Array();
    var b = new Array();
    var x = 0;
    if(importPreset >0){
        b = s.split(',');
        for (i = 0; i <16; i++){
            x = i * 159;
            a = b.slice(x, x +160);        
            programs.poke(i +1, (importPreset -1) *160, a);
        }
        if(importPreset < 128){
            importPreset ++;
            outlet(0, 
               'getstoredvalue', 
               'mchan', 
               importPreset
            );
        }else {
            importPreset = 0;
        }
    }
}

Many have already waxed long and lyrical on the technicalities of array/string conversion methods in JavaScript, which you can easily find on the Web already.

Sending messages to objects and getting object values

As of Max v8.2.1, you must declare an array before sending it to an object as a message. Otherwise the script compiles without error, but during execution the call to message() causes a fatal crash (reboot not required, and Max tries to restore the patch properly).

a = new Array(160); // YOU MUST DELCARE 'a' FIRST!

// (you don't need to declare 'a' for this:)
a = buffer.peek(1, 0, 16);
// however, you must make the buffers MORE THAN 32 bits in length,
// regardless of the number of channels, or you will sometimes get 
// persistent crashes when trying to access them from javascript.  

// BUT THIS OTHERWISE CAUSES CRASH!
thispatcher.getnamed('mchan').message(a);  

// (this populates an array without declaring an array first, strangely)
a = thispatcher.getnamed('mchan').getvalueof(); 

The problem with javascript for pattrstorage
The real problem is not really the code syntax but figuring out the cause of functional errors, which could have been due to the rather complex control structures, or something else. For me, it turned out to be something else was the worse problem, hidden in a jungle of possible infinite loops and race conditions.

  • Infinite loops occur because the only way to get the results from pattrstorage are to feed its own output back into the JavaScript, and as many of the functions are reentrant, that can easily cause an infinite loop while you're writing your code.

  • Race conditions occur when the JavaScript invokes an external function that needs to complete before a subsequent JavaScript statement. These can also occur because of 'reentrant' function: functions that can be invoked again while they are still processing a prior invocation. In Max JavaScript, some functions can easily take more than a hundred milliseconds, especially as they are being continually interrupted by audio and video processing. So it's easy for a user to call the same JavaScript function as before, by rapidly pressing a button, for example, while it's still responding to the prior button press....see section storing data() in pattrstorage without memory faults, below, for the gorey details.

Max tries to detect these problems, and it's really pretty good at JavaScript ones, but it's not always successful, and memory corruption from both can cause crashes so severe, the console stops receiving messages even after reopening the patch without trying to salvage its changes, and you are forced to restart your computer. The most obscure and probably frequent reason is in the last section of this tutorial.

  • Tip: always make sure to write lots of things to the console so you can know the memory corruption happened, or you might not realize the problem for some time. Anyway:

So the point of this tutorial is, actually, don't try to use pattrstorage if you are starting from scratch. Use dict instead. But the code listing etc. follows, so you can see what you are getting into if you ignore that advice, or already have a lot in pattrstorage like I did.

The Interface

Husserl provides a chooser list for selecting presets; a text box for editing the preset name; a "dirty" indicator when the controls have been changed so that they no longer are the same as stored; and buttons for adding, removing, and reverting programs; and a menu for initializing, loading, and saving banks.

To make things easier, the pattrstorage object is at the top level, and messages run in a ring from this subpatch into pattrstorage, which sends them back in.

Sending a 'set' message to a chooser object immediately after populating it with 'append' messages doesn't work, because the 'set' message arrives while the screen renderer is still drawing the new list. Therefore in the subpatch above the chooser object, 'set' messages from JavaScript pass through a 'route' object and are deferred. The selected item is then highlighted properly. Other objects in the subpatch move the list up, so selected items don't sit at the subpanel's bottom.

Previously Husserl needed ten subpatches to fit all the Max objects on the screen needed for bank and preset management. Reads, writes, recalls, stores, and deletes for banks and presets sum up to a dozen operations, all of which need to access over half a dozen objects with slightly different messages each time, in different orders, so it was a hodgepodge of connections across multiple levels, and impossible to edit when I needed a second pattrstorage. It's much simpler to edit with JavaScript, which does everything.

The subpatch for the multis is simpler than the one for programs. A routepass object passes the needed pattrstorage messages to javascript The one subpatch is a dialog box, which can display different information strings to the user as cautions and on file failures.

The 'multis' script is ~400 lines, which is listed in sections below. Mostly I've already described how it works. What is different in this code is that the JavaScript needs to know what to do when pattrstorage returns messages for read, write, recall, store, and preset names.

Preset names are particularly awkward, because pattrstorage generates the same message format when a single preset name is requested, as for a list of all preset names. The only difference is that a 'getslotname' message to pattrstorage returns only one message, whereas a 'getslotnamelist' message returns a sequence of identical messages, followed by a 'slotname done' message when all have been sent:

In order for the JavaScript to know what to do when it receives such messages from pattrstorage, each button and menu entry in the below script shares a global variable called "action," which is populated with different values depending on the ongoing task. So when a message arrives from pattrstorage, it does different things depending on the value of the action variable.

Outlets

Before the variables, the top of my Max scripts have my second longest code comment ever, on the 9 outlets. One can name outlets of course, but I find that I change a design without redoing the inspector labeling, so instead I got in the very good habit of labeling I/O with comments.

  • Tip 1: Comment the outlets even if you are lazy about commenting like me. Otherwise, after you get to half a dozen outlets and do something stupid (like me), Max frequently disconnects the outlets to stop our stupidity causing stack overflows and other such things, then, you'll want to know how to connect the outlets back up and get annoyed at yourself about lazy commenting (like me).

  • Tip 2: Don't delete outlets. I don't use two of these outlets for the multi pattrstorage, but I keep the outlet numbers consistent across implementations. Also I forget I renumbered them later and put the wrong numbers in. Generally, be kind to yourself and don't renumber outlets unless removing them improves performance.

outlets = 9;
// 0 = pattrstorage
// 1 = chooser object for programs list
// 2 = dialog box
// 3 = pgm# 
// 4 = preset name 
// 5 = midi (only used by program manager)
// 6 = banks menu
// 7 = pattrhub object 
// 8 = multi offset in programs buffer~
// 9 = call program pattrstorage defaults 

Instead of using outlets from a javaScript object, one could send messages to objects from JavaScript directly. The advantage of using outlets is that one can print to the console from them. That's been very useful during design (particularly for getting the preset name from pattrstorage to show up in a textedit object correctly).

I've found the pattrub object particularly useful. When setting controls to defaults, and when initializing a bank, the script simply sends the object names and values to outlet 7, and a connection port to the top-level patch pushes the commands to a pattrhub object there. Across the two pattrstorage objects there are >200 display elements and controls to update, and if setting them from JavaScript there is a noticeable flicker across the display on global preset recalls. But the pattrhub object puts the commands in the main Max queue, taking them off the lower-priority thread, so preset changes appear to happen instantaneously. My hearing isn't good enough to tell if there's an audible difference, so I design to be sure the flaws of my own senses do not flaw the audio performance.

Declarations and Initialization

Next the script has global variable declarations in order of size, which is not required but is standard coding convention, for reasons described in tutorial 8. This includes all the scripting names of the objects the pattrstorage object manages, their default values, and the 160 default sound parameter values that are stored in a shared buffer across 16 channels. These I made by sending pattrstorage recall messages while outputmode was set to 1 in the inspector and capturing the results in a zlist.group object, then banging that out to a message box.

  • Tip: Max only displays the first 7 decimal places after a floating point, probably because 32-bit floating point has seven bits of decimal precision. However if you have a fraction with a lot of zeroes before the numbers start, it won't show them all. So to get better precision, multiply by a million with a (~ 1000000.) object before writing the values to a message box, then divide by a million before putting them in JavaScript. Here is a little patch to illustrate the problem and its solution.

Max Patch
Copy patch and select New From Clipboard in Max.

After the declarations, there is an initialization routine, as described in the last tutorials:

var programs = new Buffer("programs");
var layers   = new Buffer("layers"  );
var multinames = ["bpm", "chan", "desc1", "desc2", "desc3", "desc4", 
    "desc5", "desc6", "desc7", "desc8", "desc9", "desc10", 
    "desc11", "desc12", "desc13", "desc14", "desc15", 
    "desc16", "limattM", "limlvlM", "limonM",
     "limratioM", "limrelM", "limthreshM", "matrix1", "matrix2", 
    "multins", "multiNum", "outputCount", "portin", "portout",
     "solo", "tipmode", "tuning", "vmax", "zoom"];
var multivals =[120, 1, "(empty)", "(empty)", "(empty)", "(empty)", 
    "(empty)", "(empty)", "(empty)", "(empty)", "(empty)", 
    "(empty)", "(empty)", "(empty)", "(empty)", "(empty)", 
    "(empty)", "(empty)", 25, 0, 0, 5, 25, -6, "0 0 0", "0 0 0", 
    "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", 0, 0, 0, 0, 0, 0, 0, 
    16, 2];
var bufDefaults = [0, 0, .00021671707, .0021671707, .5, 
    .0021671707,.00021671707, 0,.5, .00021671707,.00021671707,
    .5, 0, 0, .5, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 
    63.5, 63.5, 64, 64, 0, 0, 0, 0, 0, .5, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    .00021671707, .00021671707,.00021671707, 0, 0, 3, 3, 2, 0, 
    0, 2.03, 2.03, 2.03, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, .0226757377, .0226757377, 
    0, 0, 0, 0, .5, .5, 0, 0, 0, .5, .5, 0, 0, 0, 0, 0, 0, 
    64, 64, 0, 0, 0, 0, 0, 0, 0, 64, 64, 64, 64, 64, 64, 76,
     83, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
var multinames2 = ["limthreshM", "limratioM", "limattM", "limrelM",
       "limlblaM", "limlbldM", "limlblrM", "limlbltM", "limlbllM"];
var multiIds    = new Array();
var multiIds2   = new Array();
var jsonFile    = "multis.json";// filename of presets
var slotName    = "(empty)";   // current program name
var multinsId;
var mchanId;
var pgm         = 1;        // current program
var action     = 0;            // callback flag
var minit       = 0;
var debug     = 0;
function multiInit(){
    if(debug)post('multiInit()\n');
    for (i = 0; i < multinames.length; i++){
       multiIds[i] =
        this.patcher.parentpatcher.getnamed(multinames[i]);
        if(debug==2)
        post('multiInit',multinames[i],multiIds[i],'\n');
    }
    for (i = 0; i < multinames2.length; i++){
        multiIds2[i] =         
        this.patcher.parentpatcher.getnamed(multinames2[i]);
        if(debug==2)
        post('multiInit',multinames2[i],multiIds2[i],'\n');
    }
    multinsId = this.patcher.parentpatcher.getnamed('multins');
       mchanId   = this.patcher.parentpatcher.getnamed('mchan');
    if(debug==2)post('multinsId', multinsId, '\n');
    minit = 1;
}

Responding to Bank Menu Commands

I used to have a separate window for bank management containing a big live.tab object. Then I realized it could be in one simple menu, which resets itself after any of its options are chosen.

The most complex action is making a new bank, which deletes the current pattrstorage data, subscribes to all the objects it maintains again, sets their default values, stores that as a preset, copies it to the other 127 presets, recalls the first one, then updates the chooser list of preset names. So its actions jump around all over the place, and there was no obvious way to make the lower code more sequential.

//---- BANK FUNCTIONS --------------------------------------------
function banks(b){
    if(debug) post('banks(', b, ')\n');
    if (minit == 0) multiInit();
    outlet(6, "set", 0); // reset banks menu
    outlet(0, "outputmode", 0); 
    switch(b){
        case 1:                                 // new bank
            outlet(0, "clear"); // empty storage 
            outlet(1, 'clear'); // clear menu
            action = 30;
            subscribe();
            defaults();
            pgm = 1;
            outlet(0, "store" , pgm);
            outlet(0, "recall", pgm);
            break;
        case 2:                                 // open bank
            outlet(0, "clear");  
            outlet(1, 'clear');
            action = 31;
            subscribe();
            outlet(0, "read"); // read dialog
            break;
        case 3:                                 // save bank
            action = 32;
            exportPrograms();
            outlet(0, "write", jsonfile);
            break;
        case 4:                                 // save as
            action = 33;
            exportPrograms();
            //outlet(0, "write");// save-as dialog
            break;
        default:
            break;
    }
}
function defaults(){
    if(debug) post('defaults, action=', action, '\n');
    if (minit == 0) multiInit();
    for (i = 0; i < multinames.length; i +=1){
        outlet(7, multinames[i], multivals[i]); 
        if(debug==2)
            post('defaults', multinames[i],multivals[i],'\n');
    }
}
function subscribe(){
    if(debug) post('subscribing, action=', action, '\n');
    for (i = 0; i < multinames.length; i +=1){
        outlet(0, "subscribe", multinames[i]); 
        if(debug==2)
            post('subscribe', multinames[i],'\n');
    }
}

Read and Write Messages from pattrstorage

The next two functions are 'callbacks' from pattrstorage after it's told to read or write a file. pattrstorage always issues 'read' and 'write' messages even if 'outputmode' is zero, and the 'read' message is useful to continue initialization after pattrstorage has loaded its data, because if you try doing so from a loadbang, it can trigger before pattrstorage loads all its data. Pattrsatorage also issues 'recall' messages after a preset is recalled, but I've never managed to coax it into issuing a 'store' message, so part of the design requirements for pattrstorage are making sure you don't need to do anything with stored data after a store operation.

The 'read' and 'write' pattrstorage messages report if the file read or write was successful, which is useful to users. So if they're not, the JavaScript sets the text in a 'dialog' subpatch's comment, upon which reception the subpatcher opens itself and makes itself the front window (see the Max help on 'thispatcher' for how to do that more easily than with JavaScript statements).

//------READ AND WRITE FUNCTIONS ----------------------------------
function read(){ // from pattrstorage
    var fileData = arrayfromargs(arguments);
    if(debug) post('read(', fileData, '), action=', action, '\n');
    if (fileData[1] !=1){
        outlet(2, "Could not open file", fileData[0]);
    }else if (action == 0){
        if(debug) post('read() ->loadbang\n');
        if (minit == 0) multiInit();
        outlet(1, "clear");
        outlet(0, "getstoredvalue", "multiNum", 1);
    }else if (action == 30){
        if(debug) post('read()->new bank\n');
        outlet(0, "getstoredvalue", "multiNum", 1);
    }else if (action == 31){ 
        if(debug) post('read() ->open\n');
        importPrograms();
        outlet(0, "getstoredvalue", "multiNum", 1);
    }
}
function write(){ // bankfile was written, from pattrstorage
    var fileData = arrayfromargs(arguments);
    if(debug) post('write(', fileData, '), action=', action, '\n');
    if (fileData[1] !=1){
        outlet(2, "Could not write file", fileData[0]);// dialog
    }
    outlet(0, "outputmode", 1); 
}

The following function fetches the most recently recalled preset, so it can be reloaded. The multiNum value is updated every time the preset changes by a 'setall' message to pattrstorage, although it really only needs to be stored in one slot:

function multiNum(x){ // last used preset, from pattrstorage
    if(debug) post('multiNum(', x, '), action =', action, '\n');
    pgm = x;
    if (pgm == 0 ) pgm = 1;
    if (action == 0||action==31){//loadbang/read bank
        outlet(0, "recall", pgm);
    }
}

Preset Recall and Store

pattrstorage can provide a list of a fetch a sparse collection of presets, so only those created, whatever slots they are in, are visible in the UI. But in this design I decided to precreate all 128 presets. That is because I think MIDI should provide a default sound, even if it tries to recall a preset from an unpopulated slot. So I put default settings in 128 preset slots when creating a new bank.

Of course, that means all 128 slots are already full, as far as pattrstorage knows, even if they just contain defaults. So this script has its own 'insert' and 'append' type functions to put presets in 'empty' slots. That functionality has to be after you get the current slot name list (unless you cache it the slotname list in JavaScript, which is a little awkward during development, because every time you edit and save the script, you lose the cached live data). So the code for those menu options are way down near the bottom of the script.

//-------PRESET FUNCTIONS ----------------------------------------
function buttons(x){
    if(debug) post('buttons(', x, ')\n');
    outlet(0, "outputmode", 0); 
    if (minit == 0) multiInit();
    switch(x){
        case 0: // append
            action = 100; 
            outlet(0, 'getslotnamelist');
            break;
        case 1: //insert
            action = 110; 
            outlet(0, 'getslotnamelist');
            break;
        case 2: // revert
            // recall to pattrstorage
            outlet(0, pgm); 
            break; 
        case 3: // replace
            storeslot();
            break;
        case 4: //delete
            outlet(0, 'delete', pgm);
            if (pgm !=1) pgm --;
            outlet(0, 'setall', presetNum, pgm);
            outlet(1, 'set', pgm -1);
            outlet(3,
              'activebgcolor',0.545,0.435,0.216,1.);
            outlet(3, 
              'bgcolor',0.545,0.435,0.216,1.);
            outlet(3, "text", pgm); 
            outlet(3, "texton", pgm); 
            action = -1;
            outlet(0, 'getslotNamelist', 1);
            break;
    }
}

Near the top of the recall() methods are the recall commands to pattrstorage, in order to execute recalls responsivey. In the other subpatcher for part storage, which manages program sounds, the recall messages to pattrstorage issues directly from MIDI into pattrstorage without diving into JavaScript, so there is as little latency as possible during live performance. By contrast, multi presets in MIDI instruments conventionally support 16 channels. This is not because musicians are expected to use all 16 channels at once but rather that's more than enough for almost all live performances. Therefore in Husserl3, the JavaScript for the multi pattrstorage sends recall commands to pattrstorage, making the code more self explanatory.

function recallC(x){ // from chooser 
    if(debug) post('recallC(', x, ')\n');
    recallM(x +1);
}  
function recallM(x){ // from MIDI in 
    if(debug) post('recallM(', x, ')\n');
    if (minit == 0) multiInit();
    action = 1;
    outlet(0, x); // recall message to pattrstorage
}  

The recall() function is the callback from pattrstorage after a preset is recalled. The action flags were very useful here to invoke different procedures depending whether the preset is recalled during different bank and preset operations.

function recall(x){ // from pattrstorage after recall 
    var a = new Array(16);
    if(debug) 
       post('recall(', x, '), action = ', action, '\n');
    //outlet(5, x);     // send to midi first
    pgm = x;
    outlet(8, pgm *160 -160); 
    outlet(3, "text", pgm); // update displayed preset number
    outlet(3, "texton", pgm); 
    outlet(3, "activebgcolor", 0., 0., 0., 1.); 
    outlet(3, "bgcolor", 0., 0., 0., 1.); 
    outlet(0, "setall", "multiNum", pgm); 
    a = multinsId.getvalueof();
    layers.poke(1, 0, a);
    if(debug) post("layers buffer", a, '\n');
    if (action == 0){
        if(debug) post('recall()->loadbang\n');
        outlet(0, "getslotnamelist", 1); 
    }else if (action == 1){    
        if(debug) post('recall()->recall program\n');
        outlet(0, "write", jsonfile);

    }else if (action == 30){
        if(debug) post('recall()->new bank\n');
        for(i = 1; i <129; i++){
            outlet(0, "store", i); 
            outlet(0, "slotname", i, "[default]"); 
        }
        for(i = 0; i <129; i++){
            for(c = 1; c <17; c++){
               programs.poke(c, i *160, bufDefaults);
            } 
        }
        // reset buffers layer
        for(i = 0; i <16; i++){
            a[i] = 0;
        }
        layers.poke(1, 0, a);
        outlet(0, "getslotnamelist", 1); 
    }else if (action == 31){
        if(debug) post('recall()->open bank\n');
        outlet(0, "getslotnamelist", 1); 
    }else{
        post('unrecognized action\n');
    }
}  

The storeslot function does everything needed for all store actions, because there isn't a 'store' callback from pattrstorage.

function storeslot(){ // called in js by buttons() and slotlist()
    a = new Array(16);
    if(debug) post('storeslot(',pgm,')', slotName, '\n');
    a = layers.peek(1, 0, 16);
    if(debug) post("layers buffer", a, '\n');
    multinsId.message(a);
    //outlet(5, pgm);             // to midi
    outlet(3, "text"  , pgm); 
    outlet(3, "texton", pgm); 
    outlet(3, "activebgcolor", 0., 0., 0., 1.); 
    outlet(3, "bgcolor"      , 0., 0., 0., 1.); 
    outlet(0, "setall", "multiNum", pgm); 
    outlet(0, 'store' , pgm);
    outlet(8, pgm *160 -160); 
}

newname() might change because I'm still having trouble getting some characters out of pattrstorage.

function newname(s){ // user renamed preset
    if(debug) post('newname(', s, ')\n');
    slotName = s;
    outlet(1, 'clear'); // to chooser
    outlet(0, 'slotname', pgm, slotName); // stores new name
    action = -1;
    outlet(0, "getslotnamelist", 1);
}

And here is the response to slotname callbacks, which has three main parts: actions if the slotname is for the current program, actions if not, and actions if the callback is in response to a getslotnamelist command and the list is done:

function slotname(){ // from pattrstorage
    // contains pgm# in slot[0], then name
    var slot = arrayfromargs(arguments); 
    var s = slot.slice(1).toString();
    var x = 0;
    if(slot[0] != 'done'){     // if lostnamelist not done
         outlet(1, 'append', slot);
        if(slot[0] == pgm){     // if current preset
            slotName = s;
            outlet(4, "set", slotName);
            outlet(1, 'set', pgm -1);
        }
        if(action == 100){     // if append
            if(s == "[empty]"){
                x = pgm;
            }
        } else if (action == 110) {    // or insert
            if(s == "[empty]" && slotfound==0){
                x = pgm;
                slotfound = 1;
            }
        }
    } else { // if slotnamelist done 
        if(action == 0 || action == 1 ){ // if loadbang or recall
            outlet(0, "getstoredvalue", "matrix1", pgm);
        }else if(action == 30 ){ // new bank
            outlet(9, 'bang'); // defaults
            action = -1;
        }else if(action == 31 ){ // opening bank
            action = -1;
        }else if(action == 51){  // deleting preset
            action = -1;
            outlet(0, "write", jsonfile);
        }else if(action == 100 || action == 110){// append or insert
            pgm = x;
            storeslot();
        } else if (action==200) { // storing slot
            action = -1;
            outlet(0, "write", jsonfile);
        }
    }
}

Display Functions

Most of Husserls' display is in a separate script, but if one script object calls another directly, the first script gets stuck where it was. So this script has a couple of functions for display. The first toggles the 'hide on lock' attribute of some controls to switch views, as described in the last tutorial. The second changes the color of the preset number when the patch is dirtied.

//------ DISPLAY FUNCTIONS ----------------------------------
function tips(x){
    if (minit == 0) multiInit();
    if (x==0){
        this.patcher.parentpatcher.getnamed("tips").message('hidden', 0);
        multiIds[2].message('hidden', 1);
    } else{
        this.patcher.parentpatcher.getnamed("tips").message('hidden', 1);
        multiIds[2].message('hidden', 0);
    }
}
function limonM(x){
    if (minit == 0) multiInit();
     if (x == 0){
       a = [1, 1, 1, 1, 1, 1, 1, 1, 1]; //limon off
    }else if (x == 1){
         a = [0, 1, 0, 0, 0, 0, 1, 1, 0]; //limit mode
     }else{
        a = [0, 0, 0, 0, 0, 0, 0, 0, 1]; // compressor mode
    }
    for (i = 0; i < multiIds2.length; i++){
        multiIds2[i].message("hidden", a[i]);
    }
}
function dirt(){
    outlet(3, 'activebgcolor', 0.545, 0.435, 0.216, 1.);
    outlet(3, 'bgcolor', 0.545, 0.435, 0.216, 1.);
}

So that's what it takes to program pattrstorage from JavaScript. I wish I could say it's simpler, but if you want a production-quality interface to your design, as I say, you may well wish to consider using dict instead, because Max has built-in javascript functions for ti that access the dict data directly, obviating the need for action flags.

  • Tip: Using an external editor This year's Microsoft xcode is excellent for both JavaScript and gen~, and free. the biggest problem Ive had with Microsft software development tools is stopping them from updating unexpectedly. One can spend money but it's getting difficult for developers to beat the free tools these days. Adobe Dreamweaver does have an easier-to-use search-and-replace for changing code blocks that contain linefeeds. Of course Cycling74 doesn't know when you've edited the file, although there is probably some way to do that for JavaScript, it won't work for gen~.

Storing Buffer() Data in pattrstorage without Memory Faults

This turned out to be the largest design problem I had this year. For several days I was stuck with persistent crashes after pressing the preset recall/store buttons in various random patterns. I was initially convinced I had made an infinite loop because of all the branched operations caused by action variables. I removed them in sequence with no improvement, and I'm still putting all the functionality back in. That's because I finally figured out the errors were from pressing buttons too quickly, rather than any particular branch misidrection in the control tree. Eventually, after several days of continual deliberate crashing and rebooting, I traced that to these statements:

a = layers.peek(1, 0, 16);    
multinsId = 
  this.patcher.parentpatcher.getnamed('multins').message('set', a); 

for recalls, and similarly for stores:

a = 
  this.patcher.parentpatcher.getnamed('multins').getvalueof(); 
layers.poke(1, 0, a);

which always worked the first time. So here is my guess. The second time I press a button quickly, sometimes Max's JavaScript parser was still finding the object reference for an earlier call, so the JavaScript was trying to access or change the same object value twice at the same time. And after caching the object reference, the problem seems to have gone away...

var minit = 0;
var multinsId;
function multiInit(){ // contents can't be at top level 
                      // because JavaScript is compiled at startup
                      // before Max assigns IDs to objects  
    //. . .
    multinsId = this.patcher.parentpatcher.getnamed('multins');
}
recall(pgm){ // called by pattrstorage
       if(minit= 0) multiInit()
       a = layers.peek(1, 0, 16); 
       multinsId.message('set', a); 
        // . . .
    } 
 

At least it seems to be working properly so far. And that explains why I wrote the long tutorial on JavaScript (part 8), because it took me a lot of thinking to figure out that the parser's object list traversal was the source of the problem.

So you may think that is good cause to file a Cycling74 bug report. Well I don't. JavaScript was not specifically designed for optimal reentrancy, and the effort to fix a bug like that in a JavaScript parser could really be better used elsewhere.

Moreover, it is not considered good practice in JavaScript to have a lot of objects at the top level, but Husserl3's top-level patch was designed to minimize queue loading and latency for all audio control paths. After all, a single MIDI program change causes in excess of 400 messages already, and its messages only pass through one port. Hence the design does not have loads of subpatches like most programmers prefer:

That's about 500 objects, not including the connections. When the above script searches its parent patcher, it has to look through an ENORMOUS list of objects and their properties. So if I identified the problem correctly, I couldn't say I'm surprised, because I did it to myself. rofl.

***

Thank you for reading my tutorial. I plan one more on JavaScript after the Husserl release. Happy patching )

loadmess's icon

Super Nice! 😍 Please... Keep doing more JS+Max tutorials.
Thank you!

Ernest's icon

Well thank you, currently I am working on program export/import of all 128 presets in one file (you may have noticed the above example only does one preset). It exports fine, but I am getting delayed crashes on import, so I'm running traces on various ways to split up the data, as 32K values are too many to be one value array in pattrstorage.

That will take at least a few days, mostly due to lingering fatigue from continually rebooting my computer. Then I have a little design work left to do before Husserl3 is done, and some example presets, so Im concentrating on getting that done now. The good news is, I do have the data split up across the 128 presets so far, and the buffer data for 2560 values all shows up in the json file for each preset:

Ernest's icon

Oh, I'm pretty sure I fixed it already this time ) tutorial updated. Next, adding another buffer to contain decoded matrix settings for muting before migrating it to the programs pattrstorage object...