LV2の概要

音楽技術Advent Calendar 2019の11日目のエントリーです。(まあだいぶ穴が開いているのですが、マイペースに埋めていきます。)

What is LV2?

LV2は主にLinux環境で利用できる、クロスプラットフォームのオーディオプラグインの仕様です。

オーディオプラグインとは、主にDAW (digital audio workstation)環境でDTM(desktop music)の作業を行う場面で、楽器やエフェクターとして利用できるオーディオデータの生成・加工ソフトウェアとして使われるものです。オーディオプラグインは、さまざまな「ホスト」となるDAWなどのソフトウェア*1の上で、さまざまなものを繋ぎ合わせて使用することが多いです。よくある使い方としては、サンプラープラグインでサンプリング音源データをノート(キー)に合わせて波形を調整して、それをロータリーエンジンやリバーブプラグインで加工して、ミキサープラグインでゲイン調整してオーディオデータとして出力する、といった流れになります。

オーディオプラグインは、複数のホスト、複数のプラグインベンダーの間で互換性が求められるもので、デファクトスタンダートとなる仕様がいくつか存在しています。有名どころではSteinberg VSTApple AU (AudioUnit)が挙げられます。オーディオ処理がもともとプラットフォーム固有の実装になりがちだったこともあってか、ここには業界標準となるような仕様が存在しないのが現状です。また、DAW開発社でも、自分たちのDAWでしか使用できない独自のプラグイン形式を規定して公開しているものがあります。Avid Audio eXtensions (AAX)などがその典型です。

本稿で取り上げるLV2とはLADSPA v2の略であり、LADSPAとはLinux Audio Developer's Simple Plugin APIの略です。Linux環境ではVSTAUのようなデファクトスタンダートのように使われている仕様のオーディオプラグイン機構が動作しなかったため、独自にオーディオプラグイン機構を開発して発展させる必要がありました。それでもっともポピュラーだったと言える仕様がLADSPA (v1)です。

オーディオI/Oそのものはプラットフォーム固有の実装とならざるを得ない部分があるのですが、一旦そこを抽象化すると、実際のオーディオ処理の部分は、実のところプラットフォーム固有の実装になることはあまり無かったため、LADSPA v2は最早L (Linux)固有のものではなく、MacでもWindowsでも動作可能なものになっています。LV2と同様、VSTクロスプラットフォームMacでもLinuxでも使用できる存在となっていきました。

LV2は2019年現在、ardourやqtractor、museなど主要なLinuxDAWでサポートされていますが、それ以外の製品、特にWindowsMacでは未採用の製品が多いです。

LV2プラグインの概要

LV2は(VSTなどと同様)現在進行形で拡張されている仕様です。後方互換性を維持するために、LV2仕様はモジュラーアーキテクチャになっており、コア部分と拡張部分を切り分けてあります。そのため、使用可能な最低バージョンがモジュールによって異なります。これはVST3のMA(モジュールアーキテクチャ)と似ている側面があります。

LV2仕様のモジュールリスト どのプラグインフォーマットでも、プラグインにはメタ情報が含まれているものですが、LV2の場合はこれをRDFによって提供することになっています。実際には、XMLとしてのRDFではなく、RDFを「シンプルな」テキスト形式で記述するTurtle (Terse RDF Triple Language)と呼ばれる独自記法に変換した.ttlというファイルに記述することになります。

RDFの記述内容は、プラグインAPIについても当てはまり、VST3やAUの場合は、単にCやObjective-CAPIを実装するだけで済むのですが、LV2の場合は、実際にコードとして実行するために必要になる最小限の部分のみをコードとして実装し、残りの部分はこのメタデータとして作成する作業が必要になってきます。すなわち、(1)プラグイン利用者の視点でいえば、そのプラグインにどんなポートやパラメータがあるか、どんな入力を受け付けるかは、RDFの情報だけでも把握できますし、(2)プラグイン作成者の視点でいえば、そのプラグインにどんなポートやパラメータがあるか、どんな入力を受け付けるかは、コードだけで完結せず、メタデータに記載しなければならないことになります。

プラグインでは独自の型を規定して公開することも可能です。MIDIメッセージやプリミティブ型(Atom型)などLV2標準に含まれる型の多くが、このルールに基づいて規定されています。もっとも、一般的なプラグインにおいて、複雑な独自型を公開型として定義する必要は滅多に無いでしょう。

モダンなプラグインフォーマットでは、プラグインが単一のライブラリファイルとして提供されていることはなく、関連ファイルとまとめて1つのフォルダなどにまとめて配置するものが多いです。LV2も同様で、「LV2パス」のいずれかに.lv2という名前で終わる「プラグインバンドル」のディレクト*2に関連ファイルをまとめます。この中に入るのは典型的にはこのようなファイル群です:

LV2フォルダ構成 ファイル名は任意に決定できますし、1つのプラグインバンドルに複数のプラグインを含めることもできます(manifest.ttlに記載する必要があります)。マニフェストの内容は、このようなテキストにまで圧縮できます(Turtle syntaxとはこのようなものです):

<http://example.com/arbitrary-paths/foobar>
    a lv2:Plugin ;
    lv2:binary <foobar.so>  ;
    rdfs:seeAlso <foobar.ttl> .

.soLinux環境における共有ライブラリなので、実際のプラットフォームに合わせて変わります。

LV2の拡張性

LV2の標準仕様は、バージョン1.0時点で既知であった最小限の機能のみを提供し、それ以降のバージョンで追加された機能については、ホストから渡されない限り、プラグインが利用することはできない仕組みになっています。

拡張機能の中には「URIと一位識別子URID(uint32)のマッピング」「プラグインのステートの保存と復元」「UIの呼び出し」など、かなり本質的なものが含まれているため、LV2をプログラムで扱うのであれば、LV2拡張に対する基本的な理解が不可欠です。

LV2拡張のインスタンスは、LV2_Featureという構造体で表されます。

struct LV2_Feature
{
    const char *URI;
    void *data;
}

LV2拡張はURIで種別が識別され、それぞれの拡張に応じて必要になるデータがdataに渡されます。多くの拡張機能はコードによる実装を必要とし、それらは(直接ないし間接的に)関数ポインタとしてホストからクライアントに渡されることになるので、dataが空であることは通常はありません。たとえばURILV2_URID_MAP_URIhttp://lv2plug.in/ns/ext/urid#map)のとき、dataに渡されるべきものはLV2_URID_Map構造体です。

// typedef void* LV2_URID_Map_Handle
// typedef uint32_t LV2_URID
struct LV2_URID_Map
{
    LV2_URID_Map_Handle handle;
    LV2_URID(* map)(LV2_URID_Map_Handle handle, const char *uri);
}

LV2プラグインは、lv2.hをインクルードしてLV2_descriptor()およびLV2_lib_descriptor()という関数を定義することになります。いずれもLV2ホストがプラグインdlopen()でロードした後、dlsym()で動的に呼び出すことになります。LV2_lib_descriptor()ではLV2_Feature*型のfeatures が定義されており、ここにはLV2ホストから「ホストがサポートするLV2拡張」のデータを含むリストが渡されます。

先のLV2_URID_Mapの例でいえば、mapがこの拡張機能の実体であり、この関数ポインタをプラグイン側はLV2_URID_mapの実装として呼び出せる、という仕組みです。

LV2プラグイン後方互換

LV2プラグイン機構は、LV2 Coreと数多くのプラグインによって成り立っていますが、それぞれのプラグインにはバージョンがあり、これらは必ずしも後方互換性があるとは限らないようです。筆者が試した範囲では、Atom型として定義されている型がXML Schema Datatypesに由来するxsd:*型であるとしてRDF上で定義されているものがいくつかあり、これらのプラグインをlv2_validateツールで検証するとwarningが報告されました。LV2の仕様自体が後方互換性を意識していても、実際に後方互換性が達成できるかどうかは、拡張となるプラグインのバージョン間互換性・設計次第であるようです。

もしかしたらこれはLV2_Atomをサポートした時点で生じた非互換問題で、2012年に出たこの記事でQTractorの開発者に言及されている問題かもしれませんが、2012年の問題が2019年になっても問題になっているとしたら、7年も前の非互換変更が今でも尾を引いているということであり、後方互換性の問題は無視できないかもしれません。

LV2ホスティング

オーディオプラグインを使うためには、オーディオプラグインMIDIなどの制御命令や音声データを渡して処理させるホストが必要です。一般的にはDAWと呼ばれるソフトウェアがこれを担いますが、プログラムとしてはLV2プラグインをロードして処理を実行できるものであれば何でもかまいません。たとえば、プラグインのテストには、テストのみを目的とするローダーが使われるでしょう。

LV2をサポートするDAWとしては、Ardour、Audacity、Carla、QTractorなどが有力です。

LV2ホスティングを実現するためのリファレンス的な実装として、lilvというライブラリがあります。ローカルにインストールされているLV2プラグインプラグインクラスをリストアップして、プラグインインスタンスを生成して、ポートをデータバッファに接続して、オーディオ処理を走らせることができます。

lilv

lilvはLV2ホスティングのためにあるCライブラリです。RDFの詳細の多くを隠蔽しつつ、プラグインクラスを検索し、プラグインインスタンスを生成して、音声データ処理を行えます。

RDFの処理は、同じ作者が開発しているserd(RDFの解析)、sord(Turtle syntaxの解析)、sratom(LV2 atom型とRDFの相互変換)、そしてlv2の4つのソースコード・パッケージにのみ依存しています。実際にはlv2などがさらにlibsoundioやcairoなどをオプションとしてサンプルプラグインのビルド時に参照することになるので、依存関係はもう少し膨れ上がります。

LV2のRDFを書けるようになるためには、RDFについてそれなりに勉強しなければならないことになりますが、率直に言えば、オーディオプラグインを開発するためにRDFを勉強するというのは、全く本質的な作業ではない、と言わざるを得ないでしょう。これは、lilvのAPIを調べていくことである程度緩和されます。RDFの面倒な部分はとりあえず置いておいて、lilvのAPIでworld, plugin class, plugin, port, instanceといった概念を理解したほうが早いです。

*1:有名どころではSteinberg社のCubaseApple社のLogic Proなど

*2:単なる一般則なのか仕様で厳格に決められているのかは不明ですが、筆者はこの例外を見たことがありません

JUCEにおけるz-orderの扱い

また音楽技術Advent Calendar 2019JUCE Advent Calendar 2019のcross postingです。(現状どっちもとても埋まる気がしない)

JUCE GUIのComponentには、その内容としてchild componentsをaddAndMakeVisible()関数を使って載せることができます。これはやや設計として失敗の雰囲気があるものの(全てのComponentが子をもつ設計は200x年代前半の臭いがありますね)、古典的には一般的なGUIコンポーネントの設計です。そして、コンポーネントの描画はpaint()関数で行われます。JUCEの子要素は自分でレイアウトして、自身の描画もpaint()で行うのが基本です。あれ? ちょっと雲行きが怪しくなってきましたね…? 大丈夫、子要素のpaint()関数も呼び出されるので問題ありません。

子要素はデフォルトではabsolute layoutのようにpaint()で渡されたGraphicsで渡される領域の範囲内で描画します。描画する領域は親から渡され、子の描画はその子のpaint()関数が行う…ということは、複数の子がある場合、描画順序によっては、ある子の描画内容を後から別の子が上書きしてしまう可能性があります。

これでは困りますね。少なくとも描画順序を制御できなければ困ります。こういう時にわれわれが使うのは、そう、「前面に移動」「背面に移動」です。MSOfficeですら制御できるアレです。もう少し真面目に言うとz-orderですね。

このz-orderですが、JUCEのComponentには対応するプロパティがありません。z-order無いのかよ!となりそうですが、z-orderを指定できる場所がひとつあります。Componentに子要素を追加する時に使うaddAndMakeVisible()関数です。

void Component::addAndMakeVisible (Component * child, int zOrder = -1)

zOrder : The index in the child-list at which this component should be inserted. A value of -1 will insert it in front of the others, 0 is the back.

これで順番を指定できそうですね。さっそく具体例を見てみましょう。簡単な重ね合わせの生じるデモを作ったので、これを土台に検証します。

f:id:atsushieno:20191210024147p:plain

#pragma once  
#include "../JuceLibraryCode/JuceHeader.h"  
  
class Component1 : public Component {  
public:  
  void paint(Graphics& g) override {  
    g.fillAll(Colours::green);  
  }  
};  
  
class Component2 : public Component {  
public:  
  void paint(Graphics& g) override {  
    g.fillAll(Colours::blue);  
  }  
};  
  
class MainComponent : public Component  
{  
  Component1 c1;  
  Component2 c2;  
  Label l1{"label1", "Label1"};  
  Label l2{"label2", "Label2"};  
public:  
  //==============================================================================  
  MainComponent() {  
  c1.setBounds(0, 0, 200, 200);  
  c2.setBounds(100, 100, 200, 200);  
  l1.setBounds(0, 0, 200, 200);  
  l2.setBounds(100, 100, 200, 200);  
  addAndMakeVisible(c1);  
  addAndMakeVisible(c2);  
  addAndMakeVisible(l1);  
  addAndMakeVisible(l2);  
  
  setSize (300, 300);  
  }  
  
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)  
};

JUCEのソースファイルなのでPIPにしてもよかったのですが、とりあえずデフォルトテンプレートのMainComponent.hを書き換えてMainComponent.cppの中身を空っぽにしただけです。多分ビルドできるでしょう。

緑の矩形と青の矩形が重なり合っていますね。この画面で、緑を青の上に出したいと思ったら、zOrderを指定してaddAndMakeVisible()を呼び出すと良いということですね。

  addAndMakeVisible(c1, 1);  
  addAndMakeVisible(c2, 0);  
  addAndMakeVisible(l1);  
  addAndMakeVisible(l2);  

f:id:atsushieno:20191210024254p:plain

なるほど順番が変わりましたね! 実際にレイアウトを調整するときは間に他の要素が入るかもしれないので、zOrderの差を大きめに設定しておきましょう。

addAndMakeVisible(c1, 100);  
addAndMakeVisible(c2, 1);  
addAndMakeVisible(l1, 110);  
addAndMakeVisible(l2, 10);

f:id:atsushieno:20191210024147p:plain

( ゚д゚) ・・・
 
(つд⊂)ゴシゴシ
 
(;゚д゚) ・・・

あ…れ…? なんか想定外の挙動ですね…?

zOrderってなんか値域とかあるんだっけ…?と思いながら情報を探してみると、こんなforumの議論が…

forum.juce.com

In particular, if a component has N children, then all z-indices >= N will be considered equivalent, because of this code in Component::addChildComponent:

「子要素の数がNだったらzOrderの値がN以上だったら全部Nに揃えられる」…そマ?

まじすか…どうもzOrderの設計はやっつけっぽい…。その後のスレッドでは「Componentのメモリ使用量は可能な限り最小限にしてあって云々…」とあるのですが、ホントか〜? ホントにメモリ使用量を気にするんならComponentがMouseListenerを実装しているのおかしくね〜?とか思ったりしちゃったり何だったりしますね…

ともあれ、zOrderはこういう感じで「期待通りに動作させるには慎重に使わないといけない」ものなので、使うときは十分に注意しましょう。たぶんaddChildComponent()がひと通り済んだ時点で適切な順序でソートしたほうが良いです。

以上juce_gui_basicsの小ネタでした。

オーディオプラグインの理想的なGUIフレームワークを模索する

音楽ツール・ライブラリ・技術 Advent Calendar 20197日目のエントリーです。今日はポエムに近いです。

オーディオプラグインGUIの要求事項

オーディオプラグインフレームワークというソフトウェアはやや特殊な世界で、歴史的な経緯を脇に置いて2019年現在に求められている要件を列挙するなら、

  • WindowsおよびMac、可能なら*1それ以外(Linux, Web, iOS, Androidなど)をサポートすること
  • オーディオ処理とGUI機構を提供すること
  • DAW上から起動できてGUIイベントを部分的に共有すること(キーボードイベントなど)

などが挙げられます。今回はGUIフレームワークについて少し掘り下げて検討します。先日M3で頒布した同人誌では言及しなかった部分ですね。

VST SDKのように独自ブランドのGUIフレームワークを提供するものや、JUCEのように複数オーディオプラグイン機構・複数プラットフォーム向けに独自のGUIフレームワークを提供するものは、いくつか存在します。オーディオプラグインフレームワークにおいては、GUIフレームワークの提供は必須ではありません。しかし、どのようなGUI機構であってもオーディオプラグインフレームワーク側と接続可能になるような、オーディオ/MIDIストリームの出入り口を用意しておく必要はあります。

オーディオプラグインに求められるGUIアプリケーションとしての要件は、一般的なデスクトップGUIアプリケーションのものと比べると、だいぶクロスプラットフォームで開発しやすいものであるといえます。その理由のひとつは、起動および操作がほぼ必ずDAWを経由したものになるためです。メインメニューは無く、キーボードイベントを「DAWから奪い去る」ことがあまり歓迎されず、DAWからダイアログのようにポップアップされているときだけ操作することになります。

さまざまな制約を受けながら作成できるGUIの利便性は当然ながら一般的なGUIアプリケーションよりだいぶ低く、そのようなGUIであれば「ネイティブアプリケーションのポテンシャルを全然引き出せない」クロスプラットフォームGUIフレームワークの欠点が目立たなくなります。

VSTGUIやJUCE (juce_gui_basics) は、そのような特殊な環境で発展してきたといえます。もちろん、Carbon/CocoaWindows APIを直接使うGUIをもつオーディオプラグインもあり、これらも一般的なGUIアプリケーションに求められる要求事項とはだいぶ無縁に作られてきたはずです。

また、オーディオ処理がリアルタイムで厳格なフレームの中での完結を求められることもあり、これに対応するかたちでGUIを構築するのであれば、ウィジェット/コントロールを操作するGUIフレームワークを使うよりは、コールバックベースの低レベルの描画APIに基づいて実装するものが多くなるのも、それなりにわかりみがあります。

もちろんGUIの描画そのものはオーディオスレッドで行われるべきものではないので、ユーザーコードでオーディオコールバックから呼び出される描画呼び出しはpostにとどまり、実際の描画処理はUIスレッドで行われることになります。一方で、そもそもGUIはオーディオ処理に100%追従することが前提となっていないので、ある程度の遅延は容認できるし、何ならオーディオ処理とは異なりGCJITで世界が止まっても致命的な問題ではない、ということがいえます。実際これはAndroidにおけるオーディオAPIの開発方針でもあります(UIはART上でKotlin/Javaでも可能、オーディオはNDK + OpenSLES/AAudio)。

どのようなx-plat GUI Fxが向いているのか

さて、こうなってくると、オーディオプラグインGUIフレームワークは、クロスプラットフォームGUIフレームワークが利用できる分野であるといえそうです。

もし「低レベルの描画APIだけ提供していれば良い」というのであれば、CairoなりSkiaなりを使えば解決ということになりますが、実際にはマウスやキーボード入力のサポートも必要になるので、きちんとアプリケーションループをもったGUIツールキットであることは必要でしょう。

ここで「クロスプラットフォームフレームワークでは実現できない、プラットフォームの入力デバイスを完全にサポートする必要がある」という立場であれば、ネイティブのGUIツールキットで個別に開発するのが妥当でしょう(Surface DialがLinuxでも使えてLeap Motionのようなデバイスがどのプラットフォームでも動作する現在、筆者としてはそのような状況はあまり一般的には想定できないところですが)。

クロスプラットフォームGUIフレームワークを使う路線でいくとしても、このカテゴリにまとめられるGUIフレームワークにはいくつかの種類があります。

(1) JavaScriptとブラウザ環境が前提であるもの。CordovaやIonicなどが挙げられます。JUCEコミュニティの一部ではReactを使ってGUIを構築するアプローチが話題となっているようです。このやり方であればWeb開発の手法でオーディオプラグインGUIを開発できることになるので、開発は楽になりますがブラウザを組み込むことになるのはアプリケーションとしてはだいぶ重量感が増します。(オーディオプラグインにはGB単位でサンプリングを含むものが多く存在するので、それに比べたら誤差のようなものではあります。) またWebViewの組み込みはどんな環境でも問題を引き起こすことが多く、プラットフォームやブラウザコントロールのバージョンなど、環境による挙動の違いをもたらす変数が大きいこともマイナス点でしょう。

(2) プラットフォームごとにネイティブのUIコントロールを呼び出すように実装されたもの。XamarinやReact Native、WxWidgetsはここに分類されます。開発が楽になるかどうかはそれぞれの開発者のバックグラウンド次第、アプリケーションとしての配布が楽かどうかもフレームワーク次第と、十把ひとからげに評価するには変数の多いところです。プラットフォームのネイティブコントロールがUX満足度を高める要因になりますが、オーディオプラグインGUIでは、伝統的に標準コントロールがあまり使われていないので、伝統的なGUIではメリットが小さそうです。一方でプラットフォームごとに挙動がバラバラになるデメリットはそのままです。React NativeはWindows用のバックエンドはあるものの、Gtkなどはサポートされていないでしょう。XamarinのGtkサポートも実は貧弱です。Gtk2なので(!) もともとはGNOMEの開発を主導していた会社なのに(!)

(3) UIコントロールを独自にレンダリングするもの。Gtk, Qt, Flutter, JUCE, Unity UiWidgetsはこのカテゴリに属するといえます。これらは、フレームワークの完成度が重要なファクターになってきます。特にどのプラットフォームでも満足に動作するものは皆無といってもよいでしょう。QtやGtkLinux以外ではあまり歓迎されず、オーディオプラグインGUIとしても実績値は高くありません。またQtもGtkもデスクトップが「どちらであるか」によってエイリアンになる可能性が高く、まだX11が直接使われている可能性が高いといえるでしょう。しかしX11もまたWaylandなどで置き換えられつつある存在です。また、オーディオプラグインの主戦場はデスクトップ環境であり、Androidで快適、iOSでもまあまあ許容されているFlutterは、デスクトップではまだまだ発展途上(そもそも開発中)なので、現状で適切な選択肢であるとは言いがたいところです。不十分な多言語入力もこの種のフレームワークにありがちな問題です。

opinion

JUCEは古典的なオーディオプラグイン用のGUIフレームワークとしては必要最低限の機能を提供していて、しかもC++なので、現在ではよく採用されています。ウィジェット・ツールキットとしては他のフレームワークと比べると描画APIに毛が生えた程度であり、少しでも高度な機能を使おうとすると自前で実装することになります。基本中の基本のようなHBox/VBoxのようなレイアウトが"Advanced GUI layout techniques"と言われるのがJUCEのレベル感です。レイアウトエンジンがウィジェットとして組み込まれておらず、単独で存在するFlexBoxやGridのレイアウトの実装などを自前でコントロールの描画に適用することになります。JUCEはユーザー数の割には開発者層が薄く、特にjuce_gui_basicsは絶望的なので、筆者としては全然将来に期待していません。

(レイアウトエンジンは必ずしもGUIフレームワークと密接に結びついているわけではありません。具体的な例を挙げると、Facebookが開発したCSS Flexbox相当のレイアウトエンジンであるYogaはWeb以外でも使うことができましたし、AppleのAutoLayoutやAndroidのConstraintLayoutはCassowaryと呼ばれる制約付きレイアウトを実装したもので、Cassowary自体はGUIフレームワークから独立して存在しうるものです。なのでたとえばAbletonがQt用にCassowaryを実装することもできたりします。独自のGridレイアウトも同様といえます。)

筆者がこの分野で一番将来に期待しているのはFlutterです。FlutterはMaterial Design Componentをフル実装しており、十分にモダンなGUIを構築できる一方で、描画の低レベルレイヤーはSkiaが使われており、すなわちVulkanやANGLE、Metalのポテンシャルを引き出せる可能性が十分にあります(SkiaのMetalバックエンドはiOS 11以降のみですが)。Flutterでなくても、モバイル環境で育ったGUIフレームワークであれば、たとえばマルチタッチに対応した豊富な機能を持つGUIを構築することが可能でしょう。juce_gui_basicsで満足していたら逆立ちしても実現できません。

ただし、Flutterがオーディオプラグインの世界に適用されるためには、C/C++と相互運用できるだけの十分なFFIサポートが不可欠です。この部分はDartでまだまだ発展途上であり、残念ながら筆者がlibclangで相互運用を試してみた範囲では、とても実用段階にあるとは言いがたいところです。プラットフォーム独立の動的ライブラリのロードや、Stringの変換くらいは、ファーストクラスで実装されている必要があるでしょう。

あと、JUCEで最近さかんに試されているReact NativeというかJavaScriptにもいえることですが、float型が無いのはホントに大丈夫なのか?っていう問題はあるかもしれません。まあWebAudioで実証済みの世界とはいえるかもしれません。

*1:わたしはLinuxをサポートしないフレームワークには価値も未来も無いと考えています

MIDI 2.0の時代に備えて「FPGAとARMで作る自作 USBオーディオインターフェース」(同人誌)を読み解く

これは【推し祭り】技術書典で出会った良書 Advent Calendar 2019https://adventar.org/calendars/3997のクロスポスティングです。

目次

音楽技術書をもっと増やしたい…!

技術書典は誰でもフルスクラッチで執筆から始められる技術同人誌の即売会で、いわゆる「商業」では販売部数の見込みが立たず全く出版の機会が無いようなマイナー技術の本であっても、自分の信念がある限り何の躊躇もなく執筆して販売するところまでもっていけます*1

個人的には、書店のコンピューター関連書籍のコーナーにCG・DTM関連のコーナーがあるように、技術書典にもグラフィックス方面のテクニックに関連する書籍がいくつか出ているので、音楽の打ち込み指南などの書籍も出てほしいなと思っているところですが、今回は音楽関連の技術書としてハードウェア方面でカテゴライズされていそうな「FPGAとARMで作る自作 USBオーディオインターフェース #1 USB-MIDIバイス編」を紹介します。

mmitti.info

USB-MIDIバイスってなんじゃらほい?と思われるかもしれませんが、ひとつはMIDIキーボードなどの外部MIDI入力デバイス、もうひとつは外部MIDI出力デバイス、いわゆるMIDI音源モジュールです。今どきはあまり見かけないものですが、Roland(GS音源)、YamahaXG音源)、KORGといった会社がよくキーボードの付いていないシンセサイザーを出していました。本書の場合は「DTMにお金をかけない」ためにMIDI音源モジュールを自作することが主眼にあるようです。*2

本書の紹介でも十分な内容になるかと思いますが、ちょうど先月ロンドンで行われたADC (Audio Developers Conference) 2019でMIDI 2.0に関するセッションが行われていたので、MIDI 2.0の時代にこういった自作ハードウェアは使えるのか?といった話も補足的にまとめておこうと思います。

本書の内容

本書はFPGAを使ってUSB-MIDIインターフェースを制作するための情報がまとめられている本です。著者がDigilentのPynq-Z1を使って自らUSB-MIDIインターフェースを制作してきたことをもとに書かれているので、具体的かつ詳細にまとめられています。ほとんどのページに図表が入っていてかなり親切に書かれています。

USB-MIDIバイス編、とあるのは、今回はオーディオI/Oに関連する部分の言及がほぼ無く、オーディオとMIDIを両方処理する部分は「次回」になるようです。もっともあとがきから察するに、次回はオーディオ部分が中心で、MIDIメッセージによってリバーブ/コーラス/ディレイなどのDSP処理をかませたり、ピッチ指定によって周波数を変換したりといった、MIDI音源モジュールらしい部分は、次回よりさらに先ということになるのでしょう(想像)。ここまで到達すると、洋書でも無いレベルなんじゃないかと思いますし、ぜひとも完結したものが見たい…! *3

MIDIの基礎知識

USB-MIDI音源モジュールを制作するためには、まずMIDIの知識が必要になります。MIDI仕様の中には、MIDIメッセージをシリアル通信の規格であるUARTに基づいて接続する方式に関する規定も含まれていて、本書でもしっかり関わってくる部分なので、きちんと言及されています。*4

実装者向けの記述としても、ランニングステータスはきちんと処理する(受ける側は省略されたりされなかったりする送信側の挙動にまんべんなく対応しないといけない)とか、アクティブセンシングとは何であってどのように対応しないといけないとかいった話が、ちょいちょい親切に書かれていて、読み込む価値があります。

MIDI回路(FPGA)

MIDIの基本仕様の話が終わると、いよいよFPGAでこのMIDI回路をどのように実装するかという話になります。この段階では、まだUSB-UARTの接続に関する話題は出てきません。MIDIインターフェース、オーディオインターフェースの部分はFPGAだけで完結します。

FPGAの回路設計は概ねわたしの守備範囲外なので概ね表層的な話しか読み取っていないのですが(この本ではMIDIとかは詳しく解説しているけど、FPGAについてはほぼ知っていることが前提のようなので、他の資料で予習する必要がありそう)、設計レベルではブロック図やステートチャートでメッセージがどのようにやり取りされているかが親切に示されています。

実装されているのは(1) Advanced Extensible Interface (AXI) に基づいてARM CPUとMIDIインターフェースを接続する回路、(2) MIDIシリアル通信用のクロックジェネレーター回路、(3) MIDIバスからUARTに変換してシリアル出力ための回路、(4) 逆にUARTを経由して届いたメッセージをMIDIバスに変換する回路(ここはシステムメッセージとチャンネルメッセージが分岐するのでややこしいらしく、説明も長い)…とあり、これらをCPUプログラムとして配線・論理合成するところまで説明されているので、FPGA使いの人には十分わかりやすいであろう内容なんだと思います。わたしはFPGAの知見が無いので「ステートマシンの様子とか詳しく書かれているし、やればわかりそう」という次元の印象です。

MIDI外部回路の接続(5PINのコネクター)に関する説明の短い章もあります。

USB

本書の後半は、PC側からUSBを経由してARM CPUの搭載されたFPGAベースのMIDIバイスを操作するために必要になるUSB-MIDI接続の部分に関する説明になっていて、まずはUSB通信の仕様について詳しくまとめられています。USBエンドポイントとは何か、USBインターフェースとは何か、といったレベルの解説からあるので、この分野の素人であるわたしでも読める内容でした。物理層は一般的な基板側にあって自前で実装する必要がないので、データリンク以上の話が中心となっています(実践的にもそれで十分そう)。なお本書で説明されるUSBは2.0です(2.0の仕様で十分実現可能であるようです)。

USBパケットの構成要素や、転送方式(USB-MIDIで使用するのはバルク転送とコントロール転送のみです)、USB接続状態の遷移、標準リクエスト、ディスクリプターなど、おそらくUSBハードウェア開発全般に通じる内容なので、ここは他の資料で勉強できそうな部分ですが、親切に書かれているので本書だけで読み進められます。USB-MIDIに関連する部分だけを勉強するには効率的かも? データパケットの説明も含まれているのですが、エラーを明示的に返す場合に実装する必要があることを踏まえてのものです。

USB-MIDIはオーディオクラスのサブクラスとして標準で規定されているため、USB標準に準拠してデバイスを実装すれば、ドライバーを自分で実装する必要が基本的にはなくなるということも説明されています(そのため、USB-MIDIの章ではソフトウェア実装の説明がほとんどありません)。

USB-MIDIの仕様

USBの説明が終わるといよいよUSB-MIDIの仕様の説明に入ってきます(仕様についてはUSB-IFで規格化されているので、この本でなくても学べることではあります)。冒頭から、1本のUSB仮想ケーブルで16本のMIDIケーブルを扱えるという話があって、なるほど確かにこれでMIDI IN / MIDI OUTが8組処理できるな…という発見があります。

前章ではUSB標準の一部としてデバイスディスクリプターを送信する部分を実装する必要があることが説明されており、この章ではUSB-MIDIバイスディスクリプターに求められる内容としてポート数や接続に関するトポロジーがあり、それらをどう記述するか、といったことが説明されます。

それからUSB MIDIストリーミング インターフェース(USBのインターフェースの何たるかについては前章で説明があります)でやり取りされるUSB-MIDIのイベントパケットの形式の説明を経て、USB-MIDIバイスディスクリプターの内容のうち固定値のある部分についての割と長い説明を経てこの章は終わりです。ほぼ仕様のみの話です。

USB-MIDIの実装

ここまでの説明(どれも必要なやつ)を経て、ようやくこのUSB-MIDIを筆者がどう実装されたか説明されています。前半はUSB-MIDIディスクリプターの内容(固定値でない部分)、後半はUSB処理を行うファームウェアで実装されているコードの説明です(使用するマイコンによって実装が変わるので擬似コードのみ)。USBエンドポイントのメッセージ処理は割り込みハンドラー(そういえばこの辺の基礎的な単語はわからないと読めない気がする)として実装され、標準で必須になるEP0とMIDIイベントの送受信に使われるものとで2つあります。EP0のほうも説明がありますが、ファームウェアによってはほぼ実装が丸投げできそうで、Zynqではもろもろ丸投げできたようです。MIDIイベントのほうは、USBメッセージは受信したら即応しないといけないという仕様の都合上、一度リングバッファーにenqueueすることになりそうです(本書の実装ではそうなっています)。

リングバッファーのdequeueはファームウェアのmain関数の中でループ処理で行われ、dequeueされたメッセージはAXI-MIDIの回路にMIDIイベントとして書き込まれます。一方で、MIDI入力メッセージが合った場合には、このループの中でAXI-MIDI回路の対応レジスタの状態をチェックして、メッセージありとなっていたらUSB MIDIメッセージに変換して入力側としてのリングバッファーにenqueueするようです(これはUSBホスト側がfetchするまで保持することになるでしょう)。

…といった全体的な処理の流れが、本書を読んでいくとわかります。MIDIトランスポート、USB、USB-MIDIに関しては素人だったわたしでも読めるレベルでまとめられていて親切な本です。

MIDI 2.0で変わること

さて本書は当然ながらMIDI仕様を前提に書かれていて、MIDI仕様といえば1980年代に策定されたものなのですが、2019年になって、38年ぶりにこの仕様に変更を加えたMIDI 2.0と呼ばれる仕様が議論されつつあります。MIDI 2.0には、MIDI 1.0を前提としたMPE (MIDI Polyphonic Expression)やMIDI-CI (Capability Inquiry) といった既成の仕様に基づく機能拡張も含まれているのですが、他にもMIDIメッセージの拡張やチャンネル数の拡大など、全般的にモダンな内容になる予定です。

www.midi.org

www.dtmstation.com

MIDI 2.0の時代になるとMIDI 1.0にのみ対応したデバイスは使えなくなる、ということはなく、MIDI 2.0はMIDI 1.0と基本的には後方互換性を維持した仕様になるようです。たとえばMIDI-CIでは双方向的なメッセージングが必要になるわけですが(MIDI 1.0にはリクエスト/レスポンスという概念がありません)、MIDI-CIに基づくメッセージをデバイス側が理解しない場合にはレスポンスが返ってこないわけで、その場合はホスト側はMIDI 1.0を前提としたメッセージを送ることになります。その場合でも、MPEのメッセージはMIDI 1.0の形式で送信できるので、たとえば未来のMPEデバイスMIDI 2.0の形式でメッセージを送信しつつ、2019年時点で現存するROLI BLOCKS(MIDI 2.0を解さない)にはMPEに基づくメッセージを送信する、といった処理の分岐が可能です。

この意味で、MIDI 1.0のみを対象とするUSB-MIDIバイスの開発に関する本書の内容は、まだ十分に役に立つと言えるでしょう。本書では入出力の双方が実装されていることがうかがえるので、MIDIメッセージを受け取った側でMIDI-CIに基づくリクエスト/レスポンスを処理出来る可能性は十分にあります。おそらくメッセージングは単発のリクエスト/レスポンスで処理可能で、何らかの状態管理を必要とするものではないとは思いますが、MIDI-CIのProfile ConfigurationとProperty Exchangeがどんな内容になるのかは見ておいても良いかもしれません。AppleがCore MIDIMIDI-CIをサポートしています。

もっとも、MIDI-CIのサポートだけではMIDI 2.0に対応できているとはいえません。MIDI 2.0仕様には、新しくUniversal MIDI Packet(以降UMP)と呼ばれるパケットのフォーマットが含まれています。これは最大128ビットのメッセージを含むパケットであり、MIDI 1.0の範囲を超えるものです。MIDI 2.0ではこれをチャンネルボイスメッセージに適用して高分解能のメッセージ(コントロールチェンジが128段階から65536段階になったものを想像してください)のやり取りが可能になります。MIDI 2.0対応デバイスであるためには、UMPを処理できるようになる必要があるでしょう。もっとも、高分解能のメッセージであっても、MIDI 1.0の機能にマッピングされる範囲では、精度を落として7ビット値(ピッチベンドなら14ビット)で送信する等の措置がとられるようです。

また、MIDI 2.0のUMPにはJitter補正のためのタイムスタンプを含めるようになったようです(フォーマットの詳細は公知情報からは不明です)。これまでソフトウェアのレベルで処理されていた(と思われる)タイムスタンプ補正がMIDIメッセージでやり取りされるハードウェアのレベルで要求され、メッセージのバッファリングの実装が影響を受ける性質の仕様であるかもしれません。ordered queueに実装させられるのはちょっと嫌かも…?

また、チャンネル数が拡大されて、ユーザー的には最大256チャンネルまで利用できるようになるのですが、これは16のグループごとに16チャンネルを操作できるようにUMPが設計されている、というアプローチになっていて、これはもしかしたらUSB-MIDIのインターフェースがそのまま使えるということなのかもしれません(詳細は要確認ですが)。MMAはUSBの仕様を策定できる立場にはないので(USBの仕様を決めるのはUSB-IF)、MIDI 2.0では可能な限りUSB-MIDIの仕様がそのまま使えるような仕様を目指すのではないかと思います(想像)。

…とまあ、いろいろと考慮事項が増えるようですが、MIDI 2.0はまだ仕様策定中であり、実装したデバイスも無ければMacOSMIDI-CIサポート以外は何もない状態なので、まだまだ未来形の話と考えても良いでしょう。

*1:もちろんイベント自体のルールや行動規範はあるわけですが

*2:ハードウェア制作費は…ゲフンゲフン

*3:まあCQ出版あたりでしれっと出ていてもおかしくはない気もしてきました…

*4:ちなみに本書では物理層となっているのですが、わたしにはデータリンク層にも見えるというか、どこまでが物理層といえるのかOSIモデルよくわからん…という感じです。

JUCEモジュールを作って外部ライブラリを参照する

これは音楽技術Advent Calendar 2019の3日目(まあ2本目ですが…)とJUCE Advent Calendar 20193日目のクロスポストエントリーです。

長い前書き

JUCEのビルドシステムは、アプリケーションやオーディオプラグインのビルドに必要なコードを原則として全てソースコードからビルドするという仕組みです。JUCEはさまざまなモジュール群から成り立っていますが、その全てがソースからビルドされることになります。Linuxで言えばGentooです*1。これは複数のプラットフォームをターゲットとするビルドシステムを構築する場合には手っ取り早いハック(やっつけ仕事)であるといえます。

世の中には、アプリケーションのビルドに必要なものは全てソースからビルドされるべきだ、という発想の人もいますが、おそらくその他ほとんどの人はビルド済みのライブラリを使ってビルドするほうが賢いと考えます。地球環境のことを考えれば、モジュールを毎回ソースからビルドして電力を浪費するのは悪であるとすらいえます*2。そして、そもそも自分ではビルドできないようなOSのモジュールを、自分のアプリケーションから参照したくなる場合があります。

バイナリのライブラリを使えるようにするためには、それぞれのプラットフォームやビルドツールチェインでそれらを利用できるようにサポートしなければなりません。JUCEはプラットフォームの違いを吸収するだけでなく、ビルドツールチェインの違いも吸収しなければなりません。Visual Studio (for Windows)の複数バージョン、xcodeMakefile, CMake (CLion)と、多大な組み合わせと対峙することになります。

ライブラリの参照の仕方も、ライブラリファイルを直接指定する(ついでにLD_LIBRARY_PATHのようなライブラリ解決パスも追加指定する)方法と、pkg-configのようにパッケージとしてでないと解決できない方法があります。C++には、node/npmやruby/gemやpython/pipや.NET/nugetやJava/mavenなどきちんと整備された言語開発環境とは異なり、プラットフォームの違いを意識せずに利用できる依存関係解決のためのソリューションがありません。

結局JUCE/Projucerでは、まともなプラットフォーム中立の依存関係解消方法を用意できず、Exporter別にオプションでライブラリ参照を解決することにしました。オプションはProjucerのビルド設定ファイルであるところの.jucerファイルに含まれます。Exporter別に設定するのはちょっと面倒くさいですね。結局出力先ごとのオプションの指定方法を調べないといけないことになりますし。

JUCEモジュールを活用してライブラリをソースからビルドする

こういう煩雑さが面倒だ、それならばライブラリをソースからビルドしてインクルードファイルなどは直接参照したい、という人のために、JUCEではユーザーのモジュールから外部ライブラリを取り込む方法が用意されています。今回はこのカスタムJUCEモジュールを作る方法から説明します。

JUCEモジュールというのは、JUCEを使ってコードを書いている人であれば自明かと思いますが、JUCEの機能ごとにまとまったライブラリの一部分のようなものであり、JUCEではモジュール単位でユーザーのプロジェクトに含めるかどうかを指定します。JUCEモジュールには依存モジュールの概念があり、要するにライブラリのようなものです(ただしソースの集合体であり、バイナリを参照することはありません)。JUCE本体にあるjuce_audio_basicsjuce_gui_basicsなどがモジュールです。

このモジュールはユーザーが独自に作成することもできます。作り方は簡単です。foo_barというモジュールを作るには、foo_barというディレクトリを作って、その中にfoo_bar.hfoo_bar.cppというファイルを2つ作成するだけです。ただし、このfoo_bar.hには「JUCEモジュールフォーマット」に準拠したヘッダコメントが必要になります。ヘッダコメントの書式はこんな感じです。ちょっとPIPっぽいですが別物です。

/*******************************************************************************
The block below describes the properties of this module, and is read by
the Projucer to automatically generate project code that uses it.
For details about the syntax and how to create or use a module, see the
JUCE Module Format.txt file.

BEGIN_JUCE_MODULE_DECLARATION

ID: augene_file_watcher
vendor: atsushieno
version: 0.1.0
name: auegne file watcher
description: Classes for file system watcher/notifier
website: https://github.com/atsushieno/augene
license: MIT

END_JUCE_MODULE_DECLARATION
*******************************************************************************/

このヘッダファイルの残りの部分には、自分のクラス定義などを書いても良いですが、一般的には別のヘッダファイルを含めるほうがクリーンに見えるでしょう。そして、ここにはサードパーティーライブラリのヘッダファイルを含めることもできます。

このヘッダファイルは、Projucerが生成するJuceHeader.hの中で自動的にインクルードされるので、その中でサードパーティーライブラリのヘッダファイルも自動的に参照できるわけです。これは楽ちん。

さて、自作したJUCEモジュールは、まずプロジェクトに追加しないと認識してもらえません。ProjucerでModulesのセクションにある+ボタンをクリックして、モジュールのディレクトリを指定します。

f:id:atsushieno:20191203110405p:plain

モジュールが正しく認識されると、Modulesのリストに自分のモジュールが追加されます。(ヘッダファイルが正しいフォーマットになっていないとここで各種エラーメッセージが出ることになるので、メッセージの内容からエラーを推測して修正します。)

f:id:atsushieno:20191203110429p:plain

さて、さっきはもうひとつ.cppファイルも作成しました。一般的なJUCEモジュールでは、この中に実装を書くことは実際にはあまりなく、このファイルにはこのモジュールに含まれるソースコードを #include で列挙することになります(!) つまりMakefileなどでソースファイルを列挙しているような感じですね。そして、ここでもまた、サードパーティーライブラリをビルドする場合は、ここにそのソースコードを列挙すれば、きれいに外部ライブラリがビルドできるではないか、というのがJUCEプロジェクトのスタンスであるようです。

JUCEモジュールでビルド済みバイナリをリンクする

ところでここまで読まれた皆さんはお気づきでしょうか? JUCEモジュールでサードパーティライブラリのソースのビルドなんて実際にはできるはずがないということに(!)

少なくとも、こんなのは一般的に通用するソリューションではありません。

その理由は、一般的にはライブラリはそれぞれのビルドシステムに沿って作成されているものであり、それぞれには適切なコンパイラーオプションやリンカーオプションがMakefileやCMake、xcodeproj, vcxprojなどで指定されているためです。JUCEモジュールで指定できるのはインクルードするヘッダファイルとビルドするソースコードだけです。これでは全く足りません。だいたいLinuxでautotoolsとか使っていたらconfig.hとか生成するわけで、この仕組みではそういうものに全く対応できないわけです。

そういうわけで、サードパーティーライブラリを取り込むなら、別途ビルドして面倒でもそれをExporter別にリンク指定してやるほうが現実的っぽいですが、実はもうひとつプラットフォーム別のビルド指定を回避できる可能性がひとつあります。それはこのJUCEモジュールで利用できるビルド済みバイナリの参照という選択肢です。

JUCEモジュールでは、ビルド済みのサードパーティーライブラリを以下のようなサブディレクトリに置いておくことで、自動的にビルド時にリンク対象として検索してくれます。たとえば…

  • libs/VisualStudio2019/Win32/MTd - Visual Studio 2019用 MTd - あるいはMT(スレッディングモードに合わせて調整します。VSのバージョンも2017などが使えます)
  • libs/MacOS/x86_64 - MacOS
  • libs/Linux/native - Linux用(要注意)

iOSAndroidなどもあるのですが、先にリンクしたJUCE_Module_Format.txtに詳しく書かれています。ただし、ひとつ要注意なのですが、Linuxに関する記述がデタラメで、このドキュメントにはABIをディレクトリ名として使うように書かれているにもかかわらず、実際にはnativeというディレクトリ名がLinuxMakefile/Makefileに追加されます。これはROLIに報告済みのissueで何が問題なのかも明らかになっているのですが、本エントリー公開時点でも未修正です。

(実のところ筆者が検証したのはLinuxWindowsのみで、MacOSについてはドキュメントに書いてあるのをそのまま紹介しているだけです。見ての通り、このドキュメントは信用できないので、実際に通るかどうか気になる人は検証してみてください。)

そして、このビルド済みライブラリは、自動的にリンクされるわけではなく、プラットフォームごとにJUCEモジュールフォーマットの書式に沿って、それぞれのプラットフォームに合ったプロパティとして指定する必要があります。

    linuxLibs: foo_bar
    OSXLibs: foo_bar
    windowsLibs: libfoo_bar // libfoo_bar.dllの場合. foo_bar.dllなら foo_bar でOK

いずれにしろ、サードパーティーライブラリを使う場合は、それぞれのプラットフォーム上でいったんビルドしておいて、ヘッダファイルを所定のディレクトリ上にコピーしてそれをモジュールのヘッダファイルで #include しつつ、モジュールフォーマットに沿ってプロパティを追加すれば、そのライブラリが使えるようになる可能性が少し上がります。

ここまでお膳立てしてやるくらいなら、Exporter別にリンク指定したほうが早いのでは…?とも思ってしまいますが、少なくともオプションの指定方法などを知らない環境向けにオプションを調べて記述するよりは、こっちのほうが楽かもしれません。わたしも実のところvcxprojやxcodeprojでどんなリンク指定を受け付けるのか知らないのですが(もちろん調べればすぐ分かる話ですが)、この方法であればすぐ指定できますし。

補論: インクルードファイル解決パスの問題

実のところ、リンクするライブラリをビルドするより前に、ヘッダファイルが解決できないという問題が生じるのが一般的です。「サードパーティライブラリのヘッダファイルを別のヘッダファイルで #include すればOK」というProjucerの発想はカジュアルなもので、JUCEモジュールのやり方では、pkg-configが追加で-Iオプションを指定してくれるようなことは、モジュール単位では一切やってくれません(Linux Makefile exporterではpkg-configライブラリを指定できます。もちろんLinux専用)。ビルドするサードパーティライブラリがさらに別のライブラリに依存して、それが追加のインクルードパスを必要とする(そうしないと「システム上にインストールされている」インクルードファイルの解決に失敗する)場合には詰むことになります。

これを回避するには、プラットフォーム別のExporterにコンパイラーオプションで-Iを追加すれば良いということになるのですが(追加インクルードパスはプラットフォーム別のDebug/Releaseの設定の中にHeader Search Pathsがあります。ちなみにexterCompilerFlagsというビルド設定に依らないプロパティもあるのですが、CLion Exporterが取り込んでくれない等の問題があります)、それならJUCEモジュールで解決するのは無駄ですし、システムによって場所が異なる可能性があるからこそpkg-configというソリューションがあるわけで、この意味でもProjucerのやり方はお粗末です。まあもともとビルドシステムとしてはやっつけツールだし…?

いくつかのJUCEモジュールで採用されている対策としては、もうモジュールの中に必要なヘッダファイルを直接放り込んだ上で、参照するインクルードファイルの位置を書き換えて対応する、というものです。さすがにシステム上にあるものは解決できないので、これは部分的な解決方法ということになります。

将来の展望

さて、ここまでいろいろな問題の解決方法を紹介してきましたが、いかがでしたか? わたしの個人的な感想としては「もうProjucerのビルドシステムはあきらめてCMakeを使おう」なのですが(パッケージ参照もちゃんと解決できるしVisual StudioでもAndroid StudioでもCLionでも開けるんですよコレ)、Projucerの負の遺産があるうちはまだまだ無理かもしれませんね。

JUCEの本来的な魅力はなんと言ってもクロスオーディオプラグイン・ホスト開発が可能になることなので、その魅力をフルに引き出せるように、JUCEのビルドシステムはもっとさまざまな開発者にリーチできるように発展的に解消していってほしいと思っています。

*1:っていう説明の仕方はちょっと時代遅れかな? 今はバイナリもインストールできるんですよね

*2:いまCI業界を敵に回した気がしてきたぞ

【音楽技術AC12/1】 ソフトウェアMIDI入力デバイスを作ろう

音楽技術Advent Calendar 2019の1日目エントリーです。今年はちょっと手が回らなそうなので「たまに書く」くらいのノリで進めていきたいと思います。かなり空き枠があるので、もし書けそうな話題がありましたらぜひご参加ください。2018年版を見るとわかりますが、割と手短なエントリーでも全然大丈夫です。

MIDIキーボードを使いこなせない…!

みなさんはDAWで打ち込むときにMIDIキーボードを使っていますか? たぶん少なからぬ人がYESと答えるのではないかと思います。DAWでマウスでポチポチとピアノロールに音を追加していく作業、しんどいんですよね。MIDIキーボードなどで入力して、ちょいちょい音長などを調整していけば効率的です。

…わたしこれがダメなんです。主な作業環境がノートPCなのでMIDIキーボードを繋いでいないことが多いのと、ふだん楽器を弾かないので間違いだらけになってしまうのが主な挫折ポイントです。まあどう考えても慣れるしか無いですね。

とはいえ、DAWでマウス入力していくのはしんどいので、もう少し別の方法で入力できないものか…とは思うわけです。MIDI音源はハードウェアでもソフトウェアでも良いのだから、MIDI入力デバイスもハードウェアではなくソフトウェアで作っても良いのでは…?

というわけで、ソフトウェアMIDIキーボードが誕生しました…というのはちょっと今さら感ありますね。2019年にあるオーディオプラグインには、PCキーボードを押すとMIDIノートメッセージとして処理してくれるものが少なからずあります。いずれにしろ、これは便利なので単体で実装する価値があります。

実際にはキーボードである必要はないのですが、今回はとりあえずどのPCにもありそうな「入力デバイス」であるところのキーボードを使います。

MIDI出力デバイスに接続するソフトウェアMIDIキーボードを作る

まず最初に思いつくのが、プラットフォームの標準MIDI APIを使って、接続したいMIDI出力デバイスを選択して、そのデバイスMIDIメッセージを送る、というものです。これはそんなに難しくありません。 GUIでは、何らかのキーボード入力イベントを受け取れるコントロールをウィンドウに置いて、入力されたキーに応じてMIDIノートオン/ノートオフのメッセージを送信するだけです。

このアプローチでは、MIDI音源で演奏することはできるのですが、DAWのようにVSTAUなどのオーディオプラグインをトラックで指定して打ち込むタイプのツールでは、そのままでは使えないのが問題です。オーディオプラグイン側をMIDIバイスのように設定して認識させることができれば可能かもしれませんが、一般的なオーディオプラグインDAWからしてみれば、MIDIキーボードはMIDI入力デバイスであり、MIDI入力デバイスをサポートしてしまえば終わりです。オーディオプラグインをわざわざ機能の制限されたMIDI音源として利用可能にする意味がほとんど無いのです。

MIDI virtual input device / portを作る

そういうわけで、DAWにも認識してもらえるようなソフトウェアMIDIキーボードを実現するための一番確実な方法は、ソフトウェアMIDI入力デバイスあるいはポートを作るところからです。

一般的には、CoreMIDIもALSAも、作成した仮想ポートにMIDIメッセージを「出力」すると、それが「入力」となって、その仮想ポートに接続されたアプリケーションが最終的にMIDIメッセージを受け取れることになります。というわけで、GUIから受け取ったキー入力イベントは、これらの仮想ポートに出力するようにすると良いでしょう。

実例

わたしが.NET開発者だった頃に作った個人的なプロジェクトですが、仮想ポートを活用して既存のソフトウェアMIDIキーボードアプリに仮想ポートを自動的に作成するようにしたものがあります。このアプリ上で入力したキーボードイベントから生成されたMIDIメッセージは、接続されたMIDI出力デバイスに送られるほか、仮想ポートに接続したDAWが受け取ることもできます。

github.com

自分で作成したアプリなので、入力はまあまあやりやすいので、たまに打ち込み作業で使っています。

いかがでしたか? 自分で好きなように楽器のインターフェースを作れるのは割と楽しいので、もし興味が出てきたらぜひ試してみてください。

DAW・シーケンサーエンジンを支える技術@M3 2019秋

前日の晩になって書いて公開するというのは何とも手遅れ感があるわけですが、明日10/27のM3 2019秋でおよそ1年半ぶりくらいに同人誌をまとめました。サークルはサ-22x (ginga)です。参加する予定のある人・音楽同人イベントに興味がある人は、ぜひ遊びに来てください。

https://atsushieno.github.io/ginga/m3-2019a.html

f:id:atsushieno:20191026200034p:plain
表紙

内容については上記特設サイトを見てもらいたいのですが、今回は「シーケンサーエンジン」についての解説です。概ねtracktion_engineのようなDAWのバックエンド技術…要するにDAWのうちGUIの実装から独立している部分…について、全体的に俯瞰するような内容を目指しています。60ページという、いつになく薄い(!)内容で、トピックも多岐にわたるため、個々の内容についてはほとんど踏み込まず、キーワードを列挙して読者が関心をもった分野は読者に個別に深入りして本書の範囲を超えて調べてもらう…というきっかけをたくさん作るための内容になっています。

筆者としては技術書典4の時に出したMonoDevelop Masters Book(に相当する部分)の「IDEの技術を因数分解して個別に解説することで、誰でもIDE(の一部)を作れるようになる」という目標を、今度はDAWに向けてみよう、と考えながら書いたものです。自分の音楽ソフト開発経験がまだ浅いので、MonoDevelop Masters Bookと比べるとだいぶん表層的な内容になっているのが惜しいところですが、商業本も含めてこのような内容の音楽技術書は今まで出ていなかったと思うので、まずは初めの一歩を踏み出しておきたくて書いたものです。

もっとも、実のところ本書で一番よくやったと思っているのは表紙で、この方面に関する知識がゼロのいつもの絵師に何とかお願いして描いてもらえるように、細かく仕様をまとめて「難しすぎる…!」と言われながらも、何とか説明しきって、結果的にこれ以上無いくらい期待通りの「謎の楽器を操るバンド」を仕上げてもらえたのでした。楽器、わかる人にはわかる感じのものになっています。一番右の水晶玉っぽいテルミンだけはモデルがありません。適当に思いつきました。

当日の展示内容

当日は同書の頒布、旧譜?新婦?の頒布のほか、同アルバム楽曲のMIDI版を、Android端末上でmasterに取り込まれたfluidsynthを使用して再生するデモも行おうと思っています。シンプルなGM互換楽器を多用した楽曲が多いせいか、意外と実用的に聴けます。

敗戦の弁

ここからは反省です。(今書くのか)

今回始めてM3に参加するにあたって、当初は3月の幻想音楽祭と同様、自作ツールに基づいて楽曲を創作してアルバムとして仕上げるつもりでした。ところが申し込んで当選待ちの間に、何の因果か自分の予想を裏切るタイミングでフルタイムの仕事をすることになり、完全に創作のために割ける時間が無いどころか、MMLコンパイラ等のメンテナンスと新ツールの開発すら覚束ない状況になってしまい、結果的に夏の初め頃にあらかたまとめてあった本書のみが出展の目玉ということになりました。

とはいえ、M3参加は初めてなので、3月に頒布した作品もM3的には新作ということになります。

本来であれば、前回MMLからSMFを生成するところまでしか出来なかったテキストツール部分を、今回は先日まとめたオーディオプラグイン用音楽プレイヤーまで延伸して、MMLプラグイン定義だけで出来るところまで「ソースを読めて創作できる」ところまでもっていって、きちんとdogfoodingして当日展示するつもりだったのですが、実際できたのはproof of conceptとしてのツール上で何とか音楽を再生できたところまででした。まあここまででもそれなりに面白いのですが、現実的に自分が作曲作業に着手するに至らないレベルで改良の余地が多大にあるので、これを紹介・宣伝するのは筋が違うだろうと考えました。ブースでの雑談ていどに留めておこうと思います。

この方面はもっとじっくり時間を確保して開発する必要があると考えています。ただ個人的にもうひとつ未公開のプロジェクトもあってそれも進めないといけないと思っているので、今回の反省を踏まえて来月以降はフルタイムワーカーを辞して自分のプロジェクト開発にもっと時間を割いていく予定です。(もっとも来月はADC2019に参加することもあって、まだすぐには進捗しなさそうですが。)