困った経緯
- Broadway LITEs(ブラス音源)をセールで購入!1
- アーティキュレーション機能にキースイッチを登録するため、マニュアルをダウンロード
- PLEASE READ AND FOLLOW THIS MANUAL!って赤字で言われたので全力で読んだ2
- Fall Downなどの奏法は、Sustainのノートの途中でキースイッチを押すと滑らかに切り替わる
- ↑アーティキュレーション機能でどうやって表現すればいいんだ…
結論
Logic Pro内蔵のScripter MIDIプラグインを使いました。
//-----------------------------------------------------------------------------
// 隣接するノートを結合
// for Logic Pro Scripter
//-----------------------------------------------------------------------------
var NeedsTimingInfo = true;
let noteOffEvents = new Array(2032);
/**
* AUプラグインがMIDIイベントを受信した際に呼び出される関数
* @param {Event} event 受信したMIDIイベント
*/
function HandleMIDI(event)
{
// MIDIノートオフイベントの格納先Index
const mergeNoteIndex = event.pitch * event.channel - 1;
if(event instanceof NoteOff){
noteOffEvents[mergeNoteIndex] = event;
} else if(event instanceof NoteOn) {
if(noteOffEvents[mergeNoteIndex] === undefined){
// 結合対象ではないためMIDIノートオンイベントを送信
event.sendAfterMilliseconds(0.1);
} else {
// 結合対象のため、MIDIノートオンイベントを破棄
noteOffEvents[mergeNoteIndex] = undefined;
}
} else if(event instanceof ControlChange && event.number === 120) {
// CC:120 All Sound Off
// 0.1ms後に送信したMIDIノートオンが残ってしまうのを防ぐため、0.2ms後に送信
event.sendAfterMilliseconds(0.2);
} else {
event.send();
}
}
/**
* 一定の処理ブロックごとに呼び出される関数
*/
function ProcessMIDI(){
for (let i = 0; i < noteOffEvents.length; i++){
if(noteOffEvents[i] === undefined){
continue;
}
let shouldSend = hasPastInterval(noteOffEvents[i], 10 ^ -32);
if(shouldSend){
noteOffEvents[i].send();
noteOffEvents[i] = undefined;
}
}
}
/**
* MIDIイベント発生後、一定の時間経過したことを判定する関数
* @param {Event} event イベント
* @param {Number} interval 時間(拍数)
*/
function hasPastInterval(event, interval){
let info = GetTimingInfo();
let beatCount = info.blockStartBeat - event.beatPos;
if(info.playing && beatCount > 0){
if(beatCount > interval){
return true;
} else {
return false;
}
}
return true;
}
仕組みと過程が気になる人向けの情報
アーティキュレーション機能とは
Logic Proでは、1つのノートに対して1つのアーティキュレーションを設定することができます。
本来はLogic Pro付属音源向けに特化した機能ですが、Logic Pro側で各アーティキュレーションにキースイッチを割り当てることで、サードパーティ製の音源で使用する場合もノートに設定したアーティキュレーションに合わせて自動的にキースイッチを送信してくれます。
ピアノロールからキースイッチのノートが消えて滅茶苦茶スッキリする上に、
特定のアーティキュレーションのみを全選択することもできるので
「Sustainの音だけタイミングを手前に…」みたいなことができてめっちゃ便利です。
しかし、ノートの途中でアーティキュレーションを切り替えることは不可能です。
『1ノートにつき1つのアーティキュレーション』の前提が崩れてしまうので当たり前です。
Logic Pro付属音源はどうしているのか
Logic Pro付属のStudio BrassやStudio Stringsには、ノートの途中で奏法を切り替える手段が用意されています。
Logic Proで、スムーズなフォールまたはDoitを作成するには、任意のノートの直後に同じピッチの第2(「Fall」または「Doit」アーティキュレーション)のノートを追加します。前のノートからのギャップはまったく生じないか、ごくわずかになります。
ノートの間隔を狭めて配置すると、次の音に上手く繋がる仕組みになっています。
これ、やりたい!!
どう実現するか
単純に2つのノートを配置しただけでは、2つ目の音も発音されてしまいます。音が繋がりません。
Logic Pro付属音源と同じ動作をさせるには、
MIDIノートオフイベントの直後に同じピッチのMIDIノートオンイベントが流れてきたら、無視する
ことができれば良さそうです。
分かる人にしか分からないコード解説
Scripterは、Logic付属のMIDIプラグインです。
AUプラグインに流れるMIDI信号を JavaScript で加工することができます。
公式マニュアルの解説を読み解くには、JavaScript、MIDIイベントについてある程度知っている必要があります。
グローバル変数
var NeedsTimingInfo = true;
let noteOffEvents = new Array(2032);
1行目:DAWの再生状態を取得するためのTimingInfoオブジェクトを使用するための記述です。(Scripter APIの仕様)
2行目:MIDIノートオフイベントを入れるための配列を用意しています。(ピッチ127個 x 16ch)
Handle MIDI関数
/**
* AUプラグインがMIDIイベントを受信した際に呼び出される関数
* @param {Event} event 受信したMIDIイベント
*/
function HandleMIDI(event)
{
// MIDIノートオフイベントの格納先Index
const mergeNoteIndex = event.pitch * event.channel - 1;
if(event instanceof NoteOff){
noteOffEvents[mergeNoteIndex] = event;
} else if(event instanceof NoteOn) {
if(noteOffEvents[mergeNoteIndex] === undefined){
// 結合対象ではないためMIDIノートオンイベントを送信
event.sendAfterMilliseconds(0.1);
} else {
// 結合対象のため、MIDIノートオンイベントを破棄
noteOffEvents[mergeNoteIndex] = undefined;
}
} else if(event instanceof ControlChange && event.number === 120) {
// CC:120 All Sound Off
// 0.1ms後に送信したMIDIノートオンが残ってしまうのを防ぐため、0.2ms後に送信
event.sendAfterMilliseconds(0.2);
} else {
event.send();
}
}
HandleMIDI 関数は、AUプラグインがMIDIイベントを受信するたびに呼び出されます。
MIDIイベントの内容は第1引数に格納されています。
第1引数から渡ってきたオブジェクトのsend()メソッドを実行しない限り、そのMIDIイベントは実行されません。
MIDIイベントであれば、ノートもコントロールチェンジも関係なく流れてくるため、
instanceof 演算子でMIDIイベントの種類を判定し、処理分岐しています。
MIDIイベント(オブジェクト)の種類 | 処理 |
---|---|
NoteOff | noteOffEvents配列にイベントオブジェクトを追加する。この時点ではMIDIイベントを送信しない。 →次のノートが流れてくるか、一定拍数経過するまでMIDIノートオフを待機させる |
NoteOn | noteOffEvents配列に同じピッチ・同じMIDIチャンネルのオブジェクトが存在しているか判定する。 →存在しない場合:MIDIイベントを0.1ms後に送信する。 →存在する場合:MIDIイベントを送信せず、noteOffEvents配列からオブジェクトを破棄する。 |
ControlChange (Number:120) | 全ての音を停止するMIDIメッセージ。DAWの停止ボタンを押した際に送信される。NoteOnイベントよりも前に送信すると音が残ってしまうため、0.2ms後に送信する。 |
上記以外 | MIDIイベントをそのまま送信する。 |
MIDIノートオフを待機させているため、MIDIノートオフを送信する前に、次のノートのMIDIノートオンが先に送信されてしまいます。
これを防ぐために、MIDIノートオンは0.1ms後に送信するようにしています。
ProcessMIDI関数
/**
* 一定の処理ブロックごとに呼び出される関数
*/
function ProcessMIDI(){
for (let i = 0; i < noteOffEvents.length; i++){
if(noteOffEvents[i] === undefined){
continue;
}
let shouldSend = hasPastInterval(noteOffEvents[i], 10 ^ -32);
if(shouldSend){
noteOffEvents[i].send();
noteOffEvents[i] = undefined;
}
}
}
ProcessMIDI関数は、DAWから一定周期で呼び出されます。
noteOffEvents配列に貯めたMIDIノートオフイベントをhasPastInterval関数(後述)で検証し、
trueが返却された場合にMIDIノートオフイベントを送信します。
送信し終わったMIDIノートオフイベントは、noteOffEvents配列から削除します。
第2引数を10の-32乗にしてみましたが、適当な値です。
ここで設定する数値が、ノートとノートの間隔が何拍開いていたら音が繋がるか
に関係しています。
いい感じに短ければ良いと思います。(適当)
hasPastInterval関数
/**
* MIDIイベント発生後、一定の時間経過したことを判定する関数
* @param {Event} event イベント
* @param {Number} interval 時間(拍数)
*/
function hasPastInterval(event, interval){
let info = GetTimingInfo();
let beatCount = info.blockStartBeat - event.beatPos;
if(info.playing && beatCount > 0){
if(beatCount > interval){
return true;
} else {
return false;
}
}
return true;
}
引数 event から受け取ったMIDIイベントを判定し、
引数 interval の拍数経過している場合、trueを返却する関数です。
TimingInfoオブジェクトから、現在の再生位置を拍数で取得することができます。
現在の再生位置 - MIDIノートオフの拍数
を計算することで、
1音目のノートの終端からどれだけ拍数が経過したかを判定しています。
結果
最後に
結果的に、「隣接する音符を結合する処理」が出来上がりました。
一瞬Cubaseのエクスプレッションマップが羨ましくなりました3が、なんとかなりました。
簡単な記述でMIDI信号を加工できる仕組みがあると本当に助かります。
Logic Proはすごい。
今後は、実際に使用していきながらコードを修正していく予定です。
参考にした記事など