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:
- General clip metadata including start and stop time (for the currently selected time range, not the entire length of the clip) and pitch range (also for the selected notes, not the entire clip). As usual with the Live API, times are in quarter notes.
- The scale, if any. When no scale is set on the clip, this will be the Chromatic scale.
- The current time grid settings in clip view, if any. An interval of 0.25 is a 1/16 note grid (because 1 is a quarter note).
- A random seed value that changes with every Apply Cycle.
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.