LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-27

困った経緯

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

結論

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

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;
}

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

アーティキュレーション機能とは

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」アーティキュレーション)のノートを追加します。前のノートからのギャップはまったく生じないか、ごくわずかになります。

Fall Medium.gif
ノートの間隔を狭めて配置すると、次の音に上手く繋がる仕組みになっています。
これ、やりたい!!

どう実現するか

実現前.gif
単純に2つのノートを配置しただけでは、2つ目の音も発音されてしまいます。音が繋がりません。

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

分かる人にしか分からないコード解説

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

グローバル変数

Scripter.js
var NeedsTimingInfo = true;
let noteOffEvents = new Array(2032);

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

Handle MIDI関数

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イベントを送信しない。
→次のノートが流れてくるか、一定拍数経過するまで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関数

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関数

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
SustainにFall Downが繋がった!!

最後に

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

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

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

参考にした記事など


  1. Full版が欲しい(記事執筆時点で$2295) 

  2. マニュアルに書いてありました。この音源を使っている方はぜひ読んであげてください。 

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0