Logic Proのアーティキュレーション機能でノート発音中にキースイッチを切り替えたい

#困った経緯
* Broadway LITEs（ブラス音源）をセールで購入！[^1]
* アーティキュレーション機能にキースイッチを登録するため、マニュアルをダウンロード
* <font color="Red">PLEASE READ AND FOLLOW THIS MANUAL!</font>って赤字で言われたので全力で読んだ[^2]
* Fall Downなどの奏法は、Sustainのノートの途中でキースイッチを押すと滑らかに切り替わる
* ↑アーティキュレーション機能でどうやって表現すればいいんだ…

[^1]: Full版が欲しい（記事執筆時点で$2295）
[^2]: マニュアルに書いてありました。この音源を使っている方はぜひ読んであげてください。

#結論
Logic Pro内蔵のScripter MIDIプラグインを使いました。

https://support.apple.com/ja-jp/guide/logicpro/lgce728c68f6/mac

```javascript:Scripter.js
//-----------------------------------------------------------------------------
// 隣接するノートを結合
// 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;
}
```

#仕組みと過程が気になる人向けの情報

##アーティキュレーション機能とは
https://support.apple.com/ja-jp/guide/logicpro/lgcp33a49091/mac

Logic Proでは、1つのノートに対して1つのアーティキュレーションを設定することができます。
本来はLogic Pro付属音源向けに特化した機能ですが、Logic Pro側で各アーティキュレーションにキースイッチを割り当てることで、サードパーティ製の音源で使用する場合もノートに設定したアーティキュレーションに合わせて自動的にキースイッチを送信してくれます。

ピアノロールからキースイッチのノートが消えて滅茶苦茶スッキリする上に、
特定のアーティキュレーションのみを全選択することもできるので
「Sustainの音だけタイミングを手前に…」みたいなことができてめっちゃ便利です。

しかし、ノートの途中でアーティキュレーションを切り替えることは不可能です。
『1ノートにつき1つのアーティキュレーション』の前提が崩れてしまうので当たり前です。

##Logic Pro付属音源はどうしているのか
Logic Pro付属のStudio BrassやStudio Stringsには、ノートの途中で奏法を切り替える手段が用意されています。

https://support.apple.com/ja-jp/guide/logicpro/lgcp99c44c8f/mac
>Logic Proで、スムーズなフォールまたはDoitを作成するには、任意のノートの直後に同じピッチの第2（「Fall」または「Doit」アーティキュレーション）のノートを追加します。前のノートからのギャップはまったく生じないか、ごくわずかになります。

![Fall Medium.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1944795/5b5d6289-b69e-7149-c92a-23d25d34b3a1.gif)
ノートの間隔を狭めて配置すると、次の音に上手く繋がる仕組みになっています。
これ、やりたい！！

##どう実現するか
![実現前.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1944795/9ecf68c2-18c5-f57d-0d9f-2ed27bfe9c5f.gif)
単純に2つのノートを配置しただけでは、2つ目の音も発音されてしまいます。音が繋がりません。

Logic Pro付属音源と同じ動作をさせるには、
__MIDIノートオフイベントの直後に同じピッチのMIDIノートオンイベントが流れてきたら、無視する__
ことができれば良さそうです。

##分かる人にしか分からないコード解説
https://support.apple.com/ja-jp/guide/logicpro/lgcecc16550d/10.6.2

Scripterは、Logic付属のMIDIプラグインです。
AUプラグインに流れるMIDI信号を JavaScript で加工することができます。
公式マニュアルの解説を読み解くには、JavaScript、MIDIイベントについてある程度知っている必要があります。

###グローバル変数
```javascript:Scripter.js
var NeedsTimingInfo = true;
let noteOffEvents = new Array(2032);
```

1行目:DAWの再生状態を取得するためのTimingInfoオブジェクトを使用するための記述です。（Scripter APIの仕様）
2行目:MIDIノートオフイベントを入れるための配列を用意しています。（ピッチ127個 x 16ch）

###Handle MIDI関数
```javascript:Scripter.js
/**
 * 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イベントを送信しない。<br>→次のノートが流れてくるか、一定拍数経過するまでMIDIノートオフを待機させる|
|NoteOn|noteOffEvents配列に同じピッチ・同じMIDIチャンネルのオブジェクトが存在しているか判定する。<br>→存在しない場合：MIDIイベントを0.1ms後に送信する。<br>→存在する場合：MIDIイベントを送信せず、noteOffEvents配列からオブジェクトを破棄する。|
|ControlChange (Number:120)|全ての音を停止するMIDIメッセージ。DAWの停止ボタンを押した際に送信される。NoteOnイベントよりも前に送信すると音が残ってしまうため、0.2ms後に送信する。|
|上記以外|MIDIイベントをそのまま送信する。|

MIDIノートオフを待機させているため、MIDIノートオフを送信する前に、次のノートのMIDIノートオンが先に送信されてしまいます。
これを防ぐために、MIDIノートオンは0.1ms後に送信するようにしています。

###ProcessMIDI関数
```javascript:Scripter.js
/**
 * 一定の処理ブロックごとに呼び出される関数
 */
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関数
```javascript:Scripter.js
/**
 * 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音目のノートの終端からどれだけ拍数が経過したかを判定しています。

###結果
![実現後.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1944795/dbb198e9-8b60-2190-6eaa-de388022224e.gif)
__SustainにFall Downが繋がった！！__

#最後に

結果的に、「隣接する音符を結合する処理」が出来上がりました。

一瞬Cubaseのエクスプレッションマップが羨ましくなりました[^3]が、なんとかなりました。
簡単な記述でMIDI信号を加工できる仕組みがあると本当に助かります。
Logic Proはすごい。

[^3]: 最近Cubaseが羨ましくなることがありがちです。それでもまだLogicを使い続けたい理由のほうがたくさんあります。

今後は、実際に使用していきながらコードを修正していく予定です。

#参考にした記事など

https://support.apple.com/ja-jp/guide/logicpro/lgcecc16550d/mac

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference

https://www.logicprohelp.com/forum/viewtopic.php?t=132827

https://fablesounds.com/broadway-lites/