MIDI Tools #1: Transformations

As of April 2025, this tutorial requires Live 12.2 Beta, or the standalone version of Max 9 (not included with Ableton Live Suite) to use the new v8 JavaScript object (see here for standalone Max 9 setup).

This is a follow up to the "Generating MIDI Clips" tutorial. In this and the next tutorial, we'll learn how to transform and generate MIDI clips with Ableton Live's MIDI Tools and JavaScript code. MIDI tools are a special type of Max for Live device that are part of Live's MIDI clip editing workflow.

This first tutorial on MIDI Tools focuses on general usage and making transformation tools to modify existing notes in MIDI clips. It's a little easier to modify existing data than generate new data from scratch. Once we're comfortable with transformations, we'll look at generators in the next tutorial, "MIDI Tools #2: Generators" (coming soon).

General Usage

MIDI Tools come in two types: Generators and Transformations. Generators create notes in MIDI clips. If the clip isn't empty, a generator can add additional notes, potentially overwriting existing notes. Transformations modify existing notes in MIDI clips. Transformations can add or remove notes too, such as when splitting a note into multiple notes or when simplifying a clip.

Technically, Generators and Transformations are practically the same. They can both add, change, or remove notes from clips. In general, it's good to follow the conventions I described in the previous paragraph: Generators create new content, transformations modify existing content. This helps MIDI tools be intuitive.

As the Live Manual's chapter on MIDI Tools explains, MIDI Tools can be found in the Generate and Transform panels in the MIDI clip view. For example, here's the built-in Generate tool called "Rhythm", which can generate a rhythmic pattern for a given pitch:

Spend time playing around with the built-in transformations and generators to get an idea of what MIDI Tools can do.

Technical Details

The Max MIDI Tools documentation explains MIDI Tools in detail. It has tutorials for making a transformation tool and a generator tool by patching together Max objects (without JavaScript). Going through those docs will give you a better understanding of MIDI Tools.

If you want to dive into the JavaScript approach, feel free to skip the docs for now. I'll try to explain everything you need to know as we work through these JavaScript-based MIDI Tools. Give the docs a read if anything here doesn't make sense or you want to know more.

Creating a MIDI Transformation Tool

Depending on whether you want to make a transformation or a generator, you create a new MIDI tool from the Transform panel or the Generate panel of Live's clip view. We're going to start by making a transformation tool, so go to the Transform panel. Then, click on the list of transformation tools and select "Max MIDI Transformation" in the "User:" section towards the bottom of the list of tools:

This special "Max MIDI Transformation" user tool is a template for making new transformation tools. Use it any time you want to make a new transformation tool.

The main difference between user tools and the built-in ones is the user tools have an edit button in the lower left of the panel next to the "Auto" button. It's the same button used to edit standard Max for Live Devices (it looks like this ). Click the edit button to open the Max patch editor.

When you initially edit it, the first thing you should do is save the Max patch with the name of your tool. I saved it as "V8 Transformation.amxd". By default it should ask you to save under the "Max Transformations" folder, inside the "MIDI Tools" folder of your Live User Library. Save it in this default folder to make it appear in the list of MIDI transformation tools in clip view.

Once it's in the list of MIDI tools, you can select it in the Transform panel of clip view to use it, and click the edit button again to make more changes in the Max editor.

To remove a custom MIDI tool, find it under "User Library" → "MIDI Tools" in the Live Browser and delete the file. Or, right click, select "Show in Finder/Explorer", and move it outside the Live User Library to hide it from Live.

Next, we can clean up and delete the "Build your Transformation here" and "Device vertical limit" comments. Expand the Max editor window to see the "Device vertical limit" comment. Similar to standard Max for Live devices, the device vertical limit indicates the space we have to work with that is visible in Live's UI. We'll still be able to see a box indicating this visible UI area, so we don't need the "Device vertical limit" comment.

Notes Input Format

Before we start writing JavaScript code, let's take a look at the input data that we'll be working with. This data represents the notes in the MIDI clip we are transforming. It uses Max's Dictionary data structure, which is similar to JSON.

There are two objects designed to show what's inside a dictionary in Max: dict.view and dict.codebox. Try adding both of these to your MIDI tool and connect them to the left outlet of the live.miditool.in object. Make sure the clip you opened in clip view (the one we're going to transform) has some notes in it and the notes are selected.

Then, you can click the "Transform" button on the MIDI tool in Live's clip view, or you can add a button to the Max patch and connect that to live.miditool.in (as seen in the following screenshot). If your clip has selected notes, we'll see the note data in the dict.view and dict.codebox:

If you don't see any data, check Live's clip view is open, the clip has notes, the notes are selected (try ctrl+A/command+A to select all), and the objects in the Max patch are connected correctly.

dict.view and dict.codebox are two different ways of viewing the same data. I like dict.codebox when working with JavaScript because it is a JSON-compatible format and you can copy data directly out of dict.codebox and into v8 or v8.codebox.

Now we can see the data we need to transform. It looks like this:


{
  "notes" : [
    {
      "note_id" : 1,
      "pitch" : 60,
      "start_time" : 0.0,
      "duration" : 1.0,
      "velocity" : 100.0,
      "mute" : 0,
      "probability" : 1.0,
      "velocity_deviation" : 0.0,
      "release_velocity" : 64.0
    }
  ]
}

Transforming the Notes

In a transformation MIDI tool, we take a list of notes like the above example and change some of the data. We can modify any note properties except note_id because those IDs tell Live which notes we are changing. Every other property of a note can be changed. We can remove notes from the list or add additional notes (which won't have a note_id when we add them because they don't exist yet).

The question is, how do we get this data into our JavaScript code? Thankfully, there is a very straightforward way to do it that was added in Max 9. Create a v8.codebox and add this code:


function msg_dictionary({notes}) {
  notes.forEach(n => n.pitch += 1);
  outlet_dictionary(0, {notes});
}

msg_dictionary() is called when v8 or v8.codebox receives a dictionary as input. The Dictionary is automatically converted to a JavaScript object, as if we had called JSON.parse() on the text we saw in dict.codebox earlier.

In this first pass of our transformation logic, we unpack the {notes} data inside the dictionary, then we add 1 to every note's pitch, and we output the modified dictionary using outlet_dictionary(). The first argument of 0 is the outlet index, so in this case we are sending the modified dictionary to the first (and only) outlet. The dictionary needs to contain the list of notes, which we pack back up with {notes}.

msg_dictionary, outlet_dictionary, and other core functions can be found in the documentation for the jsthis object in Max's JavaScript engine.

We need to break the connection between live.miditool.in and live.miditool.out and insert the v8.codebox in between them so that the output of v8.codebox is actually used. You can also use another dict.codebox to confirm the changes to the dictionary are being made as intended (here we can see the pitch changed from 65 to 66):

Now, every time you click the "Transform" button back in Live's clip view, it transposes the notes of the clip up by one semitone.

Remember: If it doesn't work, make sure some notes are selected in the MIDI clip because MIDI transformation tools only operate on the selected notes. If nothing is selected, it does nothing.

UI Controls

We already have a functional transformation MIDI tool, but hard-coding pitch += 1 isn't very interesting. Let's parameterize the transposition amount and add a UI for controlling it.

There are at least a couple ways to approach this. One is by defining a top-level variable in the JavaScript code, adding a function to set the value, and calling that function from the Max patch. Or, this general approach can be streamlined by creating custom attributes for our v8.codebox object.

Declaring Attributes

The declareattribute function let's us create attributes for our v8 and v8.codebox objects.

We can declare a "transpose" attribute to hold the transposition amount for our MIDI tool like this:


var transpose = 0;
declareattribute("transpose", 
  { type: "long", min: -12, max: 12, default: 0 });

Attributes using the default getters and setters (as we are doing in this tutorial) must be declared with var instead of let. An attribute's corresponding JavaScript variable must be in global scope. let is lexically scoped and variables declared with it aren't exposed properly.

I set the min and max to -12 and 12 so the tool can transpose up to one octave down or up. Feel free to increase that.

Then, we use pitch += transpose instead of pitch += 1:


function msg_dictionary({notes}) {
  notes.forEach(n => n.pitch += transpose);
  outlet_dictionary(0, {notes});
}

Testing with the Max Inspector

We can test this right now even though we haven't built a proper UI for it yet. One benefit of using attributes is we can see them in the Max inspector. Open the inspector panel on the right sidebar of the Max editor and click on v8.codebox. Make sure all settings are being displayed in the filters at the top of the inspector, and scroll down until you find the v8 section. Our transpose attribute should be listed there, and you can edit its value.

I set my transpose attribute to 5. Then, clicking "Transform" in Live's clip view adds 5 to my notes' pitches:

Adding a UI

It's nice to test attributes quickly with the inspector, but we need UI controls that will be visible in Live's clip view. When a MIDI tool is being edited and the patch is unlocked, the Max editor shows a square bordered area that is the visible area in Live's interface.

First let's move all the current objects down out outside of the visible area. I am also deleting the dict.codebox objects to make space, but you should reach for those objects again when you need to debug the state of a dictionary in Max.

Next, add a live.slider object and put it in the visible area. If you hover over the left edge of the slider object a green circle arrow icon appears providing access to the Object Action Menu:

Click that green circle arrow icon, select "Connect", and select the v8 object's "transpose" attribute:

This uses Max's Parameter Connect feature to connect the slider UI object to the "transpose" attribute defined in our JavaScript code.

Now we can save our MIDI tool, close the Max editor, and try it out in Live. Try changing the transpose amount and clicking "Transform":

It works, but what's up with the "Auto" button? It should automatically transpose the notes as we change the transpose slider.

Automatically Applying Changes

Currently our v8.codebox code is only triggered when live.miditool.in sends it a dictionary of notes to transform, and that only happens when the "Transform" button is pressed in clip view in Live. To make things work with the "Auto" button, we need to force it to re-transform when our UI controls change values. We do this by sending a "bang" to live.miditool.in.

The live.slider object outputs its value whenever it changes. Its value is a number. We want to convert that to a "bang" message. We can add a button object, connect live.slider to that, and connect button to the live.miditool.in.

This works because the button responds to any input by sending a "bang" message to its outlet, so any change to the slider sends a "bang" message to live.miditool.in.

It's worth noting that an alternative to button is trigger bang (or t b for short). A trigger object is generally more efficient for this sort of conversion duty (such as convert anything into a "bang"), but we usually only need to be worried about that efficiency in very large, complex patches.

Now that we are sending "bang" to live.miditool.in whenever we change the "transpose" amount, it should immediately update the selected notes in the Live MIDI clip if the "Auto" button is enabled. Try it!

Also take some time to experiment with the undo history. You can make a many changes to "transpose" while "Auto" is enabled, and if you don't do anything else in Live, undoing goes back to when you started that set of changes. This behavior is explained in more detail in the Apply Cycle section of Max's MIDI tools docs. It's kind of difficult to explain it in words, but when "Auto" is working properly, I find the behavior very intuitive and it will hopefully make a lot of sense when you spend time using it.

Hiding Patch Cords

It's bothersome we can see the patch cord connecting the slider to the rest of our patch outside the visible area (the line drawn to the "Auto" button in this screenshot):

Max's Presentation Mode is probably the best way to handle this sort of thing, and we'll talk about that in the next tutorial. Since our MIDI tool is so simple, we'll use a quick trick: hiding the problematic patch cord.

Edit the MIDI tool again and click on the problem patch cord. Then go to the Object menu in the Max application's menu. Select "Hide on lock". Save the patch. Problem solved:

Now we have a basic transpose tool that can work on-the-fly in "Auto" mode as we change the transpose amount, or with "Auto" mode disabled every time we press the "Transform" button. And it looks ok.

The Note Context Dictionary

So far we have only used the dictionary containing the list of selected notes in the MIDI clip that is output from the first (leftmost) outlet of live.miditool.in. There's another dictionary output from the second outlet called the "note context" dictionary. It provides additional information about the MIDI clip that can be useful for transformations and generators.

Let's take a look. Once again, we'll use dict.codebox to look at our input format. This time we'll hook up a dict.codebox to the second outlet of live.miditool.in, like this:

One last reminder: if this isn't working, make sure you have notes selected in clip view.

For reference, here's what's in the screenshot above:


{
	"clip" : 	{
		"time_selection_start" : 0.0,
		"time_selection_end" : 4.0,
		"insert_marker_time" : 0.0,
		"first_note_start" : 0.0,
		"last_note_end" : 4.0,
		"lowest_pitch" : 60,
		"highest_pitch" : 65
	}
,
	"scale" : 	{
		"scale_mode" : 0,
		"root_note" : 0,
		"scale_intervals" : [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ],
		"scale_name" : "Chromatic"
	}
,
	"grid" : 	{
		"interval" : 0.25,
		"enabled" : 1
	}
,
	"seed" : 2478367475
}

Let's break it down. This note context dictionary has data about:

When the Live API gives a value representing time or a duration, the unit is quarter notes. This is true even if you are using a time signature where something other than a quarter note is one beat, such as 6/8.

Combining Two Dictionary Inputs

Let's figure out how to use this note context dictionary data in our JavaScript code. One immediate problem is we now have two dictionaries to work with, because we still want to know the list of notes.

When using msg_dictionary() in our JavaScript to handle dictionary inputs, it's easier to handle a single unified input. Although I believe it's possible to use things like jsthis.inlet to handle multiple dictionaries at multiple inlets to v8 or v8.codebox, we are using the dictionary input to trigger the transformation. It will be so much easier to continue handling one input and triggering one transformation than somehow coordinating multiple inputs.

All of that is to say, it's desirable to combine the two dictionaries from live.miditool.in into a single dictionary inside the Max patch before passing control to JavaScript. We can do that with a dict.join object. Connect the first two (leftmost) outlets of live.miditool.in to the two inlets of dict.join, and then connect that to v8.codebox. As usual, we can also connect it to dict.codebox to view the result of triggering live.miditool.in with some notes selected in the clip:

As seen in the screenshot, the note context (clip, scale, grid, and seed) data has been combined with the list of notes.

Now that there's a single dictionary containing all the clip, scale, grid, seed, and notes data coming out of live.miditool.in, we can use any of that data in our JavaScript code when transforming the list of notes.

Using Clip Data

Since we've been working with transposing pitches, let's do something with the clip.lowest_pitch and clip.highest_pitch data. We can add a feature to our transformation tool to "wrap around" when transposing, so any pitch that goes above the highest pitch will wrap around to the lowest pitch, and pitches that fall below the lowest pitch wrap around to the highest pitch. In other words, no note will exceed the initial range of pitches at the start of the transformation.

clip.lowest_pitch and clip.highest_pitch represent the lowest and highest pitches out of the currently selected notes in the clip, not the entire clip. They arguably should have been called lowest_selected_pitch and highest_selected_pitch.

Given that, we could calculate the lowest and highest pitch in JavaScript from the list of notes we are given. We won't do that because the point of this tutorial is to demonstrate how to use the clip data. We'll look at using other, more interesting data from the note context dictionary in the next tutorial.

To perform a "wrap around" operation we can use modular arithmetic. For this to work, we need to use relative pitches where the lowest pitch is 0. We transpose the relative pitch and calculate the relative transposed value modulo the pitch range by using the % operator. The modulo is what performs the "wrap around". Once we have the "wrapped" value, we need to offset it back to the original range by adding the lowest pitch.

There's a subtle detail about pitch range: Let's say our pitches are 60 and 61. For the purposes of our algorithm, this is a range of 2 because it spans two values. So it's not highest_pitch - lowest_pitch. We need to use highest_pitch - lowest_pitch + 1.

In summary, the algorithm looks like this:


range = highest_pitch - lowest_pitch + 1;
relative = note.pitch - lowest_pitch;
wrapped = (relative + transpose) % range;
note.pitch = wrapped + lowest_pitch; 

Let's see how to implement this in our msg_dictionary() function. To access the highest and lowest pitch data, first we extract the notes and clip out of the combined dictionary data passed to msg_dictionary(). Then we extract lowest_pitch and highest_pitch out of the clip data:


function msg_dictionary({notes, clip}) {  
  const {lowest_pitch, highest_pitch} = clip;

Before looping over the notes and applying our "transpose with wrap around" algorithm, we can calculate the pitch range, because it won't change inside the loop:


  const range = highest_pitch - lowest_pitch + 1;

and then apply the algorithm to all the notes:


  notes.forEach(note => {
    const relative = note.pitch - lowest_pitch;
    const wrapped = (relative + transpose) % range;
    note.pitch = wrapped + lowest_pitch; 
  });

Together, it looks like this:


var transpose = 0;
declareattribute("transpose",
  {type: "long", min: -12, max: 12, default: 0});

function msg_dictionary({notes, clip}) {
  const {lowest_pitch, highest_pitch} = clip;	
  const range = highest_pitch - lowest_pitch + 1;

  notes.forEach(note => {
    const relative = note.pitch - lowest_pitch;
    const wrapped = (relative + transpose) % range;
    note.pitch = wrapped + lowest_pitch; 
  });

  outlet_dictionary(0, {notes});
}

Fixing Downward Transposition

When you test this, you should see that it works correctly when transposing upwards, but not downwards. That's because the % operator in JavaScript does not behave the way we want for negative numbers. If our pitch range is 5, we want every wrapped value to be in the range 0 to 4 (including 4). But if you calculate -1 % 5 in JavaScript, the result is -1. Why this happens is too deep of a topic to explain here, but Wikipedia has an article if you want to know more.

If you consult you favorite search engine or AI chatbot, you'll probably arrive at the common solution for how to ensure modulo of a negative number in JavaScript is always positive. Instead of x % y, we need to do ((x % y) + y) % y.

We can make a helper function for this, which I've defined using const so it's not exposed to the containing Max patch:


const modulo = (x, y) => ((x % y) + y) % y;

and use that to calculate the modulo:


const wrapped = modulo(relative + transpose, range);

Now downward transposition wraps around correctly.

Toggle Switch Control

One last thing: Maybe we don't always want the pitches to wrap around and want to maintain the option to have the originally "unwrapped" transpose behavior. We can use what we already learned about attributes and UI controls to quickly add a toggle switch for this feature.

We'll add another attribute like we did for the transpose amount. This time the options are slightly different:


var wrap = 0;	
declareattribute("wrap",
  {type: "long", min: 0, max: 1, default: 0});

There is no boolean type for attributes, so for a boolean-like value to control a toggle switch, we use "long" again, but this time set min: 0, max: 1. This would result in a numerical input that only allows 0 or 1 to be input.

Now that we have a wrap attribute, we can use it to decide whether to perform a basic pitch transposition or transposition with wrapping:


var transpose = 0;
declareattribute("transpose",
  {type: "long", min: -12, max: 12, default: 0});
	
var wrap = 0;	
declareattribute("wrap",
  {type: "long", min: 0, max: 1, default: 0});
	
const modulo = (x, y) => ((x % y) + y) % y;
	
function msg_dictionary({notes, clip}) {
  const {lowest_pitch, highest_pitch} = clip;
	
  if (wrap) {	
    const range = highest_pitch - lowest_pitch + 1;
    notes.forEach(note => {
      const relative = note.pitch - lowest_pitch;
      const wrapped = modulo(relative + transpose, range);
      note.pitch = wrapped + lowest_pitch; 
    });
  } else {
	notes.forEach(n => n.pitch += transpose);
  }

  outlet_dictionary(0, {notes});
}

That's it for the code. Now we need to create the UI to control the attribute. We'll do the same thing we did in the Adding a UI section earlier in this tutorial, but this time we'll use a live.toggle instead of a live.slider.

Add a live.toggle object in the visible area of the MIDI tool next to the slider. Hover over its left edge, click the green circle arrow icon, and select the "wrap" attribute under "Connect" in the Object Action Menu. Unlike live.slider, a live.toggle does not have a built in label, so we can also add a live.comment, and type "wrap" into it:

Continuing on with the same setup from earlier in Adding a UI, connect the toggle switch to the button to send a "bang" to live.miditool.in when the wrap toggle switch changes state, so everything continues work with "Auto". We can reuse the existing button that the slider is using.

Finally, select the toggle's patch cord that's in the MIDI tool's visible area and enable "Hide on Lock" in Max's Object menu, like we did in Hiding Patch Cords.

Final Version

Here's the patch looks like with the "wrap" toggle switch:

And here it is in action:

And here's the patch, in case you have any problems getting your version to work:


----------begin_max5_patcher----------
1088.3ocsWssahqCE8Y3qvJRiDUMEkPAJzNcjlGNOd9BFFgLINfqB1HGGJ8f
3e+r214BzZBo2jfjX68s09ls22si2B4NVlG4dxeHc5ruamNlovI5TLti2Z5t
nTZlgLOA6Y4hm77sKoY6zloi4Q59OI4hxU1P0Qq3hkyUrHsUACF0OvmL31A3
qgSMCB5GP9aAK7Xin.weyzRwHy0oLs9kMLqLLJhKET0KdULJxWyE.YFKbP8j
VlMyFhSdnaW7geKgZJeKqejb8ZlP+F.+rhtoIrNDg23.74sFbGNwMTmbrneJ
OSyS3QTDiHIAtfXnSHF7QgnVtbYJqzL1RUB5ZVa.oIDNNzftQkOcgw6pEhBj
slolyDzEoriQyaB00AXCayijBQgt81NY9u+882erA1BWT4rYzsr34TsVwWjq
Y0ekU3qJbVn2HMmISJmtb9SQR95BKVlj34CuDUl9ITxEbMmlZINnIRdq64Dp
RkhktBRmPzZHRaDP+.WqJigeFILzwxYqjJ8EUQYvxkMlCHIS+hEDSsqaxNKe
8NSUgviVd41Kg1ZNa8G9zU93nlxGCNW93BpX4YZ5D901zIKkGyTtpH0JpHai
Li0fiv5ABMdfQ1uCl51Qb6GqvDxwSRkTcKpPeiA2B+1fuixzumhuWCO2UfCb
WBBNBb4aN25umRzFMjVWmF74qS2NA1yLlgD4H+0jUb4yHbmI4cXvXy9m3VLt
xdGVJnDoPmPiZt7sNYEIOi+eVWRP+F2iEosx5+4+JExrMflh+kWi6vDUD3Pv
SphMjGIAOLSDy.2khUkN2aVc3alm+LAgrGM76Iy7vrMXNBXZlLE3K5NLkxmD
yRn4oZ.xGtBj5LApKrU84UCtZiZHnT9NDOTcmoIPJYdpDTQuc9jWth73uH85
si7C76qwGvWFxSxElipQVmsbd8w15sWHgJXeRTJeygqH6QiwJ58oxmYY54a3
PdgOYEe4ppgG.MhbXDMgvSH8PvTveoD.u3RzKeBqjaHGKXvJCevxjwR5mHU+
CMZUObDBmBIVISVJbbrsnXQJ56RlObJKnksgECbX8V8pjw00IC9Vq8pJdOR5
OVIhqcomCVlNPXoPREXuyzuBJHNDEB65GqUZAiVmnMu83XCD9sgGiFNTUewS
YUEABonpYtsCMTKBEvMzd141re7VLm4NHlsNWyi4ZoLseKtJR3Tyq6Z7pHC7
ZZaPy+2yYBt86BufNtHfGOZB9Zxnl.bn2G6NGFYAFk30WnzXN37mBwLYtJpz
WVbKg5Ml8hgjdtn5hP+o5jaGQyJdbLSb9iZ0VMCwXR3Ez7TKMeJ8LsEHbnkl
OMdBZAd9r5YTKzyfu.8LrE5I7KPO29EkAZKEfF3aYprB1MJEpqeRpL2Hx2Lj
KrCMmAwSw1xKoejYFpBpj0PYbtx18c2X6od7viFpD47BcCvEToomA1o1bHE6
8lfVKcOz8+Y2nZVC
-----------end_max5_patcher-----------

Next Steps

In the the next tutorial, "MIDI Tools #2: Generators", we'll take a look at Generators and things we can do with the other data in the "note context" dictionary.

👉 Leave feedback on this article 👈