MIDI controllerでLinux desktopのモニター入力と明るさを操作する
Posted:
目的
KORG nanoKONTROL2 のような USB MIDI controller を、Linux desktop の物理操作盤として使いたいのでGolangで作った。
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 -lとaseqdumpの結果をそのまま調査ログとして使える- MIDI button、knob、slider の note / controller 番号を目視で確定しやすい
- ALSA sequencer port が見えていれば Go や shell からすぐ扱える
- 仕様が固まってから gomidi / RtMidi などへ移行できる
ddcutil 側も、まずは ddcutil detect、getvcp、setvcp を外部 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を DDC0..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>