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_setview, 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:

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.

In the next article we will look at how to manipulate the notes and update the clip to actually change the notes.



Adam Murray, June 2014
contact the author...