【雑記】FL StudioのMIDIコントロールの制御をカスタマイズする方法 (Roland A-49)

Roland A-49 というMIDIキーボードに付属しているボタンを FL Studioの制御に使いたくて色々調べたことをまとめておきます。

FL StudioのMIDI制御

Pythonスクリプトの導入

まず環境に合わせたHardwareデバイスフォルダに移動します。macOS環境の場合は

/Users/[ユーザー名]/Documents/Image-Line/FL Studio/Settings/Hardware/

にありました。

ここにわかりやすい名前でフォルダを作成して、”device_name.py” を配置します。

device_name.py にはまず以下のように記述します。

#name=Roland A-49
# ↑使用しているデバイスの名前.
文字コードは「UTF-8」で保存する

Windows環境だと標準では文字コードが Shift-JIS で作られることが多いので、UTF-8で保存するのがおすすめです。

#name=[デバイス名]」という記述をスクリプトの先頭に記述すると、FL Studio の MIDIのデバイス設定にその名前で追加されるようです。

FL Studio のメニューから「OPTIONS > MIDI Settings」を選んでデバイスに対応するスクリプトを設定します。

Script output を表示する

次に スクリプトの実行確認するアウトプットログ画面を表示します。FL Studioのメニューから「VIEW > Script output」を選びます。

するとこのような画面が表示されます。

MIDI信号を確認する

スクリプトを以下のように修正します。

#name=Roland A-49
# ↑使用しているデバイスの名前.

## MIDI入力時に呼び出されるコールバック関数.
def OnNoteOn(event):
    # FLが受け取ったMIDI信号を出力.
    print("note on channel:%d number:%d"%(event.status, event.data1))

## CCメッセージ受け取り.
def OnControlChange(event):
    # FLが受け取ったCCを出力.
    print("CC channel:%d number:%d"%(event.status, event.data1))
    print("- data2:%d"%event.data2)

## プログラムチェンジメッセージのコールバック関数.
def OnProgramChange(event):
    # FLが受け取ったMIDI信号を出力.
    print("program change channel:%d number:%d"%(event.status, event.data1))

## システムエクスクルーシブのコールバック関数.
def OnSysEx(event):
    # FLが受け取ったMIDI信号を出力.
    print("sys ex channel:%d number:%d"%(event.status, event.data1))

## ピッチベンドのコールバック関数.
def OnPitchBend(event):
    # FLが受け取ったMIDI信号を出力.
    print("pitch bend channel:%d number:%d"%(event.status, event.data1))
    print("- data2:%d"%event.data2)

MIDIを制御する場合、おそらく NOTE / CC / プログラムチェンジ / システムエクスクルーシブ / ピッチベンド があればおおよそ網羅できるのでこのようにしました。

device_name.py を保存して、Script output の “Reload script” ボタンをクリックします。

特に問題なくリロードできれば以下のようなログが表示されます。(スクリプトのパスは環境によって変化します)

output
FL Studio Midi scripting version: 28

"/Users/syun77/Documents/Image-Line/FL Studio/Settings/Hardware/RolandA-49/device_name.py" found


init ok

ここでエラーが出る場合には、エラー行数をよく見て修正します。

特に問題がなければ MIDIの入力を確認してみます。例えばMIDIキーボードで ドの音 (C3) を押すと以下のようにログが出力されます。

output
note on channel:144 number:36

後は押したキー・ボタンの番号に対応する処理を書いていきます。

FL Studioを制御するためのAPIリファレンスとしては以下のものがおすすめです。

  • FL Studio API Documentation: 非公式だけれど機能ごとにページが分かれていたり、公式リファレンスにない色々な注意事項が書かれていてとても参考になります

再生位置をノブで切り替えるサンプル

私の使っているMIDIキーボードの Roland A-49 では “C1″ノブに カットオフ(CC#74) のMIDI信号が割り振られています。

これをFL Studio の再生位置を調整するノブに割り当ててみます。

#name=Roland A-49
# ↑使用しているデバイスの名前.

import transport # FLのTransport(Play, Stop, Pause & Record)を操作する.
import patterns # FLのパターン操作.

# 定義が見つからないのでここで定義してみる...
SONGLENGTH_MS = 0 # SONGモードの長さをMSで取得.

# MIDI信号/CCの番号.
A49_C1 = 74 # C1ダイヤルの番号(Cut-off).

VAL_MIN = 0 # 値の最小.
VAL_MAX = 127 # 値は0〜127まで.

## CCメッセージ受け取り.
def OnControlChange(event):
    # FLが受け取ったCCを出力.
    print("CC channel:%d number:%d"%(event.status, event.data1))
    print("- data2:%d"%event.data2)

    if event.data1 == A49_C1:
        # ノブの位置で再生位置を移動してみる.
        if transport.getLoopMode() == 0:
            # パターンモード.
            idx = patterns.patternNumber() # 現在選択しているパターン.
            total = patterns.getPatternLength(idx) # パターンの長さ.
            pos = int(total * event.data2 / VAL_MAX)
            print("pos:", pos , " total:", total)
            transport.setSongPos(1.0 * pos / total)
        else:
            # ソングモード.
            total = transport.getSongLength(SONGLENGTH_MS) # 曲の長さ.
            pos = total * event.data2 / VAL_MAX
            print("pos:", pos , " total:", total)
            transport.setSongPos(pos, SONGLENGTH_MS)

        event.handled = True # FLに処理させる.

FL Studioを制御するには import文 で該当する機能を指定する必要があります。”transport” は再生や停止に関する機能で、”patterns” はパターンモードにおける制御を行うものです。

次に対応する MIDI信号に対応する処理を書きます。ここでの注意点は引数の “event.handled” の値を True にする必要がある、ということです。MIDI信号があるたびに FL Studio に処理させるのは重たいので、何らかの制御をしたい場合のみこの値を True にします。

Roland A-49 での実装例

私の好みで Roland A-49 は以下のように割り当てました。

  • C1: 再生位置の変更
  • C2: 現在選んでいるチャンネルの音量を変更する
  • S1: 再生ボタン
  • S2: 停止ボタン
  • モジュレーション・ピッチベンド:ピアノロール / チャンネルラック / プレイリスト に切り替える

Roland A-49 は ライブ演奏用途・同社のハードウェアシンセである “JUNO” との連携を前提とした設計となっていて、一般的なDTMの用途に合っていない気がしたので、このように変更しました。

それと個人的に「モジュレーション・ピッチベンド」は使わないので、FL Studioの制御に割り当てています。

注意点として、「S1」「S2」は工場出荷状態では「プログラムチェンジ」に割り当てられています。どうやらFL Studioは “プログラムチェンジ” 信号 をチャンネルラックの移動に割り当てているので、CCメッセージに変更します。これは Roland A-49独自の操作ですが「FUNCTION > S1 / S2 > CTRL CHANGE > [NUMERIC ENTRY] > – / +」で CC に割り当てるようにしました。(例えば S1 / S2 を CC#2 / CC#3 に割り当て)。

参考までに公式の取扱説明書の抜粋です。

Roland A-49 取扱説明書 より抜粋

それと Roland A-49 には “Super NATURAL” モードというのがあって、そのモードのときには “S1 / S2” をプリセット変更ボタンに切り替えるようにしてみました。

スクリプトの実装例

#name=Roland A-49
# ↑使用しているデバイスの名前.

import transport # FLのTransport(Play, Stop, Pause & Record)を操作する.
import channels # FLのチャンネルラック操作.
import patterns # FLのパターン操作.
import plugins # FLのプラグイン操作.
import ui # FLのUI操作.

# 定義が見つからないのでここで定義してみる...
SONGLENGTH_MS = 0 # SONGモードの長さをMSで取得.
## ウィンドウID.
widMixer = 0 # ミキサー.
widChannelRack = 1 # チャンネルラック.
widPlaylist = 2 # プレイリスト.
widPianoRoll = 3 # ピアノロール.

# MIDI信号/CCの番号.
A49_MOD = 1 # モジュレーションの番号.
A49_C1 = 74 # C1ダイヤルの番号(Cut-off).
A49_C2 = 71 # C2ダイヤルの番号(Resonace).
A49_C1_SN  = 16 # SuperNATURALモード時のC1の番号.
A49_C2_SN  = 17 # SuperNATURALモード時のC2の番号.
A49_S1_SN = 80 # SuperNATURALモード時のS1の番号.
A49_S2_SN = 81 # SuperNATURALモード時のS2の番号.
##  FUNCTIONキーで割り当てたCC.
A49_S1 = 2
A49_S2 = 3

VAL_MIN = 0 # 値の最小.
VAL_MAX = 127 # 値は0〜127まで.

## MIDI入力時に呼び出されるコールバック関数.
def OnNoteOn(event):
    # イベントを処理する場合はこれをTrueにする.
    event.handled = False
    
    # FLが受け取ったMIDI信号を出力.
    print("note on channel:%d number:%d"%(event.status, event.data1))

## CCメッセージ受け取り.
def OnControlChange(event):
    # イベントを処理する場合はこれをTrueにする.
    event.handled = False
    
    # FLが受け取ったCCを出力.
    print("CC channel:%d number:%d"%(event.status, event.data1))
    print("- data2:%d"%event.data2)
    #print("- pressure:%d"%event.pressure)
    #print("- progNum:%d"%event.progNum)
    #print("- controlNum:%d"%event.controlNum)
    #print("- pitchBend:%d"%event.pitchBend)
    #print("- isIncrement:%d"%event.isIncrement)
    #print("- res:%d"%event.res)
    #print("- inEv:%d"%event.inEv)
    #print("- outEv:%d"%event.outEv)
    #print("- midiId:%d"%event.midiId)
    #print("- midiChan:%d"%event.midiChan)
    #print("- midiChanEx:%d"%event.midiChanEx)
    
    # 選択しているチャンネル番号を取得する.
    channel_idx = channels.channelNumber()
    #print(plugins.getPluginName(channel_idx))
    # プラグインのパラメータをすべて出力.
    #for i in range(plugins.getParamCount(channel_idx)):
    #    print(plugins.getParamName(i, channel_idx), ":", plugins.getParamValue(i, channel_idx))

    # 再生位置を移動させられる.
    #print("ui.jog:", ui.jog(1))
    
    if event.data1 == A49_MOD:
        print("setFocused(widPianoRoll)")
        ui.setFocused(widPianoRoll) # ピアノロールにフォーカスする.
        event.handled = True # FLに処理させる.
    
    if event.data1 == A49_C1:
        # ノブの位置で再生位置を移動してみる.
        if transport.getLoopMode() == 0:
            # パターンモード.
            idx = patterns.patternNumber() # 現在選択しているパターン.
            total = patterns.getPatternLength(idx) # パターンの長さ.
            pos = int(total * event.data2 / VAL_MAX)
            print("pos:", pos , " total:", total)
            transport.setSongPos(1.0 * pos / total)
        else:
            # ソングモード.
            total = transport.getSongLength(SONGLENGTH_MS) # 曲の長さ.
            pos = total * event.data2 / VAL_MAX
            print("pos:", pos , " total:", total)
            transport.setSongPos(pos, SONGLENGTH_MS)
        event.handled = True # FLに処理させる.
    
    if event.data1 == A49_C2:
        # チャンネルボリュームを変更する.
        vol = 1.0 * event.data2 / VAL_MAX
        channels.setChannelVolume(channel_idx, vol)
        print("channel vol:%3.2f"%vol)
        event.handled = True # FLに処理させる.
    
    if event.data1 == A49_S1 and event.data2 > 0:
        print("play/stop") # 再生/停止する.
        transport.start()
        event.handled = True # FLに処理させる.
    if event.data1 == A49_S2 and event.data2 > 0:        
        print("stop") # 停止する.
        transport.stop()
        event.handled = True # FLに処理させる.
    
    if event.data1 == A49_S1_SN and event.data2 > 0:
        # プリセット変更(1つ前).
        print("prevPreset")
        plugins.prevPreset(channel_idx)
        event.handled = True # FLに処理させる.
    if event.data1 == A49_S2_SN and event.data2 > 0:
        # プリセット変更(1つ先).
        print("nextPreset")
        plugins.nextPreset(channel_idx)
        event.handled = True # FLに処理させる.

"""        
    # S1/S2をCCに割り当てない場合はこちらで再生/停止する.
    if event.data1 == A49_C2:
        # 再生/停止処理.
        if event.data2 == VAL_MIN:
            print("stop") # 停止する.
            transport.stop() # C2が最小になったら停止.
            event.handled = True # FLに処理させる.
        if event.data2 == VAL_MAX:
            print("play/stop") # 再生/停止する.
            transport.start() # C2が最大になったら再生/停止.
            event.handled = True # FLに処理させる.
"""

## プログラムチェンジメッセージのコールバック関数.
def OnProgramChange(event):
    # イベントを処理する場合はこれをTrueにする.
    event.handled = False
    
    # FLが受け取ったMIDI信号を出力.
    print("program change channel:%d number:%d"%(event.status, event.data1))

## システムエクスクルーシブのコールバック関数.
def OnSysEx(event):
    # イベントを処理する場合はこれをTrueにする.
    event.handled = False
    
    # FLが受け取ったMIDI信号を出力.
    print("sys ex channel:%d number:%d"%(event.status, event.data1))
    
## ピッチベンドのコールバック関数.
def OnPitchBend(event):
    # イベントを処理する場合はこれをTrueにする.
    event.handled = False
    
    # FLが受け取ったMIDI信号を出力.
    print("pitch bend channel:%d number:%d"%(event.status, event.data1))
    print("- data2:%d"%event.data2)
    
    if event.data2 > 64:
        # ピッチを上げる.
        print("setFocused(widPlaylist)")
        ui.setFocused(widPlaylist) # プレイリストにフォーカスする.
        event.handled = True # FLに処理させる.
    elif event.data2 < 64:
        # ピッチを下げる.
        print("setFocused(widChannelRack)")
        ui.setFocused(widChannelRack) # チャンネルラックにフォーカスする.
        event.handled = True # FLに処理させる.

これで、使い道のなかったMIDIコントロールボタンがFL Studioと連携するようになって、かなり使いやすくなりました。

Roland A-49 はキータッチがとても良くておすすめの MIDIキーボードです。

参考