JS Live API » #4-A Reading MIDI Clips
This is part of a series of articles about hacking on Ableton Live with JavaScript. These articles assume you own Ableton Live 9 Suite and are comfortable coding JavaScript.
In this article we'll learn how to access the notes inside a MIDI clip. This material continues in the second half of this article, where we'll modify the notes and write them back to the MIDI clip.
This article builds upon the previous articles, so refer back to them if you're having trouble here.
Getting Started
First
let's paste in our log()
function that we built in article #2: Logging & Debugging.
This will help us explore the Live API.
The rest of this article assumes the log() function is in your script.
I won't repeat this code again, so pretend it's at the top of all the code examples.
function log() { for(var i=0,len=arguments.length; i<len; i++) { var message = arguments[i]; if(message && message.toString) { var s = message.toString(); if(s.indexOf("[object ") >= 0) { s = JSON.stringify(message); } post(s); } else if(message === null) { post("<null>"); } else { post(message); } } post("\n"); } log("___________________________________________________"); log("Reload:", new Date);
Now let's figure out how to work with clips using techniques we learned in the previous article. Take a look
at the Live Object Model diagram
to determine a path to a clip. Starting at live_set
→ view
, we can follow the
highlighted_clip_slot
arrow to
the currently selected clip slot, and then to the clip
inside. Like this:
var path = "live_set view highlighted_clip_slot clip"; var liveObject = new LiveAPI(path); log("path:", liveObject.path); log("id:", liveObject.id); log("children:", liveObject.children); log(liveObject.info);
The first time you try this, you might get an id of 0, an Invalid syntax
error,
and a "No object" message. That's because
you don't have a clip slot selected in Live's session view, or the slot is empty and doesn't contain a clip. So
create a clip and click it in the session view grid to select the clip slot.
Then re-run the JavaScript and the Max window should show something like:
path: "live_set tracks 0 clip_slots 0 clip" id: 5 children: canonical_parent,view id 5 type Clip description This class represents a Clip in Live. It can be either an Audio Clip or a MIDI Clip\\, in an Arrangement or the Session\\, depending on the Track (Slot) it lives in. child canonical_parent ClipSlot child view View property color int property end_marker float property has_envelopes bool property is_arrangement_clip bool property is_audio_clip bool property is_midi_clip bool property is_overdubbing bool property is_playing bool property is_recording bool property is_triggered bool property length float property loop_end float property loop_start float property looping bool property muted bool property name unicode property playing_position float property signature_denominator int property signature_numerator int property start_marker float property start_time float property will_record_on_start bool function clear_all_envelopes function clear_envelope function deselect_all_notes function duplicate_loop function fire function get_notes function get_selected_notes function move_playing_pos function quantize function quantize_pitch function remove_notes function replace_selected_notes function select_all_notes function set_fire_button_state function set_notes function stop
Note the "canonical" path to the clip: "live_set tracks 0 clip_slots 0 clip"
. This means we accessed the first track's
first clip. You'll see a different path if you've selected a different clip in Live.
We could have, for example, accessed the second track's third clip with "live_set tracks 1 clip_slots 2 clip"
.
We'll be working with the currently selected clip in this article, but keep in mind you can go to any specific clip/slot this way.
Accessing Notes in a MIDI Clip
Now that we know how to access the currently selected clip in session view, let's see what we can do with the clip
object. In the info we logged above, there's a get_notes
function that looks interesting.
Create a MIDI clip and add some notes to it, and make sure it's selected. Then run this code:
var path = "live_set view highlighted_clip_slot clip"; var liveObject = new LiveAPI(path); log("path:", liveObject.path); log("notes:", liveObject.call("get_notes"));
Uh oh. That doesn't quite work. We get an error and some useless garbage value for the notes:
path: "live_set tracks 0 clip_slots 0 clip"
Invalid syntax: 'get_notes'
notes: 5e-324
Unfortunately, the JavaScript Live API doesn't provide much assistance in this situation, so
let's consult the documentation. In the Live Object Model diagram
click on the box labeled "clip" to get to the clip object's documentation and look for the get_notes
function. Here's what it says:
Parameter: from_time [double] from_pitch [double] time_span [double] pitch_span [double] Returns a list of notes that start in the given area. The output is similar to get_selected_notes.
Those four parameters are required, and we aren't providing them, which is causing the error. So let's add the parameters:
var path = "live_set view highlighted_clip_slot clip"; var liveObject = new LiveAPI(path); log("path:", liveObject.path); log("notes:", liveObject.call("get_notes", 0, 0, 256, 128));
To get all the notes in the clip, we use the following parameters:
- 0 for
from_time
to start at the beginning of the clip - 0 for
from_pitch
to start the lowest pitch - 256 for
time_span
to get the first 256 beats of the clip - 128 for
pitch_span
to cover the full range of MIDI pitch values 0-127
I'm guessing a clip won't be more than 256 beats. This is just a guess and you could use a higher number if you want. Soon we'll see how to determine the clip length with JavaScript, so we won't have to guess.
When we run this code, the output is much more promising! You notes data will look different based on your clip's notes:
path: "live_set tracks 0 clip_slots 0 clip" notes: notes,3,note,60,0,1,127,0,note,62,1,2,66,0,note,64,3,1,87,0,done
What does the notes data mean? Looking back at the documentation for get_notes
, it says
"The output is similar to get_selected_notes".
The get_selected_notes
documentation explains:
Output: get_selected_notes notes count get_selected_notes note pitch time duration velocity muted ... get_selected_notes done count [int] is the number of note lines to follow. pitch [int] is the MIDI note number, 0...127, 60 is C3. time [double] is the note start time in beats of absolute clip time. duration [double] is the note length in beats. velocity [int] is the note velocity, 1 ... 127. muted [bool] is 1 if the note is deactivated.
This doesn't exactly match our JavaScript data because this documentation is for the [live.object]
Max object
instead of the JS Live API. It's close though.
Basically, the JS Live API doesn't use separate messages (it's all one big array of data), and the data is not
prefixed with "get_selected_notes".
If we split up our JavaScript output into multiple lines, you can see how it matches the documentation. We get a notes count followed by groups of note,X,X,X,X,X where the Xs are numbers representing the pitch, (start) time, duration, velocity, and muted state of each note:
notes,3, note,60,0,1,127,0, note,62,1,2,66,0, note,64,3,1,87,0, done
This data corresponds to the notes in my clip. Take a moment to understand how this numeric data corresponds to the notes in the clip. (A C3 note in Live is MIDI pitch 60.)

The Clip Class
Let's organize our code into an object-oriented interface so we can more easily build some applications around it. Check out Mozilla's intro to object-oriented JavaScript if you are not familiar with this style of JavaScript programming.
function Clip() { var path = "live_set view highlighted_clip_slot clip"; this.liveObject = new LiveAPI(path); } Clip.prototype.getNotes = function(startTime, startPitch, timeRange, pitchRange) { return this.liveObject.call("get_notes", startTime, startPitch, timeRange, pitchRange); } var clip = new Clip(); var notes = clip.getNotes(0, 0, 256, 128); log(notes);
When we construct a new Clip object (which calls the Clip function that behaves like a class constructor),
it creates a liveObject
referencing the currently selected Clip in Live's session view. Then we call our getNotes()
function,
which simply wraps a call to that Live object's get_notes
function.
We can enhance our getNotes
function to make the parameters optional and provide default values.
The idea is that calling getNotes()
with no parameters should get all the notes in the clip.
function Clip() { var path = "live_set view highlighted_clip_slot clip"; this.liveObject = new LiveAPI(path); } Clip.prototype.getNotes = function(startTime, timeRange, startPitch, pitchRange) { if(!startTime) startTime = 0; if(!timeRange) timeRange = 256; if(!startPitch) startPitch = 0; if(!pitchRange) pitchRange = 128; return this.liveObject.call("get_notes", startTime, startPitch, timeRange, pitchRange); } var clip = new Clip(); var notes = clip.getNotes(); log(notes);
Note that I've switched the order of the startPitch
and timeRange
parameters.
Our getNotes()
function now takes startTime
and timeRange
first.
I usually either want to get all the notes, which can be done by calling getNotes()
with no parameters, or
I want to get the notes in a particular time range, which can be done by calling getNotes(startTime, timeRange)
.
The important point is we don't have to mirror the Live API. We can design our own interface that works the way we want.
Now that we have a basic class structure in place, let's stop guessing about that default timeRange value of 256 and use
the actual length of the clip. We can do that by getting the clip's live object's length property, and wrapping it in
our own getLength()
function:
function Clip() { var path = "live_set view highlighted_clip_slot clip"; this.liveObject = new LiveAPI(path); } Clip.prototype.getLength = function() { return this.liveObject.get('length'); } Clip.prototype.getNotes = function(startTime, timeRange, startPitch, pitchRange) { if(!startTime) startTime = 0; if(!timeRange) timeRange = this.getLength(); if(!startPitch) startPitch = 0; if(!pitchRange) pitchRange = 128; var data = this.liveObject.call("get_notes", startTime, startPitch, timeRange, pitchRange); return data; } var clip = new Clip(); log("clip length:", clip.getLength()); var notes = clip.getNotes(); log(notes);
This is a good start to our Clip class. Let's focus on the note data next, so we don't have to deal directly with that raw array of note data.
The Note Class
Since the note data coming from the Live API has 5 properties per note, we'll focus on modelling those 5 properties in our class:
function Note(pitch, start, duration, velocity, muted) { this.pitch = pitch; this.start = start; this.duration = duration; this.velocity = velocity; this.muted = muted; } Note.prototype.toString = function() { return '{pitch:' + this.pitch + ', start:' + this.start + ', duration:' + this.duration + ', velocity:' + this.velocity + ', muted:' + this.muted + '}'; }
We also added a toString()
function to easily see what's going on when we log our Note objects.
Now we can parse the raw note data array into Note objects. We know that the data array always starts with
"notes", count, ...
and ends with "done"
. Because of that, we don't actually need to look at the notes count value, we can
construct a loop to pull out the individual notes' data directly:
// assuming we just did something like: // var data = clip.getNotes(); var notes = []; // data starts with "notes"/count and ends with "done" (which we ignore) for(var i=2,len=data.length-1; i<len; i+=6) { // each note starts with "note" (which we ignore) and is 6 items in the list var note = new Note(data[i+1], data[i+2], data[i+3], data[i+4], data[i+5]); notes.push(note); }
In detail: Starting the loop at i=2 skips over the first two ("notes",count
) entries and starts on the first "note"
entry.
In the body of the loop
we ignore the "note"
string by skipping data[i]
and grabbing data[i+1]
through
data[i+5]
for the 5 note parameters. Then we increment
the loop by 6 to get to the next note ("note"
+ 5 parameters is 6 entries in the array). We stop the loop when we
reach data.length-1
to ignore the final "done"
string.
Putting it All Together
Let's combine all this code to log a list of our Note objects instead of the raw note data array:
//-------------------------------------------------------------------- // Clip class function Clip() { var path = "live_set view highlighted_clip_slot clip"; this.liveObject = new LiveAPI(path); } Clip.prototype.getLength = function() { return this.liveObject.get('length'); } Clip.prototype.getNotes = function(startTime, timeRange, startPitch, pitchRange) { if(!startTime) startTime = 0; if(!timeRange) timeRange = this.getLength(); if(!startPitch) startPitch = 0; if(!pitchRange) pitchRange = 128; var data = this.liveObject.call("get_notes", startTime, startPitch, timeRange, pitchRange); var notes = []; // data starts with "notes"/count and ends with "done" (which we ignore) for(var i=2,len=data.length-1; i<len; i+=6) { // each note starts with "note" (which we ignore) and is 6 items in the list var note = new Note(data[i+1], data[i+2], data[i+3], data[i+4], data[i+5]); notes.push(note); } return notes; } //-------------------------------------------------------------------- // Note class function Note(pitch, start, duration, velocity, muted) { this.pitch = pitch; this.start = start; this.duration = duration; this.velocity = velocity; this.muted = muted; } Note.prototype.toString = function() { return '{pitch:' + this.pitch + ', start:' + this.start + ', duration:' + this.duration + ', velocity:' + this.velocity + ', muted:' + this.muted + '}'; } //-------------------------------------------------------------------- var clip = new Clip(); var notes = clip.getNotes(); notes.forEach(function(note){ log(note); });
You should see something like this in the Max window:
{pitch:60, start:0, duration:1, velocity:127, muted:0} {pitch:62, start:1, duration:2, velocity:66, muted:0} {pitch:64, start:3, duration:1, velocity:87, muted:0}
Accessing Selected Notes
Let's add one more feature for accessing the note data.
In the Live API documentation, we saw a get_selected_notes
function.
This opens up some interesting possibilities where we can work with the notes selected by
the user, which can be an arbitrary subest of the notes in the clip.
As the documentation indicated, get_selected_notes
returns data in the same
format as the get_notes
function. This means we can use the same loop to parse the raw note data array into our Note objects.
Let's use good software development practice and not repeat ourselves
(the DRY principle):
//-------------------------------------------------------------------- // Clip class function Clip() { var path = "live_set view highlighted_clip_slot clip"; this.liveObject = new LiveAPI(path); } Clip.prototype.getLength = function() { return this.liveObject.get('length'); } Clip.prototype._parseNoteData = function(data) { var notes = []; // data starts with "notes"/count and ends with "done" (which we ignore) for(var i=2,len=data.length-1; i<len; i+=6) { // and each note starts with "note" (which we ignore) and is 6 items in the list var note = new Note(data[i+1], data[i+2], data[i+3], data[i+4], data[i+5]); notes.push(note); } return notes; } Clip.prototype.getSelectedNotes = function() { var data = this.liveObject.call('get_selected_notes'); return this._parseNoteData(data); } Clip.prototype.getNotes = function(startTime, timeRange, startPitch, pitchRange) { if(!startTime) startTime = 0; if(!timeRange) timeRange = this.getLength(); if(!startPitch) startPitch = 0; if(!pitchRange) pitchRange = 128; var data = this.liveObject.call("get_notes", startTime, startPitch, timeRange, pitchRange); return this._parseNoteData(data); } //-------------------------------------------------------------------- // Note class // ... // this class and the rest of the code is the same as before...
Rather than copy and
paste the raw note data parsing loop into the new function getSelectedNotes()
,
we've moved it into a helper function called _parseNoteData()
where we can reuse the code.
I'm using a convention that functions starting with a "_" are for internal use in an object
(Note: There are ways to enforce
that a function be private to a class, but that's out of the scope of this article).
Now we can do things like this at the bottom of our script, to operate on the selected notes:
var clip = new Clip(); var notes = clip.getSelectedNotes(); notes.forEach(function(note){ log(note); });
If you don't see any notes logged, make sure you've actually selected some notes inside your MIDI clip.
Next Steps
This article dealt with reading the note data our of a MIDI clip. Using the code we built, we can get the notes in
the entire clip, a specific subset of the clip (by using the optional parameters to the getNotes()
function), or the selected notes in a clip. That should
cover our needs for accessing note data in MIDI clips.
Adam Murray, June 2014
contact the author...