MIDI controllerでLinux desktopのモニター入力と明るさを操作する

目的

KORG nanoKONTROL2 のような USB MIDI controller を、Linux desktop の物理操作盤として使いたいのでGolangで作った。

midi-switch

MIDI event を ALSA sequencer で読み、ddcutil でディスプレイの入力ソース、明るさ、コントラストを操作し、必要なら pactl で音量も操作する。

構成は単純にする。

USB MIDI controller
  -> ALSA sequencer event
  -> small daemon
  -> ddcutil / pactl
  -> display input / brightness / contrast / volume

できた感想

リモートワークで窓際で作業してるのですが時間帯や天気によってディスプレーの明るさやコントラストを変えたくなるのですが MIDIのスライドバーとかボリュームボタンで変えられて嬉しいです あとラズパイの検証時、ディスプレー入力をちょいちょい変えたくなるので便利になった(稀に起動初期の画面がみたい事がある)

方針

初期実装では MIDI library へ直接つながず、aseqdump の stdout を読む方式で十分に価値がある。

理由:

  • aconnect -laseqdump の結果をそのまま調査ログとして使える
  • MIDI button、knob、slider の note / controller 番号を目視で確定しやすい
  • ALSA sequencer port が見えていれば Go や shell からすぐ扱える
  • 仕様が固まってから gomidi / RtMidi などへ移行できる

ddcutil 側も、まずは ddcutil detectgetvcpsetvcp を外部 command として呼ぶ。 daemon 内で DDC/CI protocol を直接実装しないことで、モニター差分の切り分けを ddcutil に寄せられる。

調査手順

MIDI device を確認する。

lsusb
cat /proc/asound/cards
aconnect -l

ALSA sequencer port が分かったら、event を見る。

aseqdump -p 24:0

24:0 のような port 番号は、起動順や抜き差しで変わる。 daemon では固定 port 番号だけに頼らず、nanoKONTROL2 のような port name を起動時に探す方が安定する。

DDC/CI 側は次を確認する。

ddcutil detect
ddcutil --display 1 capabilities
ddcutil --display 1 getvcp 60
ddcutil --display 1 setvcp 60 0x11
ddcutil --display 1 setvcp 60 0x12

VCP 0x60 は Input Source である。 モニターによって値の意味は異なるため、0x11 / 0x12 を固定知識として扱わず、必ず capabilities と実切替で確認する。

display指定

ddcutil --display <n> は検出順に依存する可能性がある。 常用 daemon では次のいずれかへ寄せる。

  • --bus <n> を config に書く
  • ddcutil detect を parse して model / serial で対象を選ぶ
  • config に display / bus / serial を持たせ、起動時に一致確認する

入力切替

入力切替は button event に割り当てる。

設定は command を配列にする。 sh -c へ丸投げしない方が、quote と意図しない shell 展開の問題を避けやすい。

actions:
  - name: display1-input-hdmi1
    event:
      type: control_change
      controller: 50
      value_min: 1
    ddc:
      bus: 2
      vcp: "60"
      value: "0x11"
      noverify: true

  - name: display1-input-hdmi2
    event:
      type: control_change
      controller: 66
      value_min: 1
    ddc:
      bus: 2
      vcp: "60"
      value: "0x12"
      noverify: true

setvcp 60 は、入力切替により画面や DDC の応答状態が変わるため、書き込み後の verify が失敗することがある。 実際に画面入力が切り替わっていれば成功として扱い、必要なモニターでは --noverify を検討する。

1つの MIDI button で複数の display を同じ入力へ切り替える場合は、同じ event を持つ action を display 数だけ並べる。 daemon は一致した action を順に実行し、ddcutil 呼び出しは直列化する。 次の例では controller 51 で3台を HDMI-1 に切り替える。

actions:
  - name: all-displays-input-hdmi1-display1
    event:
      type: control_change
      controller: 51
      value_min: 1
    ddc:
      bus: 2
      vcp: "60"
      value: "0x11"
      noverify: true

  - name: all-displays-input-hdmi1-display2
    event:
      type: control_change
      controller: 51
      value_min: 1
    ddc:
      bus: 3
      vcp: "60"
      value: "0x11"
      noverify: true

  - name: all-displays-input-hdmi1-display3
    event:
      type: control_change
      controller: 51
      value_min: 1
    ddc:
      bus: 4
      vcp: "60"
      value: "0x11"
      noverify: true

HDMI-2 へまとめて切り替える場合も同じ考え方で、別の button の controller と VCP 0x60 の HDMI-2 値を使う。

actions:
  - name: all-displays-input-hdmi2-display1
    event:
      type: control_change
      controller: 67
      value_min: 1
    ddc:
      bus: 2
      vcp: "60"
      value: "0x12"
      noverify: true

  - name: all-displays-input-hdmi2-display2
    event:
      type: control_change
      controller: 67
      value_min: 1
    ddc:
      bus: 3
      vcp: "60"
      value: "0x12"
      noverify: true

  - name: all-displays-input-hdmi2-display3
    event:
      type: control_change
      controller: 67
      value_min: 1
    ddc:
      bus: 4
      vcp: "60"
      value: "0x12"
      noverify: true

電源制御

DDC/CI の Power Mode は VCP 0xD6 を使う。 この環境の PHL 224E5 では D6 に次の値がある。

01: DPM: On, DPMS: Off
04: DPM: Off, DPMS: Off
05: Write only value to turn off display

off/sleep は D6=05 で送れる。 一方で off 後は通常の DDC check が失敗することがあり、on は --skip-ddc-checks を付けて D6=01 を write する方が安定する。 この挙動は monitor 実装依存なので、別機種では必ず1台だけで復帰できることを確認する。

actions:
  - name: all-displays-power-off-display1
    event:
      type: control_change
      controller: 43
      value_min: 1
    ddc:
      bus: 2
      vcp: "D6"
      value: "05"
      noverify: true

  - name: all-displays-power-off-display2
    event:
      type: control_change
      controller: 43
      value_min: 1
    ddc:
      bus: 3
      vcp: "D6"
      value: "05"
      noverify: true

  - name: all-displays-power-off-display3
    event:
      type: control_change
      controller: 43
      value_min: 1
    ddc:
      bus: 4
      vcp: "D6"
      value: "05"
      noverify: true

  - name: all-displays-power-on-display1
    event:
      type: control_change
      controller: 44
      value_min: 1
    command:
      - ddcutil
      - --bus
      - "2"
      - --skip-ddc-checks
      - --sleep-multiplier
      - "3"
      - --maxtries
      - "10,10,10"
      - --noverify
      - setvcp
      - D6
      - "01"

  - name: all-displays-power-on-display2
    event:
      type: control_change
      controller: 44
      value_min: 1
    command:
      - ddcutil
      - --bus
      - "3"
      - --skip-ddc-checks
      - --sleep-multiplier
      - "3"
      - --maxtries
      - "10,10,10"
      - --noverify
      - setvcp
      - D6
      - "01"

  - name: all-displays-power-on-display3
    event:
      type: control_change
      controller: 44
      value_min: 1
    command:
      - ddcutil
      - --bus
      - "4"
      - --skip-ddc-checks
      - --sleep-multiplier
      - "3"
      - --maxtries
      - "10,10,10"
      - --noverify
      - setvcp
      - D6
      - "01"

sliderとknob

slider や knob は control_change を大量に出す。 そのまま全 event で ddcutil を実行すると、I2C/DDC 側が詰まりやすい。

連続値では次の方針にする。

  • MIDI 0..127 を DDC 0..100 に丸めて変換する
  • controller ごとに throttle を入れる
  • queue を溜め込まず、最新値だけを後段へ渡す
  • ddcutil 実行は全 action で直列化する

変換例。

ddc_value = round(midi_value * 100 / 127)

明るさは VCP 0x10、コントラストは VCP 0x12 を使うことが多い。 ただしこれもモニターの capabilities で確認する。

音量は pactl で default sink を操作できる。

pactl set-sink-volume @DEFAULT_SINK@ 50%

音量操作は DDC より軽いが、MIDI slider の event は多いため throttle は入れておく。

複数 display を同じ slider / knob でまとめて変更する場合も、同じ event を持つ action を display 数だけ並べる。 各 action が最新 MIDI value を保持し、throttle 後に ddcutil を直列実行する。

actions:
  - name: all-displays-brightness-display1
    event:
      type: control_change
      controller: 3
    ddc:
      bus: 2
      vcp: "10"
      scale_from_midi: true
      min: 0
      max: 100
      noverify: true
      throttle_ms: 500

  - name: all-displays-brightness-display2
    event:
      type: control_change
      controller: 3
    ddc:
      bus: 3
      vcp: "10"
      scale_from_midi: true
      min: 0
      max: 100
      noverify: true
      throttle_ms: 500

  - name: all-displays-brightness-display3
    event:
      type: control_change
      controller: 3
    ddc:
      bus: 4
      vcp: "10"
      scale_from_midi: true
      min: 0
      max: 100
      noverify: true
      throttle_ms: 500

コントラストも VCP code を 0x12 に変えるだけで同じ構造にできる。

actions:
  - name: all-displays-contrast-display1
    event:
      type: control_change
      controller: 19
    ddc:
      bus: 2
      vcp: "12"
      scale_from_midi: true
      min: 0
      max: 100
      noverify: true
      throttle_ms: 500

  - name: all-displays-contrast-display2
    event:
      type: control_change
      controller: 19
    ddc:
      bus: 3
      vcp: "12"
      scale_from_midi: true
      min: 0
      max: 100
      noverify: true
      throttle_ms: 500

  - name: all-displays-contrast-display3
    event:
      type: control_change
      controller: 19
    ddc:
      bus: 4
      vcp: "12"
      scale_from_midi: true
      min: 0
      max: 100
      noverify: true
      throttle_ms: 500

systemd user service

ログイン中だけ常駐すればよいなら systemd user service で十分である。

mkdir -p ~/.local/bin ~/.config/midi-switch ~/.config/systemd/user
install -m 0755 midi-switch ~/.local/bin/midi-switch
install -m 0644 config.example.yaml ~/.config/midi-switch/config.yaml
install -m 0644 systemd/midi-switch.service ~/.config/systemd/user/midi-switch.service
systemctl --user daemon-reload
systemctl --user enable --now midi-switch.service
journalctl --user -u midi-switch.service -f

ログインしていない状態でも動かしたい場合だけ loginctl enable-linger を検討する。 通常の desktop 操作用途では、ログイン時に起動すればよい。

障害ポイント

ddcutil は I2C bus を掴むため、複数の ddcutil を並列実行すると flock diagnostic が出ることがある。 daemon 側で DDC action を直列化する。

systemd user service でだけ DDC が失敗する場合は、次を見る。

journalctl --user -u midi-switch.service
ls -l /dev/i2c-*
groups

必要なら i2c group への追加と再ログインを行う。

sudo usermod -aG i2c "$USER"

MIDI event が見えない場合は、USB 認識、ALSA card、ALSA sequencer port の順に見る。

lsusb
cat /proc/asound/cards
aconnect -l
aseqdump -p <port>