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:
- Clicking the icon in the Max patch editor's right sidebar (highlighted blue here):
- 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):
- 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:
- Clear all messages. This gets used frequently before saving changes to a script. It gives a "clean run" in the Max Console logs.
- Search the messages. Logged too much and want to confirm a log line was executed? Search for it.
- Highlight the object in the Max patch that printed the message via the "Show Object" button in the bottom toolbar or
by double clicking any message. This is helpful with multiple
js
objects or a deep patcher hierarchy.
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".