QMK Firmware で Raise/Lower と変換/無変換を同じキーに割り当てる

自作キーボード向けのオープンソースファームウェアの QMK Firmware は、LT(layer, kc) という特殊なキーコードを用意している。これを使うと、通常のキーコード(A とか)とレイヤー切り替えキーを同じキーに同時に割り当てることができるので、例えば、レイヤー切替の LOWER キーと「無変換」を一つのキーに収容するといったことができる。

ところが、この LT キーを押してから離す操作を非常に素早く行うと、レイヤーの切り替えがうまく行われずに誤入力が発生する問題がある。これはキーマップ (keymaps) の設定ではどうにもならないが、代わりに keymap.c の process_record_user 関数に手を加えることで解決できる。

この方法はあぷろさんに教えて頂いたのだが、知見を共有する意味で記事として残しておくことにしたい。具体的な実装例は本文の最後に掲載する。

なぜ親指のキー配置が重要か

僕は WindowsLinux を使う際に、日本語入力の IME オフを「無変換」キーに、IME オンを「変換」キーに割り当てるようにしている。この二つのキーの配置は Mac の JIS キーボードにおける「かな」キーや「英数」キーと同じであり、ホームポジションに置いた親指で操作できるのでアクセスしやすい。また、「半角/全角」キーと違って、現在の入力モードが英語なのか日本語なのかを気にせずに所望のモードへ確実に切り替えることができる。

Windows では歴史的経緯により別の機能が割り当てられてきたが、このようなメリットが考慮されたのか、Windows 10 の次期バージョンではこのキーアサインを既定にすることも検討されている。

blogs.windows.com

僕にとっては、この「確実に切り替えられる」という点が重要で、プログラムを書いたりコマンドを打ち始める前に、とりあえず無変換キーを連打するクセがついてしまっているほどだ。そのようなわけで、自作キーボードにおいても「無変換」と「変換」をなるべく親指でアクセスしやすい位置に置いておきたいという要求がある。

一方で、Planck や Let's Split に代表される 40% キーボードでは RaiseLower の二つのレイヤー切り替えキーが重要な役割を果たす。この種のキーボードではホームポジションから遠いキーをバッサリと削った結果、数字や記号は Raise/Lower との組み合わせで入力するようになっているので、これらもなるべく親指付近に置いておきたい。

その他にも親指で操作したいキーにはスペースやエンターなどもあるわけで、親指でアクセスしやすい一等地である↓の辺りはいよいよ混み合ってくる。

f:id:okapies:20190202133823j:plain
ErgoDash の親指クラスタ

ところが、親指という指は一般に考えられているほど器用ではないし、可動域もさほどではない。個人的には、親指で使いこなせるキーはせいぜい三つで、理想的には二つに抑えたいという感じがする。だから、使いたいキーが三つあるなら、うち二つのキーを一つの物理キーに収容して使い分けたくなる。

そこで、「変換/無変換」と「Raise/Lower」はそれぞれ単発押し (tap) と長押し (hold) なので、キーの押され方をファームウェアで識別すれば二つのキーを一つの物理キーで扱えるんじゃないか、というアイディアにたどり着く。

LT キーの問題点

QMK Firmware には、LT(layer, kc) という特殊なキーコードが用意されている。これを設定した物理キーを単発押しすると kc で指定したキーコードを送るが、長押しすると代わりに layer に切り替わる。これを使うと単発押しと長押しを一つの物理キーに収められるので、以下のように設定してやれば問題は解決…

#define KC_LOMH LT(_LOWER, KC_MHEN)
#define KC_RAHE LT(_RAISE, KC_HENK)

…しない。

これ、動くには動くのだが、おそらく多くの人間が期待していない動作をする。簡単に説明すると、QMK は長押しを「キーが 200 ミリ秒押され続けているか否か」で判定しているのだが、例えば「RAISE 押す → E 押す → RAISE 離す」を非常に素早く入力すると長押しの判定が成立せず、# を入力したいのに E になってしまう上に IME オンも働いてしまい、とても残念なことになる。

これを防ぐには RAISE を押して一呼吸おいてから文字の入力を始める、というような人間側のワークアラウンドが必要で、とてもストレスフルだ。

長押し判定の秒数は TAPPING_TERM という定数で決まっているので、これを小さく設定すれば問題は軽減するが根本的な解決ではない。その上、副作用として、通常の単発押しの受付時間も短くなってしまうので、変換キーを押して 100 ms 以内に離さないと入力が失敗するといったことが起きる。これはこれでつらい。

この件について、一年ほど前に Reddit で相談した際に QMK コントリビュータの人からも話を聞けたのだが、結論としては「あまりいい解決策はない」ということになってしまった。私の理解では、これは QMK の設計の根幹に関わる問題*1なので、残念ながら当分は改善されないと思われる。

www.reddit.com

解決編

この問題が未解決のまま一年ほど我慢して使っていたのだが、今年に入って ErgoDash mini を作ったのを機に「本格的にどうにかせねばなぁ」という機運が高まってきた。そこで自作キーボード Discordファームウェアに詳しい人たちに相談したところ、あっさりと解決してしまった。最初から聞いておけば…。

上記の通り、QMK の機能では対応していないので keymaps 配列の設定を工夫しても解決しない。そこで、同じ keymap.c にある process_record_user というキーの上げ下げが通知されるイベントハンドラを自分で実装すれば、Raise/Lower の長押しに関する挙動をカスタマイズできる(よく読み返すと、上記の Reddit スレでも軽く言及されている)。

github.com

おそらく、多くのキーボードの keymap.c には以下のようなデフォルト実装が書かれていると思う。

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  return true;
}

あるいは、最初から以下のようなコードが書かれているかもしれない(update_tri_layer という関数の説明はこの辺にある。ここでは関係ないので無視してよい)。

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  switch (keycode) {
    case LOWER:
      if (record->event.pressed) {
        layer_on(_LOWER);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);
      } else {
        layer_off(_LOWER);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);
      }
      return false;
      break;
      ...
  }
  return true;
}

これを書き換えて、LT に頼らずに LOWER キーを押して離した際の挙動をカスタマイズする。例えば:

static bool lower_pressed = false; // (1)

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  switch (keycode) {
    case LOWER:
      if (record->event.pressed) {
        lower_pressed = true; // (2)

        layer_on(_LOWER);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);
      } else {
        layer_off(_LOWER);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);

        if (lower_pressed) { // (4)
          register_code(KC_MHEN); // macOS の場合は KC_LANG2
          unregister_code(KC_MHEN);
        }
        lower_pressed = false;
      }
      return false; // (5)
      break;
    ...
    default: // (3)
      if (record->event.pressed) {
        // reset the flag
        lower_pressed = false;
      }
      break;
  }
  return true;
}

ポイントとしては以下のような感じ。

  • (1): LOWER キーの上げ下げを記録する lower_pressed 変数(値を保管する箱)を用意する
  • (2): LOWER キーが押された(record->event.pressedtrue)ら、それを lower_pressed に記録する
  • (3): 他のキーが押されたら lower_pressed をリセットする(return はしない)
  • (4): LOWER キーを離した時に lower_pressed がリセットされていない時だけ「無変換」キーを入力する
    • LOWER -> E」と組み合わせて押した場合は lower_pressed がリセットされた状態なので無変換キーは入力されない
  • (5): return false; と書く(LOWER キーについて、以降の QMK 本体側での処理を行わないことを示す)

もう少し複雑な制御(LOWER を長押ししたら、他のキーと組み合わせない場合も「無変換」を入力しない)を実装したバージョンを gist に置いておくので、もしよければご参考までに。

まとめ

このように、QMK は細かいカスタマイズをしようとすると途端に難しくなる場合が多い(これを非プログラマの人にやってもらうのはなかなか厳しい)。この辺りが、「新しい自作キーボードファームウェアを作りたい」という話が出てくる一つの動機になっている。自作キーボード Discord の #arm-keyboard-firmware チャンネルでは、この辺りの話題について議論が交わされているので、興味のある人は覗いてみてほしい。

補足

他の方法について指摘を頂いたので紹介させて頂きたい。特に MACRO_TAP_HOLD_LAYER は有用そうなのでいずれ試してみたい。

*1:TMK/QMK はキーマトリクスの走査結果をバッファせず、直接イベントハンドラをコールバックするアーキテクチャになっており、前後のキー入力イベントに依存して出力を変えるのが難しい。

Chainer に型ヒントをつける

この記事は Chainer/CuPy Advent Calendar 2018 の六日目です。前回は @marshi さんの「Chainerで確率分布が扱えるようになりました。chainer.distributionsの紹介」でした。

最近、Chainer に型ヒント (type hint) をつけるために色々と試行錯誤をしている。これによって、Chainer を使ったソフトウェアの型検査ができるようにしたり、PyCharm などの IDE でのオートコンプリートが機能するようにしたいと考えている。ここでは、既存の Python ライブラリに型を付けるにあたってのノウハウ的な話を共有したい。

型ヒントとは?

言うまでもなく Python は動的型付け (dynamic typing) なので、近年まで静的な型検査 (static type checking) には対応していなかった。そのため、例えば、ある関数に渡した変数が正しい型であるかは実行してみるまで分からない。また、コード中に出てきた変数の型が分からないので、IDE などでオートコンプリートがうまく働かないという問題があった。

そこで、Python 3.5 では型ヒントと呼ばれる機能が導入された。具体的には、関数アノテーションという記法を使って以下のように書ける:

>>> import typing
>>> def f(a: int, b: int) -> int:
...     return a + b
... 
>>> typing.get_type_hints(f)
{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

Python インタプリタは、この情報を __annotations__ 属性に格納する以外には何もしない。つまり、この機能は実行時に型検査を行うためのものではない。実際に型検査を行うには、mypy などのサードパーティの静的型検査器を使う。型検査器は、与えられた Python コードを検査して型の整合性を確認する。

mypy-lang.org

あるいは、関数アノテーションではなくコメントで書いてもよい(型コメント)。型検査器は # type: で始まるコメントを認識して、関連付けられた式の型を判断する。Python インタプリタ自体は型コメントを認識しないので、上記のように get_type_hints() などで実行時に型情報を取得することはできないが、関数アノテーション記法のない Python 2 系でも動作するコードになるというメリットがある。Chainer は Python 2 もサポートしているので、今回はこちらの記法を使うことになる。

>>> def f(a, b):
...     # type: (int, int) -> int
...     return a + b
>>> typing.get_type_hints(f)
{}

ユーザは、組み込み型以外にも自前で型を定義できる。単純なエイリアスだけでなく、ジェネリクスのような高度な型にも対応している。型ヒントに使う型の実体は単なる type クラスのインスタンスなので、新しい型を定義する際はコード中に直接書けばよい。

from typing import Generic, TypeVar

UserName = str  # alias

T = TypeVar('T')
class Node(Generic[T]):  # generic type
    ...

面白いのは、これ自体は普通の Python コードである点。例えば、角カッコ ([]) は配列アクセスにも使われる合法的な文法なので Python 3.5 以前でも問題なく実行できる。定義した型は、通常の実行時にはインスタンス化されるだけで使われない。

その他の詳細については、型ヒントの仕様である PEP 484日本語訳)を参照してほしい。仕様とは言っても分かりやすく書かれているので、静的型付き言語のユーザならば馴染みのある話が多いし簡単に読めると思う。

mypy を既存のプロジェクトに組み込む

mypy は単体でもコマンドとして実行できるが、テストや静的コード解析の一種だということを考えれば、pytest や flake8 のような他のツールと同様に CI を回したい。既存のコードベースへの組み込む際の設定方法については、このドキュメントが参考になった。

注意点としては、mypy は Python 3.4 以降でないとインストールすらできないという点。Chainer のように Python 2.7 を含む各バージョンのテストを回しているライブラリでは、下記のようにチェックしてやる必要がある。

if python -c "import sys; assert sys.version_info >= (3, 4)"; then
    pip install mypy
    mypy chainer
fi

直和型 (Union Type)

既存の動的型付き言語のライブラリに型ヒントをつける際の課題として、ある関数の引数に渡される値が様々な型を取りうる、という点が挙げられる。例えば、以下の関数 double は、数字を渡しても文字列 (str) を渡しても内部的に int に変換して二倍にする。

>>> double(2)
4
>>> double("2")
4

@overload アノテーションで型ごとにシグネチャを宣言してもよいが、これだと複雑な組み合わせへの対応が大変になる。このようなケースを扱うため、typing モジュールは Union という特別な型を提供している。型検査器は、Union に含まれていればどの型が渡されても正しい式として扱う。

>>> from typing import Union
>>> def double(a: Union[int, str]) -> int:
...     if isinstance(a, int):
...         return a * 2
...     else:
...         return int(a) * 2
... 
>>> typing.get_type_hints(double)
{'a': typing.Union[int, str], 'return': <class 'int'>}

実際の例としては、Chainer では ndarray を受け取る関数が多数存在するが、実際には numpy と cupy の両方の場合がありえる。また、今回 ChainerX の導入によって chainerx.ndarray も仲間に加わった。Union を使うことで、このような型もエンコードできる。

NdArray = Union[numpy.ndarray, cuda.ndarray, chainerx.ndarray]

また、同様に Union[..., None] の簡易表現として Optional[...] が使える。これは、以下のように None が渡されることを許容する関数のシグネチャを書く時に使う。Optional といっても(他の言語のような)モナドではないのでそういう使い方はできないが、Optional でない変数に None を渡すとエラーを出してくれるので便利。

def run(config: Optional[Config]=None):
    if config is None:
        ...

ダックタイピングにプロトコルで立ち向かう

Chainer で配列の初期化処理を実装している Initializerfunction クラスではないが、__call__ メソッドを実装しているので、あたかも関数であるかのように呼び出すことができる。また、Chainer では initializer を引数に取る関数に、ndarray を引数に取る普通の関数を渡しても動作する。なぜなら、内部的にはダックタイピングで単に initializer(array) (= initializer.__call__(array)) を呼んでいるだけだからだ。

この挙動は通常は便利なのだが、今回のように型をつけようとすると問題になる。なぜなら、Initializer は関数ではないので generate_array(initializer: Callable[[NdArray], None], ...) とすると型が合わないからだ。

この問題は Protocol で解決できる。これは type hint とは別の仕様 (PEP 544) で導入が検討されているもので、まだ draft の段階で正式な仕様ではないが mypy などでは既に使うことができる。

プロトコルは、簡単に言うと「同じ変数やメソッドを持つクラス同士は同じ型として扱う」というものだ。いわゆる構造的部分型で、「静的なダックタイピング」という説明をされることが多い。例えば、Initializerプロトコルを定義するとこのようになる:

class AbstractInitializer(Protocol):
    dtype = None  # type: Optional[DTypeSpec]

    def __call__(self, array: NdArray) -> None:
        ...

def generate_array(initializer: AbstractInitializer, ...):
    ...

このように定義すれば、generate_arrayInitializerfunction の両方を受け取ることができる。また、これは実行時には単なるアノテーションなので、これらをラップする共通クラスを作る場合と違って性能上のオーバヘッドもない。

循環参照の解決

若干ハマった点として循環参照の問題がある。例えば、このような chainer.backend.Device 型を含む型を chainer.types モジュールに定義する。

from typing import Tuple, Union  # NOQA
from chainer import backend  # NOQA

DeviceSpec = Union[backend.Device, str, Tuple[str, int], ...]

そして、定義した型を使って backend モジュールの関数に型ヒントを書く。

from chainer import types  # NOQA
from chainer._backend import Device  # NOQA

def get_device(device_spec):
    # type: (types.DeviceSpec) -> Device
    ...

ところが、こうすると import 時に循環参照を起こしてエラーになってしまう。

>>> from chainer import backend
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/okapies/chainer/chainer/backend.py", line 1, in <module>
    from chainer import types  # NOQA
  File "/home/okapies/chainer/chainer/types.py", line 4, in <module>
    DeviceSpec = Union[backend.Device, str, Tuple[str, int]]
AttributeError: module 'chainer.backend' has no attribute 'Device'

これを防ぐため、types モジュールに以下のようなおまじないを追加して、型検査器が Python コードを読み込んだ時のみ chainer.backend が import されるようにする。

try:
    from typing import TYPE_CHECKING  # NOQA
except ImportError:
    # typing.TYPE_CHECKING doesn't exist before Python 3.5.2
    TYPE_CHECKING = False

# import chainer modules only for type checkers to avoid circular import
if TYPE_CHECKING:
    from chainer import backend  # NOQA

DeviceSpec = Union['backend.Device', str, Tuple[str, int], ...]

TYPE_CHECKINGPython 3.5.2 から追加された変数で、Python インタプリタでは常に False として扱われる。一方、mypy などの型検査器はこれを True として扱うので、型チェックをする場合のみ import 文が動くようにできる。さらに、DeviceSpec に定義する型を文字列として記述することで、実行時に import されていないシンボルを読み込むことによる文法エラーを回避する(型検査器は文字列を type クラスに変換してくれる)。

まとめ

というわけでボチボチと手を動かしていが、歴史のあるプロダクトなだけに API 本数も多く、まだまだ先は長そう。気長にやっていこうと思うので、応援して頂けると幸い。

github.com

tzdata の 1887 年以前の日本標準時子午線が間違っている話

今朝、こういう話を見かけた。

tzdata (tz database) というのは、世界各地域の標準時(タイムゾーン)の情報を収録したデータファイルで、様々な OS やライブラリで利用されている。例えば、Unix 系 OS で環境変数 TZ に適当な地域を設定すると date コマンドが適切な時刻を表示してくれるのは、この tzdata を使っているおかげ。

$ TZ=UTC date
2018年 10月 12日 金曜日 03:00:00 JST
$ TZ=Asia/Tokyo date
2018年 10月 12日 金曜日 12:00:00 JST

この問題は以前から指摘されていたようで、例えばきしださんの 2015 年の記事など。この記事でも書かれているように、tzdata は Java API でも使われているので当然影響を受ける。

d.hatena.ne.jp

そこで思い出したんだけど、この天守台、正しくは旧江戸城天守台に、僕はつい最近行ってきたばかりだった。

奇しくもオリンピックに伴うサマータイム導入(つまり、標準時をいじるという話である)に反対するシンポジウムを聴講した9月2日の午後、「俺、大手町サラリーマンのくせに目の前の皇居に行ったことがないなー」ということに気づき、会場の永田町から国会議事堂を経由してテクテクと皇居東御苑へと通じる大手門へと向かったのだった。

わりと外国人観光客に人気のスポットらしく、また都会のオアシスとしてもなかなか良いロケーションだった。あとポケストップが大量にある(重要。これで、入り口の荷物検査とか諸々の面倒がなければ気軽に散歩に来れるんだけどなぁ(無理。

ところで、この天守「台」というのはちょっと奇妙なネーミングだと思わないだろうか。天守「閣」ではないのだ。普通に考えると「将軍家の居城だった旧江戸城にはさぞ立派な天守閣があったのでは?」となるだろうが、実は江戸城天守閣が存在したのは江戸時代の初期まで。何があったかというと、

まぁ、ツイートでは茶化して書いているけど、いちおう以下のようなまともな理由があるようだ。この保科雅之は江戸の町の防災性の向上に尽力した人物で、火除け用地として上野広小路を作ったのも彼なんだそうだ。

この寛永天守は、明暦3年(1657)の火災で焼け落ち、翌年に加賀藩前田家の普請により高さ18mの花崗岩でできた天守台が築かれます。これが現在残る天守台ですが、四代将軍家綱の叔父である保科正之の戦国の世の象徴である天守閣は時代遅れであり、城下の復興を優先すべきであるとの提言により、以後天守閣は再建されることはありませんでした。現在、東西約41m、南北約45m、高さ11mの石積みが残っています。

天守台 - 千代田区観光協会

その後、明治に入ってから、内務省地理局測量課がこの天守台を測量の原点と定めて三角点などが置かれたらしい。

現在はホテルオークラとなっている旧溜池葵町に置かれていた内務省地理局は、1882(明治15)年に江戸城本丸に移転した。天文台天守台(天守閣の土台)に設けられ、この新しい天文台を経緯度の零点とした。江戸城天守台は築後200年で、堅牢・安定な地盤が観測の適地と見込まれたのである。

本初子午線をゆく Part II 東京編

しかし、「東京天守台を初度とする」というお触れが出た直後、国際子午線会議の結果を受けて「東経135度を子午線とする」という勅令が出て、明石を通る現在の子午線 (GMT+9) に切り替わってしまった。だから、公式に +09:19:01 だった期間はかなり短い。この辺の細かい経緯については国立天文台の Wiki に詳しい。

というわけで、イギリス旅行の際にわざわざグリニッジ子午線を踏みに行く程度には子午線マニア(?)を自認していたわりには、足下のこういう歴史を見過ごしていたとは不覚だったなぁ、というお話でした。

なお、本題の tzdata の方ですが、修正には何かしらエビデンスがあった方がいいようなので、何かご存知の方がいらっしゃったら以下のスレッドにコメントして頂けると良いかと思います。

自作キーボードとクセをすり合わせる話

この記事は自作キーボード Advent Calendar 2017の4日目です。前日は、@mt_mukko さんの「Nyquistを組み立てた話。Pro Microもげもあるよ!」でした(この記事は ThinkPad T460s で書いてます)。

Let's Split を組み立てた

@matsPod さんの Group Buy に参加して Let's Split のパーツを購入したものの、転職やら何やらでバタバタしていてようやく組み上げたのが 10 月…。

現在は職場で活躍中。机の上を広く使えるのがいい感じ。

キー配列の調整

さて、上のツイートに謎のポストイットが写っているが、これは使っている最中に気付いた点をメモっているものだ。これを元にしてキー配列をどう変更するか考えたりしている。言うまでもなく、自作キーボードの魅力の一つは自分の好みに合わせてキー配列を自由に設定できること。

現在は、以下のような配列にしている。記号の位置についてはまだまだ調整を続けているし、Lower/Raise と無変換/変換のデュアルキー化がうまく設定できてないなど課題が多いものの、ひとまず日常業務で支障のない程度には仕上がった。

f:id:okapies:20171205005721j:plain:w500 f:id:okapies:20171205011133p:plain:w500

最初のツイートと見比べてもらうと分かると思うが、初期配列からそこまで大きな変更はしていない。QWERTY 配列そのものとか、Ctrl や Alt などの修飾キーの位置は、昔から愛用している HHKB に合わせるようにしている。

最も頻繁に変更しているのは、Lower/Raise と組み合わせる記号キーだ。Let's Split が 40% キーボードである以上、これらのキーをどこかに押し込む必要があるが、どこでもいいというわけではない。自分がプログラミングや日本語入力をする上で頻出するキータイプの順番というものがあるので、その流れに沿ってタイプしやすい配置を探す必要がある。

面白いのは、単純に頻出するキー同士を近くに置けば良いというものではないらしい、ということだ。経験的には、右手と左手を交互に動かしたり、手首の回転を使うような動きは安定感が高い。

こうして、キーを通常とは別の場所に設定していると「あのキーはどこに置いたっけ」となることが、まぁ…稀によくある(笑。この点については、上の写真を見てもらうと分かるように、キーキャップをアルファベットではなく Lower/Raise レイヤーの記号に対応したものにして解決している。一見すると奇妙に感じると思うが、どの道、アルファベットはブラインドタッチしているのであまり問題がないという寸法。

他にも、Lower/Raise は他のキーとは別形状のキーキャップを持ってきて角度を付けることで、親指で探りやすいようにしている。

キーボードと自分のクセをすり合わせる

上の写真を見て、わりと保守的なキー配列だなと思った人も多いと思う。これは Let's Split を使っているからといって他のキーボードを使う機会が無くなるわけではない、という単純な理由による。僕がキーボードを自作しているのは、身体への負担を減らして効率的に作業をするためなので、一般的でないキー配列を採用して効率を下げては元も子もないし。

一方で、多少のキー配置の変更は慣れで何とかなる部分も大きい。僕の場合、記号の位置は一週間か二週間もすれば体が覚えてしまうし、そこで他のキーボードに一時的にスイッチしても問題なく扱える。

自作キーボードに対するよくある懸念として、標準的でないキー配列ではまともにタイプできないのではないか、というものがある。僕は、自分の経験を通じて人間の適応性は自分自身で思っている以上に高いのでそこまで心配する必要はない、と考えるようになった。キー配置の変更、というのは市販キーボードでも論争のタネになりがちなテーマだが、この点を過大に見積もって新しい可能性に手を出さないのは人生の損失…というのは言い過ぎだろうか。

このように、自作キーボードには「自分のクセをキーボードに合わせる」方向と「キーボードを自分のクセ(習慣)に合わせる」方向の、二つの方向性があるように思う。

これは、どちらかが正しいという話ではない。二つの相反する要求のバランスを取っていく中で、自分が無意識のうちに従っていた習慣を一つ一つ発見し、何を守りどこで殻を破っていくか考えていくのが、自作キーボードという趣味の一つの醍醐味だろう。

これは、万人にとっての〈理想のキー配列〉は存在しない、ということでもある。キーボードと自分のクセをすり合わせていく際に、使ってみて課題を発見して修正する、というプロセスを回していくと、個々のキー配列はその時点での自分に合ったものでしかない、ということになるからだ。

このことは、自作キーボードの普及を考える上で悩ましいポイントではある。キー配列を編集する GUI エディタのようなものはすでにあるが、日本語で使うキーに十分に対応していないものも多い。その辺はコミュニティで解決していくべき課題なのかもしれない。

Akka HTTP クライアントを使う

この記事は Scala Advent Calendar 2017 の三日目です。前回は @poad1010 さんの「JupyterでScala」でした。

Akka HTTP を使って REST API を叩いてみようと思って色々と試したメモ。基本的には下記のドキュメントを読めば良いのだけど、サーバ側はともかくクライアント側の使い方について日本語で書かれたものが少ないので、使う側の目線での話を書き残しておくのも良いでしょうという感じ。ここでは、最も簡単な API である Request-Level Client-Side API について話をする。

doc.akka.io

少し難しい話になるが、簡単に使いたい場合は、記事の最後にラッパーコードを貼っておくのでコピペして使ってもらうと良いかと思う。

HTTP ボディを取得する

ドキュメントを見ると、初っ端からこういう感じで脅されるので嫌な予感がすると思うが、その予感は的中する。

「ストリームが一級市民」ではない HTTP クライアントに慣れている人は、最初に Akka HTTP クライアントの裏側にあるフルスタックなストリーミングの概念について説明した「リクエストとレスポンスのエンティティがストリームの性質を持つとはどういう事か」の章を読むことをお勧めします。

とは言っても、レスポンスを受け取るところまでは特に難しいところはなく、単に Http().singleRequest()HttpRequest を渡すだけだ(以下では、スコープに implicit な ActorSystem, Materializer, ExecutionContext が入っていることを前提に話を進める):

implicit val system: ActorSystem = ActorSystem()
implicit val materializer: ActorMaterializer = ActorMaterializer()
implicit val executionContext: ExecutionContext = ...

val request = HttpRequest(uri = Uri("http://example.com/api/v1/users"))
val response: Future[HttpResponse] = Http().singleRequest(request)

ややこしいのはレスポンスボディをメモリに読み込むところで、HttpResponse を見てもそれらしいメソッドが見当たらない。結論から言うと、以下のようにする必要がある。

val body: Future[String] =
  response.flatMap(_.entity.dataBytes.runFold(ByteString.empty)(_ ++ _).map(_.utf8String))

なぜこういう書き方をする必要があるかというと、HttpEntity から取得できる dataBytes は Akka Streams のストリーム (Source[ByteString, Any]) だから。

Akka HTTP はストリームベースのライブラリなので、サーバから到着したレスポンスを全てメモリに貯めこんでからユーザに引き渡すのではなく、バイト列が到着するたびにイベントを発火して処理する。これにより、例えば巨大なデータファイルや終端がない (unbounded) サーバログのようなようなレスポンスを効率的に扱うことができ、細かい制御(チャンクの区切り方とか)も容易になる。

一方で、従来のように文字列として全体を一回でアクセスできるようにするには、

  1. 受信した複数のバイト列を全て足しあわせて一つのバイト列に畳み込む (.runFold(ByteString.empty)(_ ++ _))
  2. バイト列を文字列に変換する (.map(_.utf8String))

という処理を明示的に記述する必要がある(ちなみに、1 と 2 を逆にすると多分文字化けが起きる。理由は考えてみよう)。分かってしまえば大した話ではないが、カジュアルな API を期待していると面食らうのは確かだ。

もう一つ注意点があり、Akka HTTP は送信側のデータ送信量を TCP レベルでスロットリング (TCP back-pressure) しているので、あるレスポンスのエンティティを消費せずに途中で放り出してしまうと、その TCP 接続の送信が詰まってしまい、その接続を利用している他のリクエストの処理に影響が出る。したがって、エラーの場合でも必ず response.discardEntityBytes() を呼び出す必要がある(将来的には自動検出できるようにしたいらしい)。

なお、全てのレスポンスをメモリに読み込む方法はもう一つあり、以下のように toStrict(timeout) を呼ぶと Future[HttpEntity.Strict} を取得できる。Strict を使うと受信したバイト列を集約した data にアクセスできるので、以下のように書ける:

import scala.concurrent.duration._
val timeout: FiniteDuration = 10.seconds
val body: Future[String] = response.flatMap(_.entity.toStrict(timeout).map(_.data.utf8String))

toStrictタイムアウトを指定する必要があるが、レスポンスを待つ時間を明示的に指定するならこちらを使うべきだろう(runFold を使う場合は idle timeout が成立するまで待つ)。

また、タイムアウトFiniteDuration という型名が示すとおり「無限」は指定できない。「タイムアウトが無限」はナンセンスである、というライブラリ作者たちの意思表示なので、いつまでも待ち続けたいお気持ちを表明する場合は適当に 100 万秒とかを指定すると良い。

JSON を変換 (unmarshal) する

メモリに読み込んだ JSON 文字列をアプリケーション内部の形式に変換するには、Akka HTTP が提供する Unmarshal(...).to[A] を使う。ただ、Unmarshal 自体は単なるラッパーで、具体的な処理は他の JSON パーサーに委譲する仕組みになっている。なので、好きなライブラリを直接使っても構わない。

標準では spary-json のラッパーが提供されている。使うには、ライブラリの依存関係に akka-http-spray-json を追加する。

libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.11"

例として、JSONUser 型にマッピング (jsonFromat2()) して Unmarshal してみる。akka-http-spray-json を使うには SprayJsonSupport の配下の implicit をスコープに入れる。こんな感じだろうか:

import akka.http.scaladsl.unmarshalling._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import spray.json._

case class User(id: Long, name: String)

trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
  implicit val UserFormat = jsonFormat2(User)
}

class FooClient() extends JsonSupport {

  val res =
    Http().singleRequest(HttpRequest(uri = Uri("http://example.com/api/v1/users/1")))
  res.entity.dataBytes
    .runFold(ByteString.empty)(_ ++ _)
    .flatMap(bs => Unmarshal(bs.utf8String).to[User])

}

spray-json ではなく、他の Jackson や circe などのライブラリを使いたい場合は、それぞれに対して Unmarshaller を実装した akka-http-json というサードパーティのライブラリの開発が進んでいる。

github.com

ラッパーを作る

以上のノウハウをまとめて、こういう感じのラッパーを作っておくと便利なのではないかと思う。言うまでもなく、このコードで 10 GB のデータファイルとかを受け取るとヒープメモリが爆発四散するので、その場合はストリームの作法に従って書こう。

def doRequest[A](req: HttpRequest, unmarshal: String => Future[A]): Future[A] =
  Http().singleRequest(req).transformWith {
    case Success(res) if res.status == StatusCodes.OK =>
      res.entity.dataBytes
        .runFold(ByteString.empty)(_ ++ _)
        .flatMap(bs => unmarshal(bs.utf8String))
    case Success(res) =>
      res.discardEntityBytes()
      Future.failed(new Exception(s"HttpRequest failed: $res"))
    case Failure(t) => Future.failed(t)
  }

追記: UnmarshalHttpEntity を直接与えることで、より短く書くことができる:

def doRequest[A](request: HttpRequest)
                (implicit unmarshaller: FromEntityUnmarshaller[A]): Future[A] =
  Http().singleRequest(request).transformWith {
    case Success(res) if res.status == StatusCodes.OK =>
      Unmarshal(res.entity).to[A]
    case Success(res) =>
      res.discardEntityBytes()
      Future.failed(new Exception(s"HttpRequest failed: $res"))
    case Failure(t) => Future.failed(t)
  }

これは、FromEntityUnmarshaller[A] の実装がバイト列の集約を実装している Unmarshaller.byteStringUnmarshaller を呼び出すため(実装を見るrunFold(...) による畳み込みをしているのが分かる)。

まとめ

Akka HTTP はドキュメントが充実しており、ライブラリの思想や使う上で必要なことは概ね書かれている。また、機能的、非機能的な制約が型として埋め込まれており、自分がどんなコードを書こうとしているのか、常にプログラマに意識させるような作りになっている。

ただ、それはごまかしが効きにくいということでもある。「サーバからデータをガサッと持ってきてババッて変換してさー」的なやり方は技術的にはいくつも穴があるが、ちょっとした作業では不問に付したい場合も多い。なので、Akka HTTP は日々の作業をアドホックにこなしていくツールとしては使いにくい点が多い。ただ、堅牢で高性能なアプリケーションを書く上では強力な道具になりうると感じた。

最近、SoftwareMill が開発する sttp が 1.0 になった。これは、akka-http や async-http-client などをラップしてシンプルな API を提供することを目的としている。こうしたライブラリを、作業に求められている品質に応じて使い分けていくのが良いのではないかと思う。

github.com

参考文献

ネットワークは「切断」しない

Disconnect という英単語を「切断」と訳すのは(特に IT エンジニアリングの文脈では)誤解の余地が大きいので良くないのではないか、という話。

Connect は「結線」しない

Connect という英単語は、一般的に「繋ぐ」とか「接続する」と訳されることが多いと思う。connect は、元々「互いに結びつける」という意味のラテン語から来ていて、初期には「物理的に一つにする」という意味で使われていたようだ。しかし今日では:

connect

  1. Bring together or into contact so that a real or notional link is established.
    1. Join together so as to provide access and communication.
    2. Link to a power or water supply.
    3. Put (someone) into contact by telephone.
    4. [no object] (of a train, bus, aircraft, etc.) be timed to arrive at its destination just before another train, bus, etc., departs so that passengers can transfer.
  2. Associate or relate (something) in some respect.
    1. Provide or have a link or relationship with.
    2. [no object] Form a relationship or feel an affinity.

connect - definition of connect in English | Oxford Dictionaries

上記のように、物理的な接続だけではなく、観念的な結びつき (link) とか関わり合い (relationship) を表すのにも使われている。

日本語で「接続する」と書くと、何となく媒体(メディア)を介して二つのモノを物理的に結線する様子を思い浮かべがちだ。しかし、どちらかというと「二者の間に関係性を作る」というニュアンスで理解するのが正しいだろう。

接続は「切断」されない

例えば、「列車の接続がいい」という表現が使われるように、connect を「接続」と訳すのは間違いではない。また、その対義語である disconnect は一般的に「切断」と訳されるが、その意味は英語でも “break the connection”、つまり connection を壊すとか断つとかいう意味になる。

だが、繋がり (connection) を断ち切るとはどういうことだろうか?

そもそも「繋がり」とは何か。ここでは、互いの存在を認識した上で、媒体を介して情報(やエネルギーや社会的な何か)を交換すること、と定義してみよう。既に見たように connection が指す「繋がり」には、物理的なものだけでなく観念的なものも含まれる。注意が必要なのは、disconnect が断つのは「繋がり」であって「媒体」ではないということだ。

あらゆる繋がりは媒体を介した交換行為を通じて実現される。そして、媒体が断たれれば繋がりも断たれることが多いので、我々は両者を同一視しがちだ。しかし、逆は真ではない。つまり、媒体が断たれなくても繋がりが断たれることはある。この世から公衆電話網が無くならなくても、友達同士は絶交できる。

disconnect を「切断」と表現すると、状態がゼロイチでキレイに切り替わる様を想像しがちだが、しかしコトはそう単純ではない。なぜなら、媒体の接続性はそれ自身で完結する問題であるのに対し、繋がりは二者の関係性、つまりお互いが相手のことをどう認識しているかの問題だからだ。

例えば、媒体に(一時的に)障害が起きると二者は一切情報を交換できなくなる。あるいは、情報の一部だけがたまたま失われて届かないような場合もあるだろう。しかし、二者の繋がりは障害が起きた途端に断たれるわけではない。お互いが諦めない限り、媒体の障害が回復した時点で再び交換が行えるようになるからだ。それでも繋がりが断たれるとしたら、例えば、以下のようなことがあった場合だろう:

  1. どちらかが、相手との交換の意思を捨てる
    • 相手に通知して辞める場合も、そうでない場合もあるだろう
    • 媒体の障害が原因の場合、一定期間待っても復活しない時点で「媒体はもう二度と復活しないに違いない」と予測して諦めたのだろう(=タイムアウト
  2. どちらかが、媒体の先にいる相手が自分との交換の意思を持っていると信じられなくなる

黒やぎさんは、白やぎさんから届いた手紙を食べてしまって、しかも返事を書くのを忘れているだけなのかもしれない。ここで白やぎさんが「黒やぎさんに手紙は届かない」とか「黒やぎさんに無視された」と判断すれば、両者の関係は disconnect される。しかし、それは「白やぎがそう判断した」というだけだ。この時、媒体に物理的な変化は何も起きていない。

特に、「相手が自分との関係をどう思っているか」を知るのは難しい。それを知るためにすら、媒体を介して相手にコミュニケーションを試みるよりないからだ。コミュニケーションを取らなければ家族や恋人が何を考えているのか分からず、相手に示す適切な愛情表現を選べないのと同じように。

ネットワークは「切断」しない

ここまでの話を踏まえると、特に TCP/IP のようなパケット通信において「ネットワークが切断」という表現は、何となく不自然なものに見えてくる:

try {
    final URL url = new URL("http://example.com");
    final InputStream in = url.openConnection().getInputStream();

    System.out.println(in.read());
} catch (IOException e) {
    System.err.println("ネットワークが切断されました!"); // (1)
}

ここでの IOException は、多くの場合、おそらくはネットワークの経路上で輻輳が発生したために、HTTP プロトコルに基づく接続 (url.openConnection()) や読み込み (in.read()) の通信が正常に完了しなかったというだけだ。ネットワークという「媒体」そのものが切断されたわけではない。

もちろん、日常の業務では様々な障害発生ケースを念頭に置いた上で、その簡便な表現として「切断」という単語が使われている。しかし、我々はふとすると、ネットワーク経路がゼロイチで開通したり不通になったりすると考えたり、そもそも媒体が完全に正常でも通信の失敗が起きうる(片側のプロセスが応答しなくなるとか)ということを忘れたりしがちだ。

何をもって「通信障害が起きている」と判断すべきかは難しい。例えば、上記で (1) のパスを通った時点で http://example.com への経路が死んでいると判断…するのは難しい。その時、たまたまパケット損失が起きただけなのかもしれないからだ。

通信障害が発生しているか否かは、実務的には統計量に基づいて判断するしかない。従って、上記のような「一回ごとの通信」について記述しているコードでそれに対処することはできないだろう。それを忘れると、一度でも IO 例外が起きたらその経路は不通と判断する、みたいなコードを書いてしまうことになる。

まとめ

connect/disconnect とは二者の間に関係を作ったり無くしたりすることであり、二者の関係性を取り持つ媒体はその本質ではない。そして「切断」という語は、二者間の繋がりの有無がゼロイチで判定できるかのような誤解を生み出しがちなので、あまりよろしくないのではないか。

まぁ、すごく細かいことにこだわっているような気もするのだが、一度自分の考えをまとめてみるのも良いかと思って書いてみた次第。

「裏のニコニコ超会議」に行ってみた

f:id:okapies:20170504162847j:plain

GW中の皆さん、休暇をいかがお過ごしだろうか? え、ずっと仕事? それはご愁傷様…。

さて、我々(オタク)にとってGW期間中のビッグイベントといえば、4/29~30 に幕張メッセで開催された「ニコニコ超会議」だろう。今年も 15 万人越を動員して大盛況だったようで、今年は僕も久々に顔を出して知り合いやマストドン本の関係者に挨拶して回ったりしていたけれど、相変わらずのカオスな活気だった。

しかし、その盛況の陰で「裏のニコニコ超会議とも言える音楽フェスが開催されていたことをあなたはご存じだろうか? ずばり、その名を Electric Daisy Carnival Japan (EDC Japan) という…。

「裏」って?

まぁ、こういうことです:

f:id:okapies:20170504142817j:plain:w400

そう、正真正銘、超会議の裏が会場なのだ。期間も全く同じなので、超会議でオタクや中高生がワイワイやってる隣で、パリピの皆さんがウェイウェイやっていたというわけ。

その結果としてこんな光景も見られた:

EDC って?

EDC は、EDM というこの十年くらい世界的に流行しているダンスミュージックをかける DJ イベント、いわゆる EDM フェスのうちでは最大級のイベントで、世界三大フェスの一つと呼ばれているらしい。規模も桁違いで、ウン十万人分のチケットが一瞬で売り切れることも珍しくない。これまで三大フェス (Ultra Music Festival, Tomorrowland, EDC) のうち、日本で開催されていたのは Ultra Japan だけだったんだけど、今回 EDC が初来日したのだ。

japan.electricdaisycarnival.com

元々、僕は BEMANI がきっかけで電子音楽を聞くようになって、自然と流行りのジャンルである EDM にも触手を伸ばすようになり、そうした楽曲の主なパフォーマンスの場である EDM フェスにも興味があった。まぁ、僕は基本的にお祭り騒ぎを見るのが好きなので。

とはいえ、正直なところ、海外のフェスに関しては治安が悪いドラッグが蔓延しているという話も耳にするので、わざわざ海外に行ってまで参加するのは敷居が高いナァと感じていた。そんな時、今回の日本での開催を聞きつけ、また以前から聴きたかった Zedd も出演するということで、これはいい機会だと当日券を握りしめて駆け付けた次第。

会場

この手の EDM フェスの特徴としては、とにかく設備や美術にカネがかかっていることが挙げられる。大音量のスピーカーや巨大なディスプレイの上に「おとぎの国」のような世界観を構築して、来場客の非日常感を盛り上げるための様々な仕掛けが凝らされている。

また、これはフォトジェニック(写真写りの良さ)を重視しているということでもあるようだ。この辺は、口コミでの拡散を狙った、いかにもインスタ世代らしい施策と言える。以下の動画が今回のトレイラーだけど、雰囲気は掴めるかと。

www.youtube.com

システムとしては、「踊ってみた」しかない超会議だと思えばよい。つまり、入場した後は3つある野外ステージのどれを見に行ってもいいという仕組み。出演者としては海外の人気 DJ を多数取り揃える一方で、一日目には中田ヤスタカきゃりーぱみゅぱみゅも出演していたらしい。

会場内を回ると各所にこういう装飾が置いてあり、夜はライティングされて雰囲気を盛り上げる。あと、写真の真ん中に映っているように仮装してきた人たちが結構いて、あちらこちらを闊歩している。

f:id:okapies:20170430165425j:plain:w480

各ステージの様子を紹介しよう。まず、第一ステージは千葉マリンスタジアムの中に構築されてて、巨大なフクロウの像が観客を睥睨する荘厳な作りになっている。一方、第二ステージは海岸線沿いの砂浜にあって、DJ ブースの後ろに巨大なディスプレイが組まれており、演奏と同時にこれを活かした演出が行われる。

f:id:okapies:20170430173323j:plain:w320 f:id:okapies:20170430175112j:plain:w320 f:id:okapies:20170430165743j:plain:w320 f:id:okapies:20170430171733j:plain:w320

普通のコンサートと違うのは、ステージ後方の VIP エリアを除いて指定座席とかはないので、出演者のプレイが始まるとみんなワーッと DJ ブースの前に集まってきてこういう騒ぎになる:

www.youtube.com

…うーん、俺、よくここから生きて帰ってこれたな。*1

そして、次第に辺りが暗くなってくると「光」による演出が本格化してくる。ここからが、出演者的にも演出的にもフェスの本番だと言っていいだろう。

f:id:okapies:20170430182737j:plain:w480

ステージは爆音で包まれ、頭上を何条ものレーザーが飛び交い、何万人という観客がすし詰め状態で踊り狂い、歌詞を合唱する。そんな中で頭をカラッポにしてひたすら恍惚感に浸るというのは、普段はなかなかできない貴重な体験だったと思う。

www.youtube.com

確かに、混じりっけ無し純度100%のリア充の中に飛び込むことになるので、我々みたいなオタクにとってかなり参加を躊躇する類のイベントなんだけど、周りに合わせていれば大体問題ないし、コミュ力も必要ないのでそういう意味では楽だった。

観客

既に紹介したように、フェスには仮装してやって来ている人たちがけっこう多い。こういう人たちを特集したユーチューバーの動画があったので紹介…するんだけど、肌の露出が多めなので一応閲覧注意。

www.youtube.com

しかし、ハロウィンもそうだけど、こういう仮装文化っていつの間にか定着したよね。一体どういう経路で浸透したんだろう。あと、動画には海外から参戦してる人がけっこう出てくるけど、これは実際にそうで、パッと周りを見渡すと必ず外人さんが視界に収まるくらいはいた。周りもちゃんと交流してて、全体的に英語スキルが高めだったという印象。真のコミュ力とは言葉の壁を超えるものなのかもしれない。

驚いたのは、観客みんなが流行りの曲の歌詞を全部覚えているらしいということ。DJ は、基本的に自分の曲だけでなく他人の曲を含めてどんどんプレイしていくので、観客は次に誰の何という曲がかかるかは予想できないんだけど、みんなイントロクイズばりに即座に曲名を割り出して大合唱が始まる。隣の人に聞いたところ「クラブの定番曲のベスト盤とかを聞いていればそのうち覚える」と言われたが…。

このように、「パリピ」について我々オタクの側からは「何も考えていない連中」みたいな偏見が広くあると思うが、これはこれでとても熱心なファン文化なんだな、ということが感じ取れたのは収穫だった。

共通点と差異

そんなわけで、今回、一日のうちにこういう両極端のイベントに参加して思ったのは、意外な感想だと思うけど「あれ、けっこう似てるな?」ということだった。

表面的な違いは山ほどあるんだけど、いろいろな場面で既視感を感じることが多かった。これは多分に感覚的なものだけど、実は世間で言われているほど根本のロジックに大きな違いはなくて、表現の手段が違うだけじゃないか、というようなことを思ったのだった(例えばコスプレ好きな事とか)。

一方で、両者の最も大きな違いはビジネスモデルだろう。超会議が、何十もの「趣味」の島を束ねたロングテールモデルであるのに対して、EDC は高々2~3の島に有名アーティストを据えて集客するモデルだ。その結果として、超会議は EDC の倍近く動員することができている。

動員数 チケット代 予想売上
超会議 15万4,601人 2,000 円 約 3 億円
EDC 8万4,000人 16,000 円 約 13.4 億円

ただ、売上を比べると別の面も見えてくる。元々、超会議は「儲ける」ことを主眼にしたイベントではないとされているが、ここ数年は動員数が伸び悩んでいるようだ。一方で、EDC は同じロケーションで(チケット代だけ考えても)4倍以上の売上を叩き出している。そろそろ転換期なのかな、という気もするのだがどうだろう。

越境

リア充文化とオタク文化の越境、という話として、Porter Robinson(ポーター・ロビンソン)というアメリカ人アーティストを紹介して締めくくりとしたい。彼は EDC に出ていたわけではないんだけど、最近も来日公演があって大変すばらしいパフォーマンスだった(同業者からも称賛の声が相次いでいるとか)。*2

彼は、一般的に EDM の文脈で紹介されるアーティストで、世界の人気 DJ ランキング “Top 100 DJs” にランクインする実力者*3。ただ、本人の音楽性は EDM の主流とは少し雰囲気の違うもので、EDM というジャンル自体が「客を踊らせる」ことを突き詰め過ぎて単純化し多様性を失っている、という声が出ている中、次世代の旗手として期待する声も出ている、とかそういう立ち位置らしい。

個人的には、彼の曲の透明感がありつつ、どこか寂寥感を帯びた曲調が好きで愛聴している。そもそも、僕も EDM の主流のガチャガチャした雰囲気は苦手で、どちらかというと彼や Madeon のような曲調が好みなので、僕の EDM に関する知識はわりと偏っているんだけど…。

ところで、彼は日本文化に非常に造詣が深いことで知られている。造詣が深いというか、はっきり言うとガチヲタである。

そもそも、彼が音楽制作を始めたのは、言わずと知れた日本の音楽ゲームDance Dance Revolution” をプレイしたのがきっかけであり*4、他にも彼に日本の深夜アニメの話を振ると熱く語り始めるとか、度々来日してオタク系の DJ イベントに出没しているらしいとか、自分の DJ パフォーマンスで欧米人相手に唐突に アニソンを 流し始める とか、そういう逸話に事欠かない。ちなみに、最近は「Re:ゼロ」のエミリア推しらしい。

楽曲制作の方でも、わざわざ許可を取って好きな声優の声をサンプリング *5したり、ボーカロイドに歌わせたりと趣味に走ってチャレンジを重ねていたのだが、盟友 Madeon とのコラボ楽曲 “Shelter” で、ついに自分の MV のためにアニメ制作を手掛けるに至った。それがこれ:

www.youtube.com

見てもらうと分かるが、欧米人が作るアニメによくあるような「バタ臭さ」が一切ない。最大の理由は制作に日本のアニメスタジオ (A-1 Pictures) が関わっているからだが、しかし全体のストーリーやキャラクターイメージの多くは Porter 自身のアイデアなんだとか。アニオタ DJ の面目躍如といったところか。

iflyer.tv

www.fuze.dj

これが世界でどの程度受け入れられたかというと、現在の YouTube の再生数が 1,700 万越えを達成しており、なかなか成功した数字と言えるだろう。まぁ、先ほど挙げた Top 100 DJs 一位の Martin Garrix(彼は EDC Japan 一日目の大トリを務めた)の MV が軽々と1~10億を叩き出す世界なので、それと比べてしまうと見劣りはするのだが…。

とはいえ、日本のオタク文化が海外のオタクにウケている、という話から一歩踏み込んで、世界的なメジャーシーンの側から日本のオタク文化への越境を達成しつつあるわけで、十分に注目に値する試みと言えるだろう。インタビューによれば、Porter は今後もオーディエンスの反応を見つつ、オタク文化の良さを世界へ発信していきたいと考えているようだ。

ただ、翻って日本のオタク文化から世界のメジャーシーンへの越境ということが可能だろうかと考えると、なかなか課題は多そうだ。上で紹介した Porter の楽曲や DJ パフォーマンスを見ると、自分が好きなものを素材として積極的に使っていく一方で、(当たり前だが)内輪ネタ臭は極力取り除かれていることが分かる。それはもちろん、彼の軸足がメジャーシーンにあるからだ。

これは昔から繰り返されてきた話題なのだと思うが、オタク文化は内輪ネタとは切っても切り離せない関係がある。だから、メジャーへの露出を増やすために内輪ネタを排除する、というのは本末転倒な可能性がある。しかし、国内市場が飽和しつつある(参考: コミケの動員数)中で現状の水準を維持する方向は、新陳代謝を失って緩やかな衰退へと繋がる可能性もある。今や、2020年に向けて、様々なことが岐路にあると言うべきなのだろう。

まとめ

今回、超会議と EDC という全く性質は異なるがどこか似た所のあるイベントが並んで開催されたのは、色々なことを象徴していて面白い経験だった。面倒なこともあった(オタクだとバレて絡まれかけたりとか…)が、文化の越境というのはいつも新鮮な驚きがある。皆さんにもぜひお勧めしたい。

*1:まぁ、後ろの方はわりかしこんな感じでマターリとしている。立ち位置が自由に選べるのは良い。www.youtube.com

*2:YouTube を検索すると公演の動画が出てくるので探してみてほしい。

*3:2016年版では大幅に順位を落としているが、どうも2015年頃からうつ病を患って最近まで十分に活動できていなかったそうで、その影響もあるのだろう。

*4:ちなみに、影響を受けたのは NAOKI というより dj TAKA の方らしい。

*5:あの夏で待ってる」で田村ゆかりが演じる「山乃檸檬」のセリフ…らしいけど、僕はあまり詳しくない。