Getting Started

log() 関数をもう一度用意しておきましょう。この記事ではこの関数が準備できている前提で進めていきます。

Again, let's paste in our log() function that we built in article #2: Logging & Debugging. The rest of this article assumes the log() function is in your script.

log() 関数
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);

そして以下のコードが前回の記事で作成した selected MIDI Clip の notes にアクセスする関数です。これもコピペしておいてください。

And below that we'll paste in the code we wrote in the previous article to access the selected MIDI clip's notes:

MIDI clip notes にアクセスする機能
//--------------------------------------------------------------------
// 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
 
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.getSelectedNotes();
notes.forEach(function(note){
  log(note);
});

ここからはこれらのコードの「変更する」部分だけを示すようにします。もちろん最後にコードの全体像を示しますので、途中でわからなくなっても安心してください。

For the rest of this article, I'm only going to show changes to the relevant section of the script. At the end, I'll show the entire script in case you make a mistake along the way.

Writing Notes to a MIDI Clip

Live Object Model documentation の Clip object の項目を見ると set_notes の機能は以下のように書いてあります。

The Live Object Model documentation for the Clip object shows there's a set_notes function that works like this:

Clip object のドキュメント
Parameter:
list_of_notes  [pitch, time, duration, velocity, is_muted]
Will apply the given notes to the clip, overwriting existing notes.
An example sequence of calls looks like this:

call set_notes
call notes 2
call note 60 0.0 0.5 100 0
call note 62 0.5 0.5 64 0
call done

For MIDI clips only.

JavaScript Clip class の一番最後の部分に関数を追加しましょう。この関数は JavaScript Note object のリストを引数として受け取って Live API の set_notes 関数を呼び出し、その note 情報を用います。

Let's add a function to the bottom of our JavaScript Clip class. It takes a list of our JavaScript Note objects as the parameter and call's the Live API set_notes function with the note data:

set_note を call する関数
Clip.prototype.setNotes = function(notes) {
  var liveObject = this.liveObject;
  liveObject.call("set_notes");
  liveObject.call("notes", notes.length);
  notes.forEach(function(note) {
    liveObject.call("note", note.pitch, note.start, note.duration, note.velocity, note.muted);
  });
  liveObject.call("done");
}

この関数が正常に動くかテストするために、JavaScript コードの最後の部分を次のコードに変更しましょう。(Note class 定義の後の部分です)このコードでは clip に C major chord のアルペジオをセットしています。(一拍ずつ C3 E3 G3 を演奏させます)

To test it out, replace the bottom of our script (after the Note class definition) with this code. Here we're trying to set the clip to have an arpeggiated C major chord (one beat each of C3, E3, G3):

var notes = [];
notes.push( new Note(60, 0, 1, 100, 0) );
notes.push( new Note(64, 1, 1, 100, 0) );
notes.push( new Note(67, 2, 1, 100, 0) );
 
var clip = new Clip();
clip.setNotes(notes);

うーん。うまくいかないようです…

Hmm... That doesn't work:

Error
Invalid syntax: 'note 60 1 1 100 0'
Invalid syntax: 'note 64 2 1 100 0'
Invalid syntax: 'note 67 3 1 100 0'
wrong note count

デバグしてみましょう。コードの最後の部分を変えて、直接 set_note 関数を呼び出すことにしましょう。(私たちが独自に作った Clip class と Note class はあえて使わないで実行してみます。)

Let's debug. Replace the code at the end of our script with a direct attempt to call the set_note function (bypassing our Clip and Note classes):

直に set_notes を実行してみる
var path = "live_set view highlighted_clip_slot clip";
var liveObject = new LiveAPI(path);
 
liveObject.call("set_notes");
liveObject.call("notes", 1);
liveObject.call("note", 60, 0, 1, 100, 0);
liveObject.call("done");

まだ同じエラーが出ます。私はこの問題に少し時間を取られましたが、最終的にはなんとか解決策を見つけました。ドキュメントの例には "note 62 0.5 0.5 64 0" という値が使われています。この例と同じく start time と duration を 0.5 に変更してみるとうまくいくのです!つまり JavaScript の整数値を Max の floating point message に変換する際に起きているバグのようです。(このバグは Cycling '74 に報告しましたので、将来的に修正されることを期待します。)最終的に値を小数点を含む文字列にするとこの問題を避けることができることがわかりました。

It gives the same error. I spent a while debugging this, and eventually stumbled across a solution. The documentation gave an example containing the line "note 62 0.5 0.5 64 0". If we change our start time and duration to 0.5, then it works! There seems to be a bug converting JavaScript integer numbers to Max floating point messages (which I have reported to Cycling '74 so hopefully it will be fixed in the future). I found that giving a string value containing a decimal point avoids the problem:

解決策
var path = "live_set view highlighted_clip_slot clip";
var liveObject = new LiveAPI(path);
 
liveObject.call("set_notes");
liveObject.call("notes", 1);
liveObject.call("note", 60, "0.0", "1.0", 100, 0);
liveObject.call("done");

これを実行しても何も起きていないようでしたら、現在選択している MIDI Clip が含んでいる note を全て削除してください。そうしてから JavaScript のコードを保存すると、C3 note が Clip の冒頭に作成されるはずです。

If it doesn't look like this script is doing anything, try deleting all the notes in the currently selected MIDI clip. Then when you save the script it should create a C3 note at the beginning of the clip.

この問題一般に対処する方法として JavaScript に組み込まれている数値用の関数である toFixed() を使うことができます。この関数は引数として数値を受け取り、この数値分の小数点をつけた文字列を返します。setNotes() 関数でこれを用いることにしましょう。出力結果を小数点が 4 つある文字列とすることにします。これだけあれば十分正確な値と言っていいでしょう。

We can generalize this workaround with the built-in toFixed() function for numbers in JavaScript. This function takes a parameter for the number of digits to appear after the decimal point, and returns the number as a string. Let's update our Clip setNotes() function to use this workaround. We'll output 4 digits after the decimal place, which should provide plenty of accuracy.

小数点の数を指定する
Clip.prototype.setNotes = function(notes) {
  var liveObject = this.liveObject;
  liveObject.call("set_notes");
  liveObject.call("notes", notes.length);
  notes.forEach(function(note) {
    liveObject.call("note", note.pitch,
                    note.start.toFixed(4), note.duration.toFixed(4),
                    note.velocity, note.muted);
  });
  liveObject.call("done");
}

ではコードの最後の方の部分を元々やりたかった形に戻してみましょう。

And replace the code at the bottom with what we had before:

0 を使っても正常に動くようになった
var notes = [];
notes.push( new Note(60, 0, 1, 100, 0) );
notes.push( new Note(64, 1, 1, 100, 0) );
notes.push( new Note(67, 2, 1, 100, 0) );
 
var clip = new Clip();
clip.setNotes(notes);

すると 3 つの note が Clip に作成されたはずです。(繰り返しになりますが clip に note が存在するばあいには、まずこれを削除してから実行するようにしてください。)

You should see 3 notes be created in your clip (again, you may need to delete existing notes first to see the new notes appear).

Handling Out-of-Bounds Values

このコードで色々試してみるとわかるのですが、他にも問題が発生します。例えば次のコードの場合です。

If you spend time playing around with the code we've written so far, you will probably have more issues. For example, try this code:

問題が起きるコード
var notes = [];
notes.push( new Note(60, 0, 1, 128, 0) );
var clip = new Clip();
clip.setNotes(notes);

ほとんど役に立たない Invalid syntax: 'done' というメッセージしか情報がありません。ただ MIDI velocity の値である 128 は、MIDI velocity 規格の最大値である 127 よりも大きい値であることと照らし合わせると、エラーが起きている理由がなんとなく予想できます。同じように velociy をマイナスの値にしてもエラーが起きます。これらをうまく対処して set_notes に有効な値を渡した上で call できるようにコードを変更しましょう。

We get an error Invalid syntax: 'done', which is not very helpful. But with some thought we might realize that a MIDI velocity value of 128 is higher than the maximum velocity value 127. We see similar behavior if we try using a negative velocity. So we need to be careful about calling set_notes with valid values.

Clip class の setNotes() 関数を改良して velocity が有効な範囲になるようにすることもできるのですが、この機能は Note class の責務とすることにしました。Note class に getter 関数を実装し、これによって値が正常な範囲となるようにします。以下のコードを Note class の toString() 関数の後ろに加えてください。

We could keep enhancing our Clip setNotes() function to ensure our velocity is in a valid range. However, I decided I'd like to make it the responsibility of Note class to enforce that the note properties are valid with some getter functions. Add the following code after the Note class's toString() function:

さらに改善
Note.MIN_DURATION = 1/128;
 
Note.prototype.getPitch = function() {
  if(this.pitch < 0) return 0;
  if(this.pitch > 127) return 127;
  return this.pitch;
}
 
Note.prototype.getStart = function() {
  // we convert to strings with decimals to work around a bug in Max
  // otherwise we get an invalid syntax error when trying to set notes
  if(this.start <= 0) return "0.0";
  return this.start.toFixed(4);
}
 
Note.prototype.getDuration = function() {
  if(this.duration <= Note.MIN_DURATION) return Note.MIN_DURATION;
  return this.duration.toFixed(4); // workaround similar bug as with getStart()
}
 
Note.prototype.getVelocity = function() {
  if(this.velocity < 1) return 1;
  if(this.velocity > 127) return 127;
  return this.velocity;
}
 
Note.prototype.getMuted = function() {
  if(this.muted) return 1;
  return 0;
}

MINDURATION は多少恣意的に決めた値です。実際的な用途ではこの値で問題ないと思いそうしましたが、もし 1/128 よりもさらに短い値を使いたい場合にも変更できるようにしました。MINDURATION 定数を変更するだけです。必要であればこの値を変更してください。

The MINDURATION is somewhat arbitrary. I decided that, for practical purposes, if I try to make a note shorter than a 1/128th note, it's duration will be 1/128. I used a MINDURATION constant so that this value can be easily changed in one place. Change it if you want.

また note の start time はマイナスにもできるのですが、これはエラーを引き起こしません。start time がマイナスの場合には、clip の開始よりも前に note がスタートするので、音は鳴りません。(一般的なケースでは少なくともなりません。しかし値としてはマイナスの値を持つことできます。)私は start time は少なくとも 0 になるようにさせることにしました。もちろん創造的な理由によってマイナスの値を start time に設定したい場合もあると思うので、その場合は自由に変更してください。

Also note that the start time for the note can actually be a negative number and this doesn't cause an error. Then the note starts before the beginning of the clip and you'll never hear it (at least, not normally. Apparently clips can have negative start times too). I decided to ensure that the start time is at least 0. Perhaps you will come up with some creative reasons to have a negative start time, so feel free to adjust this code.

では Clip class の setNotes() 関数を修正して、note を clip に set する際により堅牢になるようにします。これでエラーの大半は起きないでしょう。

Now we can adjust our Clip's setNotes() function to set the notes in a clip more robustly, avoiding errors in a lot of cases:

より堅牢に
Clip.prototype.setNotes = function(notes) {
  var liveObject = this.liveObject;
  liveObject.call("set_notes");
  liveObject.call("notes", notes.length);
  notes.forEach(function(note) {
    liveObject.call("note", note.getPitch(),
                    note.getStart(), note.getDuration(),
                    note.getVelocity(), note.getMuted());
  });
  liveObject.call("done");
}

以下のコードでは意図的に「間違い」をおかしていますが、それでも正常に note を clip に set することができます。

And with that code, we can make some "mistakes" and notes can still be set on a clip:

エラーが起きない
var notes = [];
notes.push( new Note(60, -1, 1, 100.5, 0) );
notes.push( new Note(64,  1, 0, 128, false) );
notes.push( new Note(67,  2, 1, -1, -1) );
 
var clip = new Clip();
clip.setNotes(notes);

理論上はこのような「間違い」を起こさなければいいだけですが、次に取り上げる "humanize" 機能では time と velocity の値をランダムに変更しますので、その際にエラーを起こす可能性のある値になってしまうことがあります。そのような場合にも今まで実装してきた機能によって正常に動作することが保証されます。

Ideally, we wouldn't make such "mistakes", but soon we're going to introduce a "humanize" feature that randomizes times and velocities. This code makes it so we don't have to worry about out-of-bounds values when working with random numbers.

Replacing Notes

Live API の set_notes 関数と似た非常に便利な関数として replace_selected_notes 関数があります。ドキュメントをみるとこの二つの関数同じシンタックスを持っていることがわかります。ですので replace_selected_notes 関数を導入するにあたっては、今まで書いてきたコードの関連する部分を helper 関数にまとめてロジックが一箇所にまとまるようにしましょう。これはちょうど _parseNoteData() helper 関数を Clip class の getSelectedNotes() 関数と getNotes() 関数で共有するように以前の記事で整理したのと同じです。では既存の setNotes() 関数を以下のように置き換えましょう。

The Live API setnotes function for Clip's has a related, useful function called replaceselectednotes. The Live Object Model documentation indicates the syntax is the same. So when we introduce this function, we can rework some of the code into a helper function and keep all the logic in one place. This is similar to what we did when sharing a `parseNoteData()` helper function between our Clip class's getSelectedNotes() and getNotes() functions in the previous article. Let's replace our existing setNotes() function with the following code:

変更
Clip.prototype._sendNotes = function(notes) {
  var liveObject = this.liveObject;
  liveObject.call("notes", notes.length);
  notes.forEach(function(note) {
    liveObject.call("note", note.getPitch(),
                    note.getStart(), note.getDuration(),
                    note.getVelocity(), note.getMuted());
  });
  liveObject.call('done');
}
 
Clip.prototype.replaceSelectedNotes = function(notes) {
  this.liveObject.call("replace_selected_notes");
  this._sendNotes(notes);
}
 
Clip.prototype.setNotes = function(notes) {
  this.liveObject.call("set_notes");
  this._sendNotes(notes);
}

変更を加えた setNotes() が正常に動くことを確認してください。その上で replaceSelectedNotes() 関数が動くかテストしてみましょう。

Test that the setNotes() function is still working properly. Then try replaceSelectedNotes():

replaceSelectedNotes() の確認
var notes = [];
notes.push( new Note(60, 0, 1, 100, 0) );
notes.push( new Note(64, 1, 1, 100, 0) );
notes.push( new Note(67, 2, 1, 100, 0) );
 
var clip = new Clip();
clip.replaceSelectedNotes(notes);

このコードが本当に期待した通りに動いているか確認するために、C3 E3 G3 以外の note に移動させて、それらの note を選択された状態にした上で、スクリプトを実行しましょう。選択した古い note が消え、C3 E3 G3 に置き換えられれば成功です。

To see that it's actually working as expected, move the C3, E3, and G3 notes that we've been generating to a different time/pitch, make sure the notes are selected, and run this script. The old notes should disappear and the original C3, E3, and G3 should replace them:

補足ですが、note を動かした上で、replaceSelectedNotes 関数ではなく clip.setNotes(notes) 関数を call すると、古い note を保持したまま新しい note を clip に加えます。この挙動はある状況においては望ましいものです。

Note that if you drag the notes like this, and try calling clip.setNotes(notes) instead of replaceSelectedNotes, it adds notes to the clip and keeps the old notes. This behavior may be desirable in some situations, depending on what you are trying to do.

ただし、明示的に Clip の全ての note を選択せずに、全ての note を置き換えたい場合にはどうしたらいいでしょうか。そのために Clip class にもう一つ関数を追加しましょう。

What if we want to replace all the notes in a clip without having to explicitly select them all? Let's add more functions to our Clip class to do this:

さらに関数を追加
Clip.prototype.selectAllNotes = function() {
  this.liveObject.call("select_all_notes");
}
 
Clip.prototype.replaceAllNotes = function(notes) {
  this.selectAllNotes();
  this.replaceSelectedNotes(notes);
}

この関数を以下のように使って全ての note を置き換えることができます。

And now we can replace all the notes in a clip like this:

置き換える
var notes = [];
notes.push( new Note(60, 0, 1, 100, 0) );
notes.push( new Note(64, 1, 1, 100, 0) );
notes.push( new Note(67, 2, 1, 100, 0) );
 
var clip = new Clip();
clip.replaceAllNotes(notes);

Humanizing a Clip

ここまで作ってきた MIDI Clip を操作するための Clip class と Note class は、エラーを起こさない堅牢さを重視してきました。ですが次は面白くて便利な "humanize" 機能を作っていきます。"humanizing" はクオンタイズの反対の機能といっていいでしょう。クオンタイズは note を time grid ぴったりの位置に修正します。Humanizing は反対に、note を grid から少しだけ外して、人間の不正確さを模倣します。この機能を実装するシンプルな方法として、note の time と velocity を少しだけランダムにずらします。以下のコードはこれを実装する方法の一つです。

At this point, we have fairly robust Clip and Note classes for interacting with MIDI clips. So let's build something fun/useful: a "humanize" feature. I think of "humanizing" like the opposite of quantizing. Quantizing aligns your notes to the underlying time grid. Humanizing slightly misaligns the notes from the grid to simulate human imperfections. A simple implementation of this is to slightly randomize the note's times and/or velocities. Here's one way to do it:

humaniing 機能の実装
function humanize(type, maxTimeDelta, maxVelocityDelta) {
  var humanizeVelocity = false,
      humanizeTime = false;
 
  switch(type) {
    case "velocity": humanizeVelocity = true; break;
    case "time": humanizeTime = true; break;
    default: humanizeVelocity = humanizeTime = true;
  }
 
  if(!maxTimeDelta) maxTimeDelta = 0.05;
  if(!maxVelocityDelta) maxVelocityDelta = 5;
  
  clip = new Clip();
  notes = clip.getSelectedNotes();
  notes.forEach(function(note) {
    if(humanizeTime) note.start += maxTimeDelta * (2*Math.random() - 1);
    if(humanizeVelocity) note.velocity += maxVelocityDelta * (2*Math.random() - 1);
  });
  clip.replaceSelectedNotes(notes);
}

time, velocity, time+velocity を変更する各関数を個別に実装するのではなく、関数は一つにして、受け取った引数によって三つの機能を実行できるようにしました。 humanize("velocity") もしくは humanize("time") と実行するれば velocity もしくは time どちらか一つだけをランダムに変更することができますし、何も引数を与えなければ velocity と time の両方をランダムに変更することもできます。

Rather than introduce separate functions for humanize time, velocity, and time+velocity, I wrote one parametrized function to do all three. You can call humanize("velocity") or humanize("time") to randomize just the velocity or just the time, otherwise both will be randomized.

さらに maxTimeDelta and maxVelocityDelta にはデフォルト値を設定しました。これらの値は、time と velocity をダンラムに変更する値の最大値となります。(プラスの方向、マイナスの方向、どちらに動かされるにせよ、その際の最大値です。)デフォルト値は相対的に小さいけれど効果がわかる程度の値を設定しました。

Then we set default values for maxTimeDelta and maxVelocityDelta if needed. These optional parameters affect the maximum change to the time and velocity in either the positive or negative direction. The defaults were chosen to be relatively small but still have a noticeable effect.

注意して欲しいのですが maxTimeDelta を 0 に設定すると (!maxTimeDelta) は true になるので、結果として default 値の 0.05 が適用されることになります。default parameter を JavaScript で扱う他の手法を使えば 0 を指定しても default 値が適用されないようにすることもできます。ですが可読性を優先し、すこし甘い実装を選びました。

Note that if maxTimeDelta is 0, then (!maxTimeDelta) will be true and we'll use the default value 0.05. There are other ways to handle default parameters in JavaScript, so that 0 doesn't turn into the default value like this. I choose this "naive" approach for readability.

ランダム化は Math.random() を実行することで行なっています。この関数は 0.0 から 1.0 までの範囲の floating point number を返します。(2*Math.random() - 1) の計算結果は -1.0 から 1.0 の範囲のランダムな値を返します。ですのでこの式に対して maxTimeDelta をかけることで、-maxTimeDelta から maxTimeDelta までの範囲の値を返すようにしています。(これは maxVelocityDelta に対しても同様です。)このランダムな値が note の start time と velocity に加えられます。

We do the randomization by calling Math.random(), which returns a floating point number between 0.0 and 1.0. The expression (2*Math.random() - 1) calculates a random number between -1.0 and 1.0. So when we multiply that expression by maxTimeDelta, it produces a number between -maxTimeDelta and maxTimeDelta (and similarly for maxVelocityDelta). This random number is added to the note's start time or velocity.

以下が今回作成した script の全体像です。(log()関数だけは含まれていません。)このコードを実行すると現在選択されている time と velocity の両方をランダマイズします。コードの最後の部分で実行している humanize() 関数に色々な値を与えて実行してみましょう。例えば humanize("time", 0.25) とか humanize("velocity", 0, 64) といった具合にです。

Here's the entire script (not including our log() function). Simply running this code will humanize both the time and velocity of the currently selected notes. Experiment with different parameters to the humanize() function call at the end, such as humanize("time", 0.25) and humanize("velocity", 0, 64)

完成形
//--------------------------------------------------------------------
// 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);
}
 
Clip.prototype._sendNotes = function(notes) {
  var liveObject = this.liveObject;
  liveObject.call("notes", notes.length);
  notes.forEach(function(note) {
    liveObject.call("note", note.getPitch(),
                    note.getStart(), note.getDuration(),
                    note.getVelocity(), note.getMuted());
  });
  liveObject.call('done');
}
  
Clip.prototype.replaceSelectedNotes = function(notes) {
  this.liveObject.call("replace_selected_notes");
  this._sendNotes(notes);
}
  
Clip.prototype.setNotes = function(notes) {
  this.liveObject.call("set_notes");
  this._sendNotes(notes);
}
 
Clip.prototype.selectAllNotes = function() {
  this.liveObject.call("select_all_notes");
}
 
Clip.prototype.replaceAllNotes = function(notes) {
  this.selectAllNotes();
  this.replaceSelectedNotes(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 + '}';
}
 
Note.MIN_DURATION = 1/128;
  
Note.prototype.getPitch = function() {
  if(this.pitch < 0) return 0;
  if(this.pitch > 127) return 127;
  return this.pitch;
}
  
Note.prototype.getStart = function() {
  // we convert to strings with decimals to work around a bug in Max
  // otherwise we get an invalid syntax error when trying to set notes
  if(this.start <= 0) return "0.0";
  return this.start.toFixed(4);
}
  
Note.prototype.getDuration = function() {
  if(this.duration <= Note.MIN_DURATION) return Note.MIN_DURATION;
  return this.duration.toFixed(4); // workaround similar bug as with getStart()
}
  
Note.prototype.getVelocity = function() {
  if(this.velocity < 0) return 0;
  if(this.velocity > 127) return 127;
  return this.velocity;
}
  
Note.prototype.getMuted = function() {
  if(this.muted) return 1;
  return 0;
}
  
//--------------------------------------------------------------------
// Humanize behavior
 
function humanize(type, maxTimeDelta, maxVelocityDelta) {
  var humanizeVelocity = false,
      humanizeTime = false;
  
  switch(type) {
    case "velocity": humanizeVelocity = true; break;
    case "time": humanizeTime = true; break;
    default: humanizeVelocity = humanizeTime = true;
  }
  
  if(!maxTimeDelta) maxTimeDelta = 0.05;
  if(!maxVelocityDelta) maxVelocityDelta = 5;
   
  clip = new Clip();
  notes = clip.getSelectedNotes();
  notes.forEach(function(note) {
    if(humanizeTime) note.start += maxTimeDelta * (2*Math.random() - 1);
    if(humanizeVelocity) note.velocity += maxVelocityDelta * (2*Math.random() - 1);
  });
  clip.replaceSelectedNotes(notes);
}
     
//--------------------------------------------------------------------
 
humanize();

Next Steps

この記事では MIDI Clip の note を操作する方法を学んできました。作成したコードを使うことで Clip に note を追加したり、選択している note を置き換えたり、もしくは全ての note を置き換えることができます。これらの機能を使って "fumanize" 機能を作成しました。これを使うことで選択した note の time と velocity を randomizing することができます。

In this article we learned how to manipulate the notes in a MIDI clip. Using the code we built, we can add notes to a clip, replace the selected notes, or replace all the notes. We applied these features to make a "humanize" function for randomizing the time and velocity of the selected notes.

次の記事では humanaize 関数関係のユーザーインターフェイスを作成し、これらの機能を Max for Live デバイス経由でアクセスできる機能として Live に露出させる方法を紹介します。

In the next article we will build a proper user interface around the humanize function, so we can expose these features to Live directly via our Max for Live device.