You are here

Page 2: Cycling '74 Max 8

Programming Environment For Audio & MIDI By Nick Rothwell
Published November 2019

On The Node

Node.js is a JavaScript runtime: a program that runs JavaScript outside the context of the web browser. It's generally used for building web services or running utility programs, but thanks to Node for Max, it can now operate as part of Max itself. Whereas the Java runtime loads as part of the Max process, Node.js runs as a separate process. In practice this distinction is largely irrelevant, but it does mean that you have to launch a Node.js process before you can use it (although this can be automated). And you might also have several Node.js processes running at once (which is actually a good thing: they won't interfere with one another). Node.js is built into Max itself, so there are no hassles finding and downloading a separate runtime, as is necessary with Java. And Node.js itself is totally open-source.

Node.js within Max offers a  dynamic Web interface for scheduling calendar-based events.Node.js within Max offers a dynamic Web interface for scheduling calendar-based events.

To get a Node.js process that you can talk to, create a 'node.script' object with an argument naming a JavaScript file to load, and then send it a 'script start' message to launch the Node.js process. Once the process is running, it can read and write files and run network services. In the screen, Node.js is actually running a local web server, allowing audio files to be uploaded by a web browser for Max to play back. (The browser could be on the same machine that's running Max, or elsewhere, for example on a phone or tablet.)

The web interface is potentially extremely rich — browsers can render 2D and 3D graphics and support responsive and adaptable user interfaces — so you may be led towards the browser as your natural environment for interface building instead of (or in addition to) Max itself, if you have the coding skills. On the other hand, Node.js's connection to Max is a little sparse. You can send Max-style messages between Node.js and Max, and read and write Max data dictionaries, but that's about it; there's no access to Max's internal API for patcher scripting or graphics, for example, so if you want to do things like that you'll have write more code on the Max side and connect things up manually, or stick with the existing 'js' and 'jsui' objects.

Packing Up

Node.js supports a huge number of open-source third-party libraries, managed by the Node Package Manager, or NPM. NPM is bundled into Max alongside Node.js, so again there's no need for separate installation. If you want to run some Node.js JavaScript that refers to external libraries, you first need to send 'node.script' a specific 'npm' command to load the dependencies. (This only needs to be done once, before you attempt to run the script for the first time.)

At this stage we start to get into issues of file management. If your script uses external libraries, these are generally specified in a particular format in a 'package.json' file alongside the JavaScript source itself. And once the libraries have been fetched by NPM they'll be in a folder called 'node_modules' in the same location. All well and good, unless your work is part of a Max Project, which expects patcher files, JavaScript and other assets to be laid out in a very specific fashion. The workround is to place everything Node-related into a dedicated folder, and then add this folder to the project's search path so that the 'node.script' object can locate the main script. It works, but it's a little bit of a faff.

If you want to use more than one Node.js instance in one project, there's also some potential for confusion, since each script notionally has its own libraries. You can bundle multiple scripts in the same place and use a shared set of libraries, or put each script in its own folder and set up the project search paths accordingly, whichever suits you.

Since timing is so critical in a lot of Max applications, I was curious to see how snappy Node.js scripts might be, especially since Node.js runs as a separate process. To test this, I built a small Max patcher to generate repeated synchronised pairs of clicks, with the left one directly connected to a metronome and the right one receiving a message sent into Node.js and out again. With Overdrive turned off (ie. no interrupt-level scheduling) the clicks sounded completely coincident apart from the occasional flam, perhaps due to some housekeeping in the JavaScript runtime. A quick examination of the output in a DAW showed the clicks to be pretty much cycle-accurate, apart from the occasional pair differing by 10ms or so. With Overdrive turned on (and the metronome triggered from the interrupt thread), the flam was present each time, with timing differences varying between 3 and 10 ms. This suggests to me that, whilst you probably wouldn't want to build a step sequencer in Node.js (at least without pulling some trick like generating notes in advance and storing them in Max), for a whole variety of tasks the latency and jitter seems fairly respectable. (I've known hardware MIDI keyboards perform worse than this.)

Cartography

From the very beginning, Max has been able to take in MIDI controller data and map it to on-screen controls. In fact, for many years, MIDI was pretty much all that Max could process. With Max 8, this process has become much easier, with a dedicated editing mode to quickly attach MIDI controllers, as well as computer keys, to UI objects. In effect, Max can now do what DAWs have been able to do for years: easily establish links from external MIDI controllers to on-screen knobs, faders and buttons.

Purists might argue that this is an unnecessary complication, given that Max programmers are already used to doing this process by hand, but the new mapping system does have some advantages: it's quick, it's easy, and it's implicit (in other words, you set up a mapping by moving a control, not by typing in numbers to specify which control it is). And everything is done behind the scenes, with a dedicated mapping pane, rather than superfluous clutter in the patcher itself.

There are some down sides, though: you lose some flexibility since you're constrained to what the mapping system supports, and mappings don't differentiate between distinct MIDI ports. A binding from (say) modulation wheel on MIDI channel 12 will respond to any modulation wheel on channel 12, regardless of the physical device providing it (although you can select which devices are enabled for mapping). For what it's worth, Ableton Live's mapping mechanism is the same in this regard; and if you're exclusively a Max for Live user then Max's new mapping feature will be of little interest to you, since all your mappings will be done in Live, not Max.

Automated MIDI mapping is now available for some buttons, dials and sliders.Automated MIDI mapping is now available for some buttons, dials and sliders.To get into MIDI mapping mode, click the (music) keyboard icon in the bottom patcher toolbar. Mappable on-screen objects will be tinted in purple. (Any object which isn't a Max for Live UI object will need to be enabled for mapping, much as it would need to be enabled for automation in Max for Live.) Click the object, move the MIDI controller, and the mapping is done. The right-hand pane shows all mappings for the patcher, with configuration options for control minimum and maximum, exponent (to vary the response curve), number of steps, relative mode (for endless controllers), trigger mode (useful for note inputs) and pickup (for reconciling controller and on-screen value).

I experimented with relative mode, since there's no actual description of how it works in the documentation. The modes are labelled 'Push User/Arturia REL 2', 'Arturia REL 1' and 'Arturia REL 3', suggesting that it's aimed at controllers like the Ableton Push and Arturia MiniLab and KeyLab. The only dedicated encoder-enabled controller I had to hand was from Faderfox, so I plugged this in and fired things up. In Ableton Live the Faderfox works in '2's Complement' mode, while in Max it worked after a fashion in REL 2, but not without some glitches: encoder turns would sometimes have little effect, and sometimes cause a sudden value jump in the mapped object. (In the other REL modes there was no response at all.) Although I don't use my Ableton Push 2 as a generic controller, its inclusion here suggested that I might have more luck, so I tried mapping the Push's encoders as well. The results were, again, mixed: anticlockwise rotation was tracked smoothly and accurately, while clockwise glitched and jumped. (A minor 'gotcha' to watch out for when mapping a Push is that touching and releasing an encoder generates note events, so don't let go of an encoder in the middle of setting up a mapping.)

Cycling '74 told me that there were known issues with mapping the Push to on-screen objects which aren't the Live UI ones; I confirmed by testing with a Live level slider and it worked fine. The Faderfox can apparently put out large incremental values when its encoders are turned, and so needs a bit of fixing up on the Max side to accommodate. By the time you read this I'd hope these issues are solved. Computer keyboard mapping works in a similar manner to MIDI mapping, although the keyboard can only generate discrete events. Key press and release are, however, detected distinctly, allowing for 'momentary' mappings which kick in only while a key is being held down.

With Max 8 comes first-class support for multi-channel audio processing, courtesy of a framework and set of objects that go under the collective banner MC.

Off The Map

It's unfortunate that the textual list of mappings is the only way to examine them: there's no annotation on an object itself apart from a small dark square indicating that a mapping is present. If you click a line in the mappings list the corresponding object is selected, but this highlighting doesn't work in the other direction. Cycling '74 tell me they're still working on finessing the visual feedback in this area.

Mappings are stored inside the patcher itself, although they can be saved to, and restored from, a separate file. Each mapping is made according to an object's 'long name' attribute, so if this name is changed, the mapping will break. If a key or MIDI control is mapped, it still retains any effect or function it had before. Max has a handful of single-letter keyboard shortcuts, so if you map any of these you can end up editing Max and firing controls at the same time, which is a bit distracting. And there isn't any way to turn the mapping system off and on globally.

Predictably, the situation gets a little more complex when you start working with sub-patchers (embedded in the main patcher) and abstractions (loaded from other patcher files). In essence, all mapping is done from the outermost patcher. Even though you can turn on, and view, mapping from a sub-patcher window, the mapping happens at top level. When using abstractions, again, the mapping is done at top level, and it's not possible to enable mapping in the abstraction. If the abstraction's patcher file contains any mappings of its own, they are ignored.

The mappings feature generally works well — it's immediate and intuitive — but having come from more than 20 years of building my own MIDI mapping systems, I feel that the built-in system feels a little opaque. As sub-patchers and abstractions are loaded up, there's some internal object renaming going on to make sure that mappings are maintained properly. (To be fair, the same process happens when binding Max for Live controls for automation.) And there's one minor drawback that other mapping systems cater for: with physical devices which transmit multiple control streams at once, such as joysticks, Max's mapping mode has no way to select which incoming control number to use. (This feels like a relatively simple future enhancement.)

Further Improvements

There have been some subtle but significant improvements to Max's editing interface. Objects can now be grouped, or ungrouped, using menu commands (which, sadly, do not have keyboard shortcuts). Grouping a set of selected objects draws a narrow bounding box around them, and subsequently the selection of any object in the group selects them all. The entire group can be moved around as a single entity, edited (for example, changing colours or fonts), duplicated and so on. The bounding box can also be resized: all the objects within it will remain proportionally distributed horizontally and vertically within the group, but will not be individually resized. Groups are persistent and saved within the patcher. It helps to think of a group purely as a saved selection of objects, and there's no reason why groups can't be overlapped, for example: I can see this helping when dragging together or separating tight layouts of objects, where individual selection might be difficult. Groups don't have names, so can't be scripted in the same way as individual objects.

Objects can now be Shift-dragged to place them within existing patch connections. Object colour editing has been improved, with options for creating complementary colour ranges of various kinds: useful for creating an overall colour theme for an interface. And the separation between locked and unlocked patchers, which, to date, has always seemed fundamental to the way Max works, has now been undone thanks to a new Operate While Unlocked mode, where objects behave as if the patcher were locked, except they can still be clicked on their boundary for editing. Such a fundamental change to the Max interface is a bit disorienting at first, but now I'm getting used to it, and it is thankfully optional.

New search functionality covers packages, blog entries and forum posts.New search functionality covers packages, blog entries and forum posts.There's a new search pane which addresses a common gripe about Max: how hard it can be to find things. The built-in documentation browser is pretty comprehensive, but searching there only uncovers things in that documentation. The new search pane seems much more comprehensive, covering all the documentation plus installed packages, Cycling '74 staff blog posts, and the entire user forum. (Some searches I tried turned up forum posts I'd written more than 10 years ago.) Formal documentation tends to concentrate on what, rather than how, so it's good to see a more flexible search feature which conveniently accesses background articles, tutorials and so on. Of course there's a chance you'll come across articles that are out of date, but they do at least provide a starting point.

There are numerous other small refinements covering Max in general as well as the Jitter environment, mostly involving 3D visuals. (The Vizzie video-programming library now makes better use of graphics hardware to improve performance, for example.) Audio-related improvements that caught my eye include better support for multi-channel audio files, an enhancement to the 'poly~' object supporting a different patcher in each voice, and VST3 plug-in support. More generally, Max 8 promises faster patcher loading and user interface response, in particular to maintain video frame rate when editing. It's hard to make objective measurements of improvements, but patcher loading did indeed seem to be pretty snappy compared to what I was used to in Max 7. My early impressions of Max 8's UI performance were not that great — I noticed some timing issues when dragging objects around, for instance — but that's been improved tremendously in the various point releases, and as of Max 8.0.6 everything seems smooth and responsive. As an aside, DSP compilation is still a performance bottleneck, so while adding (say) a number box to a running patcher won't cause any glitches, a new audio object will briefly pause while the DSP code is regenerated. (If you can accept a slight increase in latency, turn on Mixer Crossfade to smooth the changeover when you make audio-related edits.)

Eight: Ace?

Max 8 is the first upgrade for a while that hasn't incorporated a noticeable redesign of the interface and patching experience, and for that reason it's perhaps tempting to regard this upgrade as 'minor'. To do so would be to underplay the effort that has been put into performance improvement, debugging and searching: improvements that don't immediately jump out, but which do have a noticeable positive effect on the user experience. As a Java and JavaScript coder of some years, my attention was immediately caught by the Node.js integration and the opportunities that offers. While hardcore Max coders have been doing network and systems interfacing in various ad hoc ways for years now (OSC being a favourite technology), Node.js ups the game by making that cleaner and more standardised, whilst opening the doors to the wealth of third-party Node code that's available. As a somewhat seasoned Max hacker I'm used to writing MIDI input routines for patchers, so I'm not sure how much I'll make use of the new MIDI and keyboard mapping feature, but it's so easy to use that I expect I'll eventually succumb, as and when the encoder mapping issues are addressed.

The real leap forward in Max 8, though, is multi-channel audio. MC is a shift in thinking which took me a while to grasp (over and above the obvious benefit of multi-channel patch cables), but now I've spent some time with it the way I think about Max patching is starting to fundamentally change. If MC takes off as I think it might, Max 8 becomes a pretty compelling upgrade.

Debugging Aids

Console debugging: watchpoints, filters and message times help show what's going on.Console debugging: watchpoints, filters and message times help show what's going on.

It has always been part of the Max credo that the program should not display dialogue boxes with information and errors while it's running, presumably because these would interfere with the creative flow of patching and performing, so for years Max users have had to get used to watching the text-based Max Console for real-time information about the system, or risk errors accumulating without realising it. One immediate improvement in Max 8 is a small red dot which appears on the console icon on the right-hand patcher toolbar whenever an error is reported, which at least directs you to the console to investigate. Mousing over any message in the console now highlights the object which generated it (in Max 7 you had to click an error icon to get the same information), although it seems not all objects support this consistently. Messages can be dynamically filtered by class, patcher, object or text, and there's a new feature to display the time of each message (or rather, the elapsed time since the message was issued).

A new kind of patch cord breakpoint allows messages to be printed to the console as they pass down the cord, while a new Event Probe option lets you mouse over a patch cord to show the last message that passed through it. Whilst neither feature offers radically new options compared to debugging in Max 7, they do streamline the process, and do away with much of the the need to clutter up the patcher with printing support.

Pros

  • Powerful new multi-channel audio features.
  • Integration with Node.js for web interfacing and JavaScript systems programming.
  • Quick and easy MIDI and keyboard mapping.
  • Improved debugging and search tools.
  • Performance improvements.

Cons

  • Some lingering gaps in the documentation.
  • Interface to controller mapping could be improved.
  • MIDI mapping of encoders in Max 8.0.6 is glitchy.
  • Node.js requires a bit of housekeeping to work with Max's Project file management.

Summary

Max 8 updates the popular graphical programming environment with multi-channel audio support, Node.js integration and quick mapping for MIDI and keyboard events. Debugging features have been improved, there's a new powerful search facility, and overall performance and response have been improved.

information

$399, $99 per year, or $9.99 per month. Upgrade from Max 7 $149.

www.cycling74.com

$399, $99 per year, or $9.99 per month. Upgrade from Max 7 $149.

www.cycling74.com

test spec

  • Cycling '74 Max 8.0.6.
  • Apple MacBook Pro with 2.5GHz Intel Core i7 CPU and 16GB RAM, running Mac OS 10.14.5.