pyusbでオーディオインターフェースコントローラーを作る (2)

大分間が空いてしまったが、[前回](https://qiita.com/c_kuwata/items/2dcf59f7e8e16a79b8d9)の続き。今回は

* Linuxでの動作確認
* Pythonコードからのデバイス状態の変更

この2つ。ただしLinuxではFocusriteのソフトウェアが当然対応していないので、Windowsでは勝手にやってくれた初期化シーケンスを自前で行う必要がある。まずはその解析から。

# Scarlett初期化シーケンスの解析

まずはRatatoskrを起動した上でScarlett 18i20の電源をON/OFFしてみる。

![VirtualBox_Windows 10_07_01_2020_00_40_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531760/a6f54ab2-8a47-994c-bf0f-7f1091343544.png)

かなりの量の通信が行われているが要点となるのは通番のリセットなので、通番の位置に着目して当たりをつける。通番の位置は15..16バイトのようなので、普通に考えれば通番が0か1になっており、また9..14バイトのメッセージ種別の部分にも特徴が表れそうに思える。（※画像ではフィルターでいろいろサプレスしている。）

結果として以下の2つのコントロール転送がそれっぽいと当たりが付いた。

```
SEND 21 02 00 00 03 00 10 00 00 00 00 00 00 00 01 00
SEND 21 02 00 00 03 00 10 00 02 00 00 00 00 00 01 00
```

通番1のメッセージを2回送っているが、多分最初の方が通番リセット処理であり、2回目は普通に通番1でメッセージを出しているのだと思われる。まずはこの通りのコードを書いて、前回書いたデバイス状態の取得コードをリセット後の通番で動作させてみる。（下記のコードは抜粋。詳細は前回のコードを参照。）

```reset_seq.py
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x80, 0x00, 0x08, 0x00, 0x01, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x7C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00])
```

ちなみに通番リセット命令の通番部分の値は何でもよいようだ。

# Linuxでの動作

LinuxでUSBデバイスとの通信を見る場合、wiresharkを使う方法が結構メジャーなのかと思うが、私は/sys/kernel/debug/usbmon以下をcatして不要な出力をgrep -vで弾いている。（いずれにせよusbmonモジュールを組み込む必要がある。）

またLinuxでは初期化処理もWindowsと少々異なるようで、例えばset_configurationを下手に呼ぶとエラーになったりする。

```analogstat4linux.py
import usb.core
import usb.backend.libusb1
from ctypes import c_void_p, c_int
backend = usb.backend.libusb1.get_backend()

from usb.util import CTRL_IN, CTRL_OUT, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE, build_request_type
from usb.control import get_status

VENDOR_ID = 0x1235
PRODUCT_ID = 0x8215
device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID, backend=backend)
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x80, 0x00, 0x08, 0x00, 0x01, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x7C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00])
device.read(0x83, 8, 100)
ret = bytearray(device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0028))
print(' '.join(map(lambda x: '{0:0{1}x}'.format(x, 2), ret)))
```

```
$ sudo cat /sys/kernel/debug/usbmon/3t > dump.txt
※別のターミナルでスクリプト実行
Ctrl-C
$ grep -v 'Z[io]:' dump.txt 
ffff95a78595f500 560234266 S Co:013:00 s 21 02 0000 0003 0010 16 = 00000000 00000100 00000000 00000000
ffff95a78595f500 560234401 C Co:013:00 0 16 >
ffff95a78595f500 560234429 S Ci:013:00 s a1 03 0000 0003 0010 16 <
ffff95a78595f500 560234646 C Ci:013:00 0 16 = 00000000 00000000 00000000 00000000
ffff95a78595f500 560234667 S Co:013:00 s 21 02 0000 0003 0018 24 = 00008000 08000100 00000000 00000000 7c000000 18000000
ffff95a78595f500 560234893 C Co:013:00 0 24 >
ffff95a78595f500 560235404 S Ii:013:03 -115 8 <
ffff95a78595f500 560235649 C Ii:013:03 0 8 = 01000000 00000000
ffff95a78595f500 560235693 S Ci:013:00 s a1 03 0000 0003 0028 40 <
ffff95a78595f500 560235776 C Ci:013:00 0 40 = 00008000 18000100 00000000 00000000 00000000 00000000 00000000 00000000
```

※ダンプ結果のフォーマットについては[usbmonのドキュメント](https://www.kernel.org/doc/Documentation/usb/usbmon.txt)を参照。

どうやらこちらも期待通りの結果になっているようだ。（※一部バイトオーダーの変換が入っているのでWindows環境の結果と一致しない部分がある）

尚いろいろコードを書いて実験しているうちにOperation Timeoutになってデバイスがハングした事が何回かあったが、現状そうなった場合の復帰方法がハードウェアの電源OFF以外に見つかっていない。これは今後の課題。

# Pythonコードでデバイスの状態を変更

いよいよ本題。Scarlett 18i20の機能のうち、本体のスイッチで対応できず、またLinuxの汎用ドライバーで操作できない機能は以下の通り。

* AIR機能の切り替え[^1]
* Scarlett内蔵ミキサーの操作

後者は結構ややこしいというか面倒くさい割に無くても私が使う分にはさほど困らないので[^2]、前者とついでにLINE/INSTのIMPEDANCE[^3]切り替えや入力のPAD[^4]にも対応してみる。

## AIR切り替えなどの解析

手順は今までと概ね同じ。

* Focusrite Controlを起動
* Focusrite Control上で適当な入力チャンネルのAIRを変更
* Focusrite Controlを終了
* Ratatoskrで該当箇所っぽい部分を気合で探す[^5]

結果、以下の通信がAIR切り替えのようだ。

```
シーケンス（1）
SEND: 21 02 00 00 00 03 00 19 01 00 80 00 09 00 01 00 00 00 00 00 00 00 00 00 8c 00 00 00 01 00 00 00 01
SEND: 03 00 00 00 03 00 10 
RECV: 01 00 80 00 00 00 01 00 00 00 00 00 00 00 00 00

シーケンス（2）
SEND: 21 02 00 00 00 03 00 14 02 00 80 00 04 00 02 00 00 00 00 00 00 00 00 00 08 00 00 00
SEND: 03 00 00 00 03 00 10 
RECV: 01 00 80 00 00 00 02 00 00 00 00 00 00 00 00 00

```

他にIMPEDANCEなども切り替えた結果、アナログ入力の状態変更のシーケンス(1)での出力コントロール転送は以下のフォーマットになっているようだ。

* 1..8バイト: 転送ヘッダ。
* 9..14バイト：メッセージ種別。
* 15..16バイト：通番。（ここまでは大抵の出力コントロール転送共通のようだ）
* 17..24バイト: 0埋めの固定値。
* 25バイト: アナログ入力とそのモードの番号
* 26..32バイト: 固定値
* 33バイト: 0 or 1。モード切り替え

25バイト目の詳細は以下。

* 0x7C～0x7D: アナログ入力1～2のIMPEDANCE
* 0x83～0x8B: アナログ入力1～8のPAD
* 0x8C～0x93: アナログ入力1～8のAIR

またシーケンス(2)の出力コントロール転送は何の意味があるのかと思っていたが、どうもこれは本体のインジケーターの表示切替命令のようだ。**このメッセージを送信しないと、本体のINST/PAD/AIRのインジケーターが切り替わらない。**

## ここまでのまとめ的なコード

以下は実行する度にアナログ入力1のAIRの状態を切り替えるコード。

```switch_air.py
import usb.core
import usb.backend.libusb1
from ctypes import c_void_p, c_int
backend = usb.backend.libusb1.get_backend()

from usb.util import CTRL_IN, CTRL_OUT, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE, build_request_type
from usb.control import get_status

VENDOR_ID = 0x1235
PRODUCT_ID = 0x8215
device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID, backend=backend)
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)

#状態の問い合わせ
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x80, 0x00, 0x08, 0x00, 0x01, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x7C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00])
device.read(0x83, 8, 100)
ret = bytearray(device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0028))
stat = ret[32] #アナログ入力1のAIR

device.ctrl_transfer(0x21, 0x02, 0x00, 0x03, [0x01, 0x00, 0x80, 0x00, 0x09, 0x00, 0x02, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x8C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
    stat ^1]) # XORで状態をスイッチ
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)

# インジケーターの切り替え
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03, [0x02, 0x00, 0x80, 0x00, 0x04, 0x00, 0x03, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00])
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)


```

----

実際に私が開発しているコードは下記にて公開中。

* https://github.com/ckuwata/open-scarlett

私が現状多忙につきまったくもって手が足りないので、

* Pythonが書ける
* Linuxで開発作業ができる
* Scarlettシリーズのgen3を持っている/購入予定がある

上記に該当する方は、是非とも協力してください。[^6]

[^1]: マイク録音時の音質を、同社のベストセラー商品のマイクプリアンプをエミュレートしたものに変更する機能。これが使えないと56,000円で買った甲斐がない。
[^2]: レコーディング作業時には入力信号をPCで録音しつつ、レイテンシーなしにモニタリングするために出力に直に繋ぐなどを行う事があるので、内蔵ミキサーを弄れるに越したことはない。
[^3]: 機材によって流れる信号のインピーダンスが異なるのでそれの切り替え。
[^4]: 過大入力に対してゲインを下げる。
[^5]: 起動シーケンスが終わった後にF4でログを一旦クリアする、前回述べたようにフィルターで絞るなど状況に応じて工夫が必要。余計な操作を行わないというのも重要か。
[^6]: 日本に何人いるか知らんけど。海外では似たようなことをやってる人を2人発見した。