The Max Console

This is a follow-up to the JavaScript in Live tutorial "Real Time MIDI Processing". In this tutorial, start with another new Max MIDI Effect device, as explained in "Getting Started".

To work with JavaScript in Live, we need to be able to understand what our code is doing. One of the best ways to do that is to send messages to the Max Console to check the state of variables and trace the execution of the script. As we saw in previous tutorials, we can use the built in post() function to display messages in the Max Console. This tutorial shows how to use post() to display whatever data you want in an understandable way, which is not always straightforward.

To achieve this, we'll wrap post() with a general purpose log() function for printing information in the Max Console. This is inspired by the console.log() function that's available to JavaScript in web browsers and Node.js. We'll use log() in future tutorials including the next tutorial, "The Live API", where we'll explore how the Live API works.

Finding the Max Console

Before we start coding, let's take a closer look at the Max Console. There are a few ways to open it:

  1. Clicking the icon in the Max patch editor's right sidebar (highlighted blue here):
  2. Opening a dedicated window via the Max patch editor's "Window" menu (note the keyboard shortcut: Command+Shift+M on macOS, Ctrl+Shift+M on Windows):
  3. Right-clicking the Max for Live device's title bar in Live (where it's called the "Max Window"):

Note the "Max Window" opened from the device in Live is independent of the console opened from the Max patch editor. There's two different copies of your device: The one that runs in Live and the one you edit in Max. Each one has its own console.

When you're actually using your device in Live, opening the "Max Window" from the device's title bar is a good place to check for errors when something isn't working. Otherwise, when actively editing the device, use the Max Console opened from the patch editor.

Console Features

The Max Console has useful features in its top and bottom toolbars. My most used are:

Try right clicking a message for even more features.

The Built-in post() Function

Like we did in "Getting Started", run this JavaScript code using a js object in a Max device:

post("Hello");

You should see js • Hello in the Max Console.

Let's try printing multiple messages. Run this code:

post("Hello");
post("Goodbye");

Now the Max Console shows js • Hello Hello Goodbye on a single line. That's not ideal. We can add explicit newlines:

post("Hello\n");
post("Goodbye\n");

And now the Max Console should look like:

js • Hello
js • Goodbye

To be exact: The first time you ran it, "Hello" was still be on the same line as the other messages because none of them ended with an explicit newline. Run it twice and, if you've been following along, you now see something like:

Before we move on, note the js • prefix indicates the message came from a js object. This will always be the case for JavaScript code. I'm going to omit this prefix from now on.

A Custom log() Function

I don't know about you, but I don't want to type "\n" at the end of every message I send to the Max Console. Let's introduce our own function for logging information in the console:


function log(message) {
  post(message, "\n");
}

log("Hello");
log("Goodbye");

Now the Max Console prints our log messages on their own lines by default. Nice:

Hello
Goodbye

Let's try throwing a whole bunch of stuff at log() and see what happens:


function log(message) {
  post(message, "\n");
}

log("Hello");
log(1);
log(3.14159);
log(true)
log(null);
log();
log([1, 2, 3]);
log({a: 'a', b: 'b'});
log([1,{a:{b:true}}]);
log(new Date());
Hello
1
3.14
1
jsobject  -1407374883553280
<undefined>
1  2  3
jsobject  -1266631771819824
1  jsobject  -1266631771819720
jsobject  -1266631799910064

😒
This situation could be.... better. Note some of the subtle issues like 3.14159 displays as 3.14 and true displays as 1. This will impede efforts to debug. It's why I wrote a tutorial about logging.

Using String()

Some of the output above looks like jsobject, a representation of objects in the code. JavaScript has a String() function we can use to convert anything into a string. Let's try that:


function log(message) {
  post(String(message), "\n");
}

log("Hello");
log(1);
log(3.14159);
log(true)
log(null);
log();
log([1, 2, 3]);
log({a: 'a', b: 'b'});
log([1,{a:{b:true}}]);
log(new Date());
Hello
1
3.14159
true
null
undefined
1,2,3
[object Object]
1,[object Object]
Sun Nov 03 2024 17:44:04 GMT-0800 (PST)

Some things have improved: floating point numbers have more precision, we see true instead of 1, null instead of jsobject, commas are printing in between array items, and the Date is printing reasonably.

Let's see what we can do about [object Object].

Using JSON.stringify()

Since String() is giving us problems logging objects, let's try converting them to a JSON string with JSON.stringify(). As an experiment, try it on everything to see what happens:


function log(message) {
  post(JSON.stringify(message), "\n");
}

log("Hello");
log(1);
log(3.14159);
log(true)
log(null);
log();
log([1, 2, 3]);
log({a: 'a', b: 'b'});
log([1,{a:{b:true}}]);
log(new Date());
"Hello"
1
3.14159
true
null
<undefined>
[1,2,3]
{"a":"a","b":"b"}
[1,{"a":{"b":true}}]
"2024-11-04T01:49:02.899Z"

Not bad! Now we can see the objects and everything else still looks ok. Strings are wrapped in explicit quotes now (like "Hello"), and the Date looks different. Those aren't necessarily problems. Except...

I'm jumping ahead here to save time: If we blindly use JSON.stringify() like this, it's going to make some logs unusable. Let's try logging a LiveAPI object:


function log(message) {
  post(JSON.stringify(message), "\n");
}

log(new LiveAPI('live_set').info);
"id 3\u000atype Song\u000adescription This class represents a Live set.\u000achildren cue_points CuePoint\u000achildren return_tracks Track\u000achildren scenes Scene\u000achildren tracks Track\u000achildren visible_tracks Track\u000achild groove_pool GroovePool\u000achild master_track Track\u000achild view View\u000aproperty appointed_device NoneType\u000aproperty arrangement_overdub bool\u000aproperty back_to_arranger bool\u000aproperty can_capture_midi bool\u000aproperty can_jump_to_next_cue bool\u000aproperty can_jump_to_prev_cue bool\u000aproperty can_redo bool\u000aproperty can_undo bool\u000aproperty clip_trigger_quantization int\u000aproperty count_in_duration int\u000aproperty current_song_time float\u000aproperty exclusive_arm bool\u000aproperty exclusive_solo bool\u000aproperty file_path str\u000aproperty groove_amount float\u000aproperty is_ableton_link_enabled bool\u000aproperty is_ableton_link_start_stop_sync_enabled bool\u000aproperty is_counting_in bool\u000aproperty is_playing bool\u000aproperty last_event_time float\u000aproperty loop bool\u000aproperty loop_length float\u000aproperty loop_start float\u000aproperty metronome bool\u000aproperty midi_recording_quantization int\u000aproperty name str\u000aproperty nudge_down bool\u000aproperty nudge_up bool\u000aproperty overdub bool\u000aproperty punch_in bool\u000aproperty punch_out bool\u000aproperty re_enable_automation_enabled bool\u000aproperty record_mode bool\u000aproperty root_note int\u000aproperty scale_intervals IntVector\u000aproperty scale_mode bool\u000aproperty scale_name str\u000aproperty select_on_launch bool\u000aproperty session_automation_record bool\u000aproperty session_record bool\u000aproperty session_record_status int\u000aproperty signature_denominator int\u000aproperty signature_numerator int\u000aproperty song_length float\u000aproperty start_time float\u000aproperty swing_amount float\u000aproperty tempo float\u000aproperty tempo_follower_enabled bool\u000aproperty tuning_system NoneType\u000afunction capture_and_insert_scene\u000afunction capture_midi\u000afunction continue_playing\u000afunction create_audio_track\u000afunction create_midi_track\u000afunction create_return_track\u000afunction create_scene\u000afunction delete_return_track\u000afunction delete_scene\u000afunction delete_track\u000afunction duplicate_scene\u000afunction duplicate_track\u000afunction find_device_position\u000afunction force_link_beat_time\u000afunction get_beats_loop_length\u000afunction get_beats_loop_start\u000afunction get_current_beats_song_time\u000afunction get_current_smpte_song_time\u000afunction is_cue_point_selected\u000afunction jump_by\u000afunction jump_to_next_cue\u000afunction jump_to_prev_cue\u000afunction move_device\u000afunction play_selection\u000afunction re_enable_automation\u000afunction redo\u000afunction scrub_by\u000afunction set_or_delete_cue\u000afunction start_playing\u000afunction stop_all_clips\u000afunction stop_playing\u000afunction tap_tempo\u000afunction trigger_session_record\u000afunction undo\u000adone"

🫠
Yikes. That's not going to work. We want to read this information to help understand the Live API.

The Best of Both Worlds

We can combine both approaches. We'll try to use String() first, and if that results in any "[object Object]" text, we'll use JSON.stringify() instead. To be robust, we'll look for the prefix "[object " because in theory we might encounter other types like "[object Something]":


function log(message) {
  var s = String(message);
  if(s.indexOf("[object ") >= 0) {
    s = JSON.stringify(message);
  }
  post(s, "\n");
}

log("Hello");
log(1);
log(3.14159);
log(true)
log(null);
log();
log([1, 2, 3]);
log({a: 'a', b: 'b'});
log([1,{a:{b:true}}]);
log(new Date());
log(new LiveAPI('live_set').info);
Hello
1
3.14159
true
null
undefined
1,2,3
{"a":"a","b":"b"}
[1,{"a":{"b":true}}]
Sun Nov 03 2024 17:54:08 GMT-0800 (PST)
id 3
type Song
description This class represents a Live set.
children cue_points CuePoint
children return_tracks Track
children scenes Scene
...

The line that says id 3 is where the new LiveAPI('live_set').info starts printing a lot of information across many lines, most of which I've omitted. Don't worry about what all of that means yet. You'll find out soon. The point is: it is now readable. Mission accomplished. 🙏

Multiple arguments

For more flexibility, let's support passing multiple values to log() so we can log things like log(x, y); instead of log(x + " " + y);. Right now, if we do log(1, 2, 3) it only prints 1. We can handle multiple parameters by looping over the pre-defined arguments object:


function log() {
  for(var i=0; i < arguments.length; i++) {
    var message = arguments[i];
    var s = String(message);
    if(s.indexOf("[object ") >= 0) {
      s = JSON.stringify(message);
    }
    post(s);
  }
  post("\n");
}

log(1, 2, 3);
log();
log([1, 2, 3]); // should log the same as before

Which results in:

1  2  3

1,2,3

Note that calling log(); with no arguments now prints an empty line instead of undefined. I prefer this behavior. It's similar to how console.log() works in web browsers.

At this point I've found log() to be good enough for most needs. Because we're going to use it in a lot of other scripts, I can't help but want to make it a little more compact:


function log() {
  for(var i=0; i < arguments.length; i++) {
    var s = String(arguments[i]);
    post(s.indexOf("[object ") >= 0 ? JSON.stringify(arguments[i]) : s);
  }
  post("\n");
}

There are a couple other things worth mentioning about the Max Console before we wrap up this tutorial.

Reporting Errors

Max's js object has another function similar to post() called error(). It prints to the Max Console with a reddish background to indicate something went wrong. So, for example, maybe you have a try{...} catch(exception) block in your code where you want to log the exception and make it clear it's an error. We can create another logging function for this. Since Max's built-in function is called error(), I'll call ours warn():


function log() {...} // same as above

function warn() {
  for(var i=0; i < arguments.length; i++) {
    var s = String(arguments[i]);
    error(s.indexOf("[object ") >= 0 ? JSON.stringify(arguments[i]) : s);
  }
  error("\n");
}

log("Everything is ok.");
warn("Something bad happened!");

It's exactly the same code as log() but replaces post() with error(). It looks like this in the Max Console:

I don't use warn() in these tutorials, but you may find it useful, especially in large complex projects. The Max console has a button to toggle between showing only errors or showing all messages.

You can debug remotely if you log errors. If you share your patch with someone and it's not working: have them open the Max Window (from the Device's title bar right-click menu), filter by errors, and tell you what they see.

Tip: Logging Script Changes

When you are debugging, making lots of changes, and logging a lot, the Max Console can get messy and confusing. Its "Clear All" button is certainly useful. I always come back to this simple technique: Add something like this to the top of a script while developing it:

function log() {...} // same as above
log("------------------------------------------------------------------")
log("Reloaded on", Date());
//---------------------------------------------------------------------
// "Real" code starts here...

Every time the script re-runs, it draws a separator and displays the current time to distinguish different runs of the script:

... logs from previous script run ...
------------------------------------------------------------------  
Reloaded on Sun Nov 03 2024 18:10:23 GMT-0800 (PST)

If you have multiple scripts, you can log distinct names here, like "Reloaded octave switcher on". Do whatever helps you keep track of what's happening. It might keep you sane through the worst debugging sessions.

Next Steps

Now that we're able to print out information from our JavaScript code, we can use this ability to learn the Live API in the next tutorial, "The Live API".